From 7fad23db39d1625a6088ef3ccd2b549f59b74693 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Thu, 3 Feb 2022 17:31:42 +0000 Subject: [PATCH 01/44] Capability Controllers --- flips/20220203-capability-controllers.md | 342 +++++++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 flips/20220203-capability-controllers.md diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md new file mode 100644 index 000000000..919a728e8 --- /dev/null +++ b/flips/20220203-capability-controllers.md @@ -0,0 +1,342 @@ +# Capability controllers + +| Status | Proposed | +| :------------ | :------------------------------------------------- | +| **FLIP #** | [NNN](https://github.com/onflow/flow/pull/NNN) | +| **Author(s)** | Janez Podhostnik (janez.podhostnik@dapperlabs.com) | +| **Updated** | 2022-02-03 | + +## Preface + +Cadence encourages a [capability-based security](https://en.wikipedia.org/wiki/Capability-based_security) model as described on the Flow [doc site](https://docs.onflow.org/cadence/language/capability-based-access-control). Capabilities are themselves a new concept that most Cadence programmers need to understand, but the API for syntax around Capabilities (especially the notion of “links” and “linking”), and the associated concepts of the public and private storage domains, lead to Capabilities being make this concept even more confusing and awkward to use. This proposal suggests that we could get rid of links entirely, and replace them with Capability Controllers (henceforth referred to as CapCons) which could make Capabilities easier to understand, and easier to work with. + +The following is a quick refresher of the current state of the Capabilities API (from the [flow doc site](https://docs.onflow.org/cadence/language/capability-based-access-control)). + +### Example Resource definition + +In the following examples let's assume that the following interface and resource type that implements the interface are defined. + +```cadence +pub resource interface HasCount { + pub count: Int +} + +pub resource Counter: HasCount { + pub var count: Int + + pub init(count: Int) { + self.count = count + } +} +``` + +The _issuer_ (authAccount) has also created an instance of this resource and saved it in its private storage. + +```cadence +authAccount.save(<-create Counter(count: 42), to: /storage/counter) +``` + +### Public Capabilities + +To allow anyone (read) access to the `count` field on the counter, the _issuer_ needs to create a public typed link at a chosen path that points to their stored counter resource. + +```cadence +authAccount.link<&{HasCount}>(/public/hasCount, target: /storage/counter) +``` + +Anyone can now read the `count` of the counter resource via the `HasCount` interface. This can be done by + +- getting the PublicAccount object of the issuer (using the issuers address), +- then getting a typed capability from the chosen path, +- then finally calling borrow on that capability to get a reference to the instance of the counter (constrained by the `HasCount` interface) + +```cadence +let publicAccount = getAccount(issuerAddress) +let countCap = publicAccount.getCapability<&{HasCount}>(/public/hasCount) +let countRef = countCap.borrow()! +countRef.count +``` + +### Private Capabilities + +To allow only certain accounts/resources (read) access to the `count` field on the counter, the _issuer_ needs to create a private typed link at a chosen path that points to their stored counter resource. + +```cadence +authAccount.link<&{HasCount}>(/private/hasCount, target: /storage/counter) +``` + +The receiving party needs to offer a public way to receive `&{HasCount}` capabilities. + +```cadence +// this would probably be defined on the same contract as `HasCount` +pub resource interface HasCountReceiverPublic { + pub fun addCapability(cap: Capability<&{HasCount}>) +} + +pub resource HasCountReceiver: HasCountReceiverPublic { + + pub var hasCountCapability: Capability<&{HasCount}>? + + init() { + self.hasCountCapability = nil + } + + pub fun addCapability(cap: Capability<&{HasCount}>) { + self.hasCountCapability = cap + } +} + +//... +// this is the receiver account setup +let hasCountReceiver <- HasCountReceiver() + +receivingPartyAuthAccount.save(<-hasCountReceiver, to: /storage/hasCountReceiver) +receivingPartyAuthAccount.link<&{HasCountReceiverPublic}>(/public/hasCountReceiver, + target: /storage/hasCountReceive) +``` + +With this in place the _issuer_ can create a capability from its private link and send it to this receiver. + +```cadence +let countCap = authAccount.getCapability<&{HasCount}>(/private/hasCount) + +let publicAccount = getAccount(receivingPartyAddress) +let countReceiverCap = publicAccount.getCapability<&{HasCountReceiverPublic}>(/public/hasCountReceiver) +let countReceiverRef = countCap.borrow()! +countRef.addCapability(cap: countCap) +``` + +The receiving party can then use this capability when they wish. + +### Capability API requirements + +There are two requirements of the capability system that must be satisfied. + +- **Revocation**: Any capability must be revocable by the issuer. +- **Redirection**: The issuer should have the ability to redirect the capability to a different (compatible) object. + +Revocation can be currently done by using the `unlink` function. + +In the public example this would mean calling `authAccount.unlink<&{HasCount}>(/public/hasCount)` which would invalidate (break) all capabilities created from this public path (both those created before unlink was called and those created after unlink was called). + +In the private example the call would change to `authAccount.unlink<&{HasCount}>(/private/hasCount`. It is important to note that if the issuer wants to have the ability to revoke/redirect capabilities in a more granular way (instead of doing them all at once), the solution is to create multiple private linked paths (e.g. `/private/hasCountAlice`, `/private/hasCountBob`). + +If a path that was unlinked is linked again all the capabilities created from that path resume working. This can be used to redirect a capability to a different object, but can also be dangerous if done unintentionally, reviving a previously revoked capability. + +### Capabilities are values + +Capabilities are a value type. This means that any capability that an account has access to can be copied, and potentially given to someone else. The copied capability will use the link on the same path as the original capability + +## Problem statement + +There are two potential pain points in the current capability API. + +The first one is that capabilities/linking/revoking/redirecting are hard to understand for new developers that are coming to Flow, as that is a lot of new concepts to grasp before one can get started. + +The second is that issuing and managing private capabilities that can be revoked at a granular level, by creating a custom private linked path for each capability, is difficult. + +- Unless the path name includes some hints, there is no way to know when a path was created, thus making it more difficult to remember why a certain capability was issued. +- If an old path (that has been unlinked) is accidentally reused and re-linked, this will revive a capability that is supposed to be revoked. +- Copying a capability is easy (since it is a value type) but doing so might give unintended access to third parties. + +## Suggested change + +The suggested change addresses these pain points by: + +- Removing (or abstracting away) the concept of links. +- Making it easier to create new capabilities (with individual links). +- Making capabilities resources, so that it is more difficult to give unintended access to third parties. +- Offering a way to iterate through capabilities on a storage path. +- Removing the need to have a `/private/`domain. +- Changing the `/public/` domain to be able to store capabilities (and only capabilities). +- Introducing Capability Controllers (CapCons) that handle management of capabilities. + +### Capabilities as Resources + +Changing capabilities into resources is trying to address two problems. + +The first problem is that capabilities as resources would not be able to be copied. While a reference to a capability can still be created and passed on, this is a more explicit process than just simply creating a duplicate of a capability. + +The second problem is a revocation problem. With capabilities as values the following scenario can happen: + +Alice created capabilities B and C and gave them to Bob and Charlie respectively. Bob then also copied his capability (marked as B’) and gave it to Dan. The picture now looks like this. + +- Bob has capability B +- Charlie has capability C +- Dan has capability B’ + +Revoking C yields the expected result that Charlie can no longer use his capability. However, revoking B also revokes all copies of B, so both Bob’s and Dan’s capabilities are revoked. This is not very intuitive at first glance, as there is little differnce between the capabilities + +B and C, but Dan’s ability to use his capability depends on which copy he has. + +With capabilities as resources this scenario would not have occurred as there is no way to copy B to create B’. If Dan also needs this capability, Alice must create a capability D to give to him. However this also means that there is no way for Bob to directly grant this capability to someone else (without losing his own). + +Dan could also have a reference to the capability B (&B), but in this case Dan knows that his capability is just a reference, and that if B is revoked it makes sense that his reference to B also stops working. + +### Accounts public domain as a capability storage + +This part of the change proposes that accounts can store capabilities in their public domain. Capabilities would be borrowed by anyone that needs to use them. The `borrowCapability `method would be added to the PublicAccount which would be equivalent to borrowing the capability then calling borrow on it. + +```cadence +let publicAccount = getAccount(issuerAddress) +let countCap = publicAccount.borrowCapability<&{HasCount}>(/public/hasCount)! +countRef.count +``` + +### Capability Controllers (CapCons) + +Each Capability would have an associated CapCon that would be created together with the Capability (Capabilities and Capability Controllers are in a 1 to 1 relation). The data associated with CapCons would be stored in arrays, so that each storage path on an account has an array of CapCons of Capabilities issued from that storage path. + +CapCons are a non-storable object. + +Capabilities also have an address field and a target field. The target field is the storage path of the targeted object. Creating a capability from a capability should be illegal. + +The definition of the `CapabilityController` . + +```cadence +// CapabilityController can be retrieved via: +// - authAccount.getControllers(path: StoragePath): [CapabilityController] +// - authAccount.getController(capabilityId: UInt64): CapabilityController? +struct CapabilityController { + // The block height when the capability was created. + let issueHeight: UInt64 + // The id of the related capability + // This is the UUiD of the created capability + let capabilityId: UInt64 + + // Is the capability revoked. + fun isRevoked(): Bool + // The target storage path. + fun target(): StoragePath + // Revoke the capability. + // Returns true if successfully revoked. + fun revoke(): Bool + // Restore the capability. + // Returns true if successfully restored. + fun restore(): Bool + // Retarget the capability. + // Returns if successfully restored. + // This would move the CapCon from one CapCon array to another + fun retarget(target: StoragePath): Bool +} +``` + +The auth account would get new methods to get CapCons in order to manage capabilities: + +```cadence +// Get all capability controllers for capabilities that target this storage path +fun getControllers(path: StoragePath): [CapabilityController] +// Get capability controller for capability with the specified id +// If the id does not reference an existing capability +// or the capability does not target a storage path on this address, return nil +fun getController(capabilityId: UInt64): CapabilityController? +``` + +Some methods would be removed from the AuthAccount object as they are no longer needed: + +```cadence +fun link(_ newCapabilityPath: CapabilityPath, target: Path): Capability? +fun getLinkTarget(_ path: CapabilityPath): Path? +fun unlink(_ path: CapabilityPath) +``` + +The` `method`getCapability`would be renamed to `issueCapability `to reflect the fact that a new capability is created every time. + +And also from the PublicAccount: + +```cadence +fun getCapability(_ path: PublicPath): Capability +fun getLinkTarget(_ path: CapabilityPath): Path? +``` + +This would remove all references to `Link`s. + +### Impact of the solution + +#### Changes for capability consumers + +The following pattern: + +```cadence +let publicAccount = getAccount(issuerAddress) +let countCap = publicAccount.getCapability<&{HasCount}>(/public/hasCount) +let countRef = countCap.borrow()! +countRef.count +``` + +Would change to: + +```cadence +let publicAccount = getAccount(issuerAddress) +let countCap = publicAccount.borrow>(/public/hasCount)! +let countRef = countCap.borrow()! +countRef.count +``` + +Or using the `borrowCapability `shorthand: + +```cadence +let publicAccount = getAccount(issuerAddress) +let countRef = publicAccount.borrowCapability<&{HasCount}>(/public/hasCount)! +countRef.count +``` + +Consuming private capabilities would change in the way that capabilities are resources and must be stored as such. The borrowing part would be the same. + +#### Changes for capability issuers + +There would be more change on the issuer's side. Most notably creating a public capability would look like this. + +```cadence +let countCap <- authAccount.issueCapability<&{HasCount}>(/storage/counter) +authAccount.save(<- countCap, to: /public/hasCount) +``` + +Unlinking and relinking issued capabilities would change to getting a CapCon and calling the appropriate methods. + +```cadence +let capCon = authAccount.getController(capabilityId: capabilityId) + +capCon.revoke() +// or +capCon.restore() +// or +capCon.relink(target: /storage/counter2) +``` + +This example assumes that the capability id is known. This is always the case for capabilities in the accounts public domain, since the account has access to those directly. For private capabilities that were given to someone else this can be achieved by keeping an on-chain or an off-chain list of capability ids and some extra identifying information (for example the address of the receiver of the capability). If no such list was kept, the issuer can use the information on the CapCons retrieved through`authAccount.getControllers(path: StoragePath)`to find the right id. + +#### Capability Minters + +In certain situations it is required that an issuer delegates issuing capabilities to someone else. In this case the following approach can be used. + +Let's assume that the issuer defined a `AdminInterface` resource interface and a `Main` resource (besides the `Counter` and `HasCount` from previous examples). + +```cadence +public resource interface AdminInterface { + fun createCountCap(): @Capability<&{HasCount}> +} +public resource Main : AdminInterface { + fun createCountCap(): @Capability<&{HasCount}> { + return <- self.account.getCapability<&{HasCount}>(/storage/counter) + } +} +``` + +The issuer can then store a `Main` resource in their storage and give the capability to call it to a trusted party. The trusted party can then create `&{HasCount} `capabilities at will. + +```cadence +authAccount.save(<-create Main(), to: /storage/counterMinter) +let countMinterCap <- authAccount.getCapability(/storage/counterMinter) +countMinterCap //give this to a trusted party +``` + +It is worth noting that everytime the `AdminInterface` is used a CapCon is created in the issuers storage taking up some resources. + +## Sources + +1. Miller, Mark & Yee, Ka-ping & Shapiro, Jonathan. (2003). Capability Myths Demolished. +2. [Capability-Based Security](https://en.wikipedia.org/wiki/Capability-based_security) +3. [Flow Doc Site](https://docs.onflow.org/cadence/language/capability-based-access-control) +4. [Flow Doc Site: capability receiver](https://docs.onflow.org/cadence/design-patterns/#capability-receiver) +5. [Flow Forum Post](https://forum.onflow.org/t/private-capability-revoke/1997) From 25107db82f85f2e4ced5e0fe8c1b27d02f133f09 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Thu, 3 Feb 2022 17:34:03 +0000 Subject: [PATCH 02/44] Update flip number --- flips/20220203-capability-controllers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index 919a728e8..caba738eb 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -2,7 +2,7 @@ | Status | Proposed | | :------------ | :------------------------------------------------- | -| **FLIP #** | [NNN](https://github.com/onflow/flow/pull/NNN) | +| **FLIP #** | [798](https://github.com/onflow/flow/pull/798) | | **Author(s)** | Janez Podhostnik (janez.podhostnik@dapperlabs.com) | | **Updated** | 2022-02-03 | From 3a2fe24a8adab078a0992f14c13f0389f2b758be Mon Sep 17 00:00:00 2001 From: Paul Gebheim Date: Tue, 17 May 2022 14:09:19 -0700 Subject: [PATCH 03/44] Apply suggestions from code review Co-authored-by: Joshua Hannan --- flips/20220203-capability-controllers.md | 27 ++++++++++++------------ 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index caba738eb..29750f412 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -8,7 +8,7 @@ ## Preface -Cadence encourages a [capability-based security](https://en.wikipedia.org/wiki/Capability-based_security) model as described on the Flow [doc site](https://docs.onflow.org/cadence/language/capability-based-access-control). Capabilities are themselves a new concept that most Cadence programmers need to understand, but the API for syntax around Capabilities (especially the notion of “links” and “linking”), and the associated concepts of the public and private storage domains, lead to Capabilities being make this concept even more confusing and awkward to use. This proposal suggests that we could get rid of links entirely, and replace them with Capability Controllers (henceforth referred to as CapCons) which could make Capabilities easier to understand, and easier to work with. +Cadence encourages a [capability-based security](https://en.wikipedia.org/wiki/Capability-based_security) model as described on the Flow [doc site](https://docs.onflow.org/cadence/language/capability-based-access-control). Capabilities are themselves a new concept that most Cadence programmers need to understand, but the API for syntax around Capabilities (especially the notion of “links” and “linking”), and the associated concepts of the public and private storage domains, lead to Capabilities being even more confusing and awkward to use. This proposal suggests that we could get rid of links entirely, and replace them with Capability Controllers (henceforth referred to as CapCons) which could make Capabilities easier to understand, and easier to work with. The following is a quick refresher of the current state of the Capabilities API (from the [flow doc site](https://docs.onflow.org/cadence/language/capability-based-access-control)). @@ -30,10 +30,10 @@ pub resource Counter: HasCount { } ``` -The _issuer_ (authAccount) has also created an instance of this resource and saved it in its private storage. +The _issuer_ (`AuthAccount`) has also created an instance of this resource and saved it in its private storage. ```cadence -authAccount.save(<-create Counter(count: 42), to: /storage/counter) +AuthAccount.save(<-create Counter(count: 42), to: /storage/counter) ``` ### Public Capabilities @@ -41,12 +41,12 @@ authAccount.save(<-create Counter(count: 42), to: /storage/counter) To allow anyone (read) access to the `count` field on the counter, the _issuer_ needs to create a public typed link at a chosen path that points to their stored counter resource. ```cadence -authAccount.link<&{HasCount}>(/public/hasCount, target: /storage/counter) +AuthAccount.link<&{HasCount}>(/public/hasCount, target: /storage/counter) ``` Anyone can now read the `count` of the counter resource via the `HasCount` interface. This can be done by -- getting the PublicAccount object of the issuer (using the issuers address), +- getting the `PublicAccount` object of the issuer (using the issuers address), - then getting a typed capability from the chosen path, - then finally calling borrow on that capability to get a reference to the instance of the counter (constrained by the `HasCount` interface) @@ -62,7 +62,7 @@ countRef.count To allow only certain accounts/resources (read) access to the `count` field on the counter, the _issuer_ needs to create a private typed link at a chosen path that points to their stored counter resource. ```cadence -authAccount.link<&{HasCount}>(/private/hasCount, target: /storage/counter) +AuthAccount.link<&{HasCount}>(/private/hasCount, target: /storage/counter) ``` The receiving party needs to offer a public way to receive `&{HasCount}` capabilities. @@ -117,9 +117,9 @@ There are two requirements of the capability system that must be satisfied. Revocation can be currently done by using the `unlink` function. -In the public example this would mean calling `authAccount.unlink<&{HasCount}>(/public/hasCount)` which would invalidate (break) all capabilities created from this public path (both those created before unlink was called and those created after unlink was called). +In the public example this would mean calling `authAccount.unlink<&{HasCount}>(/public/hasCount)` which would invalidate (break) all capabilities created from this public path (both those created before unlink was called and those created after unlink was called). -In the private example the call would change to `authAccount.unlink<&{HasCount}>(/private/hasCount`. It is important to note that if the issuer wants to have the ability to revoke/redirect capabilities in a more granular way (instead of doing them all at once), the solution is to create multiple private linked paths (e.g. `/private/hasCountAlice`, `/private/hasCountBob`). +In the private example the call would change to `authAccount.unlink<&{HasCount}>(/private/hasCount)`. It is important to note that if the issuer wants to have the ability to revoke/redirect capabilities in a more granular way (instead of doing them all at once), the solution is to create multiple private linked paths (e.g. `/private/hasCountAlice`, `/private/hasCountBob`). If a path that was unlinked is linked again all the capabilities created from that path resume working. This can be used to redirect a capability to a different object, but can also be dangerous if done unintentionally, reviving a previously revoked capability. @@ -153,9 +153,9 @@ The suggested change addresses these pain points by: ### Capabilities as Resources -Changing capabilities into resources is trying to address two problems. +Changing capabilities into resources attempts to address two problems. -The first problem is that capabilities as resources would not be able to be copied. While a reference to a capability can still be created and passed on, this is a more explicit process than just simply creating a duplicate of a capability. +The first problem is that, as resources, capabilities would not be able to be copied. While a reference to a capability can still be created and passed on, this is a more explicit process than just simply creating a duplicate of a capability. The second problem is a revocation problem. With capabilities as values the following scenario can happen: @@ -166,7 +166,6 @@ Alice created capabilities B and C and gave them to Bob and Charlie respectively - Dan has capability B’ Revoking C yields the expected result that Charlie can no longer use his capability. However, revoking B also revokes all copies of B, so both Bob’s and Dan’s capabilities are revoked. This is not very intuitive at first glance, as there is little differnce between the capabilities - B and C, but Dan’s ability to use his capability depends on which copy he has. With capabilities as resources this scenario would not have occurred as there is no way to copy B to create B’. If Dan also needs this capability, Alice must create a capability D to give to him. However this also means that there is no way for Bob to directly grant this capability to someone else (without losing his own). @@ -175,7 +174,7 @@ Dan could also have a reference to the capability B (&B), but in this case Dan k ### Accounts public domain as a capability storage -This part of the change proposes that accounts can store capabilities in their public domain. Capabilities would be borrowed by anyone that needs to use them. The `borrowCapability `method would be added to the PublicAccount which would be equivalent to borrowing the capability then calling borrow on it. +This part of the change proposes that accounts can store capabilities in their public domain. Capabilities would be borrowed by anyone that needs to use them. The `borrowCapability `method would be added to the PublicAccount which would be equivalent to how we currently get the capability then call `borrow` on it. ```cadence let publicAccount = getAccount(issuerAddress) @@ -240,7 +239,7 @@ fun getLinkTarget(_ path: CapabilityPath): Path? fun unlink(_ path: CapabilityPath) ``` -The` `method`getCapability`would be renamed to `issueCapability `to reflect the fact that a new capability is created every time. +The method `getCapability` would be renamed to `issueCapability `to reflect the fact that a new capability is created every time. And also from the PublicAccount: @@ -310,7 +309,7 @@ This example assumes that the capability id is known. This is always the case fo In certain situations it is required that an issuer delegates issuing capabilities to someone else. In this case the following approach can be used. -Let's assume that the issuer defined a `AdminInterface` resource interface and a `Main` resource (besides the `Counter` and `HasCount` from previous examples). +Let's assume that the issuer defined an `AdminInterface` resource interface and a `Main` resource (besides the `Counter` and `HasCount` from previous examples). ```cadence public resource interface AdminInterface { From 8e9d62565b533d00e33d16e5d047c1fff70fe2b4 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Thu, 11 Aug 2022 15:43:23 +0200 Subject: [PATCH 04/44] rename authaccount to issuer --- flips/20220203-capability-controllers.md | 32 ++++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index 29750f412..7902d7231 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -33,7 +33,7 @@ pub resource Counter: HasCount { The _issuer_ (`AuthAccount`) has also created an instance of this resource and saved it in its private storage. ```cadence -AuthAccount.save(<-create Counter(count: 42), to: /storage/counter) +issuer.save(<-create Counter(count: 42), to: /storage/counter) ``` ### Public Capabilities @@ -41,7 +41,7 @@ AuthAccount.save(<-create Counter(count: 42), to: /storage/counter) To allow anyone (read) access to the `count` field on the counter, the _issuer_ needs to create a public typed link at a chosen path that points to their stored counter resource. ```cadence -AuthAccount.link<&{HasCount}>(/public/hasCount, target: /storage/counter) +issuer.link<&{HasCount}>(/public/hasCount, target: /storage/counter) ``` Anyone can now read the `count` of the counter resource via the `HasCount` interface. This can be done by @@ -59,10 +59,10 @@ countRef.count ### Private Capabilities -To allow only certain accounts/resources (read) access to the `count` field on the counter, the _issuer_ needs to create a private typed link at a chosen path that points to their stored counter resource. +To allow only certain accounts/resources (read) access to the `count` field on the counter, the _issuer_ (of type `AuthAccount`) needs to create a private typed link at a chosen path that points to their stored counter resource. ```cadence -AuthAccount.link<&{HasCount}>(/private/hasCount, target: /storage/counter) +issuer.link<&{HasCount}>(/private/hasCount, target: /storage/counter) ``` The receiving party needs to offer a public way to receive `&{HasCount}` capabilities. @@ -98,7 +98,7 @@ receivingPartyAuthAccount.link<&{HasCountReceiverPublic}>(/public/hasCountReceiv With this in place the _issuer_ can create a capability from its private link and send it to this receiver. ```cadence -let countCap = authAccount.getCapability<&{HasCount}>(/private/hasCount) +let countCap = issuer.getCapability<&{HasCount}>(/private/hasCount) let publicAccount = getAccount(receivingPartyAddress) let countReceiverCap = publicAccount.getCapability<&{HasCountReceiverPublic}>(/public/hasCountReceiver) @@ -117,9 +117,9 @@ There are two requirements of the capability system that must be satisfied. Revocation can be currently done by using the `unlink` function. -In the public example this would mean calling `authAccount.unlink<&{HasCount}>(/public/hasCount)` which would invalidate (break) all capabilities created from this public path (both those created before unlink was called and those created after unlink was called). +In the public example this would mean calling `issuer.unlink<&{HasCount}>(/public/hasCount)` which would invalidate (break) all capabilities created from this public path (both those created before unlink was called and those created after unlink was called). -In the private example the call would change to `authAccount.unlink<&{HasCount}>(/private/hasCount)`. It is important to note that if the issuer wants to have the ability to revoke/redirect capabilities in a more granular way (instead of doing them all at once), the solution is to create multiple private linked paths (e.g. `/private/hasCountAlice`, `/private/hasCountBob`). +In the private example the call would change to `issuer.unlink<&{HasCount}>(/private/hasCount)`. It is important to note that if the issuer wants to have the ability to revoke/redirect capabilities in a more granular way (instead of doing them all at once), the solution is to create multiple private linked paths (e.g. `/private/hasCountAlice`, `/private/hasCountBob`). If a path that was unlinked is linked again all the capabilities created from that path resume working. This can be used to redirect a capability to a different object, but can also be dangerous if done unintentionally, reviving a previously revoked capability. @@ -194,8 +194,8 @@ The definition of the `CapabilityController` . ```cadence // CapabilityController can be retrieved via: -// - authAccount.getControllers(path: StoragePath): [CapabilityController] -// - authAccount.getController(capabilityId: UInt64): CapabilityController? +// - AuthAccount.getControllers(path: StoragePath): [CapabilityController] +// - AuthAccount.getController(capabilityId: UInt64): CapabilityController? struct CapabilityController { // The block height when the capability was created. let issueHeight: UInt64 @@ -231,7 +231,7 @@ fun getControllers(path: StoragePath): [CapabilityController] fun getController(capabilityId: UInt64): CapabilityController? ``` -Some methods would be removed from the AuthAccount object as they are no longer needed: +Some methods would be removed from the `AuthAccount` object as they are no longer needed: ```cadence fun link(_ newCapabilityPath: CapabilityPath, target: Path): Capability? @@ -287,14 +287,14 @@ Consuming private capabilities would change in the way that capabilities are res There would be more change on the issuer's side. Most notably creating a public capability would look like this. ```cadence -let countCap <- authAccount.issueCapability<&{HasCount}>(/storage/counter) -authAccount.save(<- countCap, to: /public/hasCount) +let countCap <- issuer.issueCapability<&{HasCount}>(/storage/counter) +issuer.save(<- countCap, to: /public/hasCount) ``` Unlinking and relinking issued capabilities would change to getting a CapCon and calling the appropriate methods. ```cadence -let capCon = authAccount.getController(capabilityId: capabilityId) +let capCon = issuer.getController(capabilityId: capabilityId) capCon.revoke() // or @@ -303,7 +303,7 @@ capCon.restore() capCon.relink(target: /storage/counter2) ``` -This example assumes that the capability id is known. This is always the case for capabilities in the accounts public domain, since the account has access to those directly. For private capabilities that were given to someone else this can be achieved by keeping an on-chain or an off-chain list of capability ids and some extra identifying information (for example the address of the receiver of the capability). If no such list was kept, the issuer can use the information on the CapCons retrieved through`authAccount.getControllers(path: StoragePath)`to find the right id. +This example assumes that the capability id is known. This is always the case for capabilities in the accounts public domain, since the account has access to those directly. For private capabilities that were given to someone else this can be achieved by keeping an on-chain or an off-chain list of capability ids and some extra identifying information (for example the address of the receiver of the capability). If no such list was kept, the issuer can use the information on the CapCons retrieved through `issuer.getControllers(path: StoragePath)`to find the right id. #### Capability Minters @@ -325,8 +325,8 @@ public resource Main : AdminInterface { The issuer can then store a `Main` resource in their storage and give the capability to call it to a trusted party. The trusted party can then create `&{HasCount} `capabilities at will. ```cadence -authAccount.save(<-create Main(), to: /storage/counterMinter) -let countMinterCap <- authAccount.getCapability(/storage/counterMinter) +issuer.save(<-create Main(), to: /storage/counterMinter) +let countMinterCap <- issuer.getCapability(/storage/counterMinter) countMinterCap //give this to a trusted party ``` From 94e62d4b8e696960da0bb68f01faca97098de8a2 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Thu, 11 Aug 2022 15:49:03 +0200 Subject: [PATCH 05/44] minor text changes --- flips/20220203-capability-controllers.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index 7902d7231..2eb29f1bb 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -65,7 +65,7 @@ To allow only certain accounts/resources (read) access to the `count` field on t issuer.link<&{HasCount}>(/private/hasCount, target: /storage/counter) ``` -The receiving party needs to offer a public way to receive `&{HasCount}` capabilities. +The _receivingParty_ (`AuthAccount`) needs to offer a public way to receive `&{HasCount}` capabilities, and store them for later use. ```cadence // this would probably be defined on the same contract as `HasCount` @@ -90,8 +90,8 @@ pub resource HasCountReceiver: HasCountReceiverPublic { // this is the receiver account setup let hasCountReceiver <- HasCountReceiver() -receivingPartyAuthAccount.save(<-hasCountReceiver, to: /storage/hasCountReceiver) -receivingPartyAuthAccount.link<&{HasCountReceiverPublic}>(/public/hasCountReceiver, +receivingParty.save(<-hasCountReceiver, to: /storage/hasCountReceiver) +receivingParty.link<&{HasCountReceiverPublic}>(/public/hasCountReceiver, target: /storage/hasCountReceive) ``` @@ -195,13 +195,13 @@ The definition of the `CapabilityController` . ```cadence // CapabilityController can be retrieved via: // - AuthAccount.getControllers(path: StoragePath): [CapabilityController] -// - AuthAccount.getController(capabilityId: UInt64): CapabilityController? +// - AuthAccount.getController(capabilityID: UInt64): CapabilityController? struct CapabilityController { // The block height when the capability was created. let issueHeight: UInt64 // The id of the related capability - // This is the UUiD of the created capability - let capabilityId: UInt64 + // This is the UUID of the created capability + let capabilityID: UInt64 // Is the capability revoked. fun isRevoked(): Bool @@ -228,7 +228,7 @@ fun getControllers(path: StoragePath): [CapabilityController] // Get capability controller for capability with the specified id // If the id does not reference an existing capability // or the capability does not target a storage path on this address, return nil -fun getController(capabilityId: UInt64): CapabilityController? +fun getController(capabilityID: UInt64): CapabilityController? ``` Some methods would be removed from the `AuthAccount` object as they are no longer needed: @@ -294,7 +294,7 @@ issuer.save(<- countCap, to: /public/hasCount) Unlinking and relinking issued capabilities would change to getting a CapCon and calling the appropriate methods. ```cadence -let capCon = issuer.getController(capabilityId: capabilityId) +let capCon = issuer.getController(capabilityID: capabilityID) capCon.revoke() // or From b14135748e0cac3b80920a235b200d6da8785ee9 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Mon, 22 Aug 2022 16:01:32 +0200 Subject: [PATCH 06/44] Capabilities are values --- flips/20220203-capability-controllers.md | 102 ++++++++++++++++------- 1 file changed, 70 insertions(+), 32 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index 2eb29f1bb..bb12de5eb 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -4,11 +4,11 @@ | :------------ | :------------------------------------------------- | | **FLIP #** | [798](https://github.com/onflow/flow/pull/798) | | **Author(s)** | Janez Podhostnik (janez.podhostnik@dapperlabs.com) | -| **Updated** | 2022-02-03 | +| **Updated** | 2022-08-22 | ## Preface -Cadence encourages a [capability-based security](https://en.wikipedia.org/wiki/Capability-based_security) model as described on the Flow [doc site](https://docs.onflow.org/cadence/language/capability-based-access-control). Capabilities are themselves a new concept that most Cadence programmers need to understand, but the API for syntax around Capabilities (especially the notion of “links” and “linking”), and the associated concepts of the public and private storage domains, lead to Capabilities being even more confusing and awkward to use. This proposal suggests that we could get rid of links entirely, and replace them with Capability Controllers (henceforth referred to as CapCons) which could make Capabilities easier to understand, and easier to work with. +Cadence encourages a [capability-based security](d be clearer to Charlie that he received a capability t) model as described on the Flow [doc site](https://docs.onflow.org/cadence/language/capability-based-access-control). Capabilities are themselves a new concept that most Cadence programmers need to understand, but the API for syntax around Capabilities (especially the notion of “links” and “linking”), and the associated concepts of the public and private storage domains, lead to Capabilities being even more confusing and awkward to use. This proposal suggests that we could get rid of links entirely, and replace them with Capability Controllers (henceforth referred to as CapCons) which could make Capabilities easier to understand, and easier to work with. The following is a quick refresher of the current state of the Capabilities API (from the [flow doc site](https://docs.onflow.org/cadence/language/capability-based-access-control)). @@ -145,33 +145,11 @@ The suggested change addresses these pain points by: - Removing (or abstracting away) the concept of links. - Making it easier to create new capabilities (with individual links). -- Making capabilities resources, so that it is more difficult to give unintended access to third parties. - Offering a way to iterate through capabilities on a storage path. - Removing the need to have a `/private/`domain. - Changing the `/public/` domain to be able to store capabilities (and only capabilities). - Introducing Capability Controllers (CapCons) that handle management of capabilities. -### Capabilities as Resources - -Changing capabilities into resources attempts to address two problems. - -The first problem is that, as resources, capabilities would not be able to be copied. While a reference to a capability can still be created and passed on, this is a more explicit process than just simply creating a duplicate of a capability. - -The second problem is a revocation problem. With capabilities as values the following scenario can happen: - -Alice created capabilities B and C and gave them to Bob and Charlie respectively. Bob then also copied his capability (marked as B’) and gave it to Dan. The picture now looks like this. - -- Bob has capability B -- Charlie has capability C -- Dan has capability B’ - -Revoking C yields the expected result that Charlie can no longer use his capability. However, revoking B also revokes all copies of B, so both Bob’s and Dan’s capabilities are revoked. This is not very intuitive at first glance, as there is little differnce between the capabilities -B and C, but Dan’s ability to use his capability depends on which copy he has. - -With capabilities as resources this scenario would not have occurred as there is no way to copy B to create B’. If Dan also needs this capability, Alice must create a capability D to give to him. However this also means that there is no way for Bob to directly grant this capability to someone else (without losing his own). - -Dan could also have a reference to the capability B (&B), but in this case Dan knows that his capability is just a reference, and that if B is revoked it makes sense that his reference to B also stops working. - ### Accounts public domain as a capability storage This part of the change proposes that accounts can store capabilities in their public domain. Capabilities would be borrowed by anyone that needs to use them. The `borrowCapability `method would be added to the PublicAccount which would be equivalent to how we currently get the capability then call `borrow` on it. @@ -199,6 +177,8 @@ The definition of the `CapabilityController` . struct CapabilityController { // The block height when the capability was created. let issueHeight: UInt64 + // The Type of the capability + let CapabilityType: Type // The id of the related capability // This is the UUID of the created capability let capabilityID: UInt64 @@ -287,8 +267,8 @@ Consuming private capabilities would change in the way that capabilities are res There would be more change on the issuer's side. Most notably creating a public capability would look like this. ```cadence -let countCap <- issuer.issueCapability<&{HasCount}>(/storage/counter) -issuer.save(<- countCap, to: /public/hasCount) +let countCap = issuer.issueCapability<&{HasCount}>(/storage/counter) +issuer.save(countCap, to: /public/hasCount) ``` Unlinking and relinking issued capabilities would change to getting a CapCon and calling the appropriate methods. @@ -307,22 +287,33 @@ This example assumes that the capability id is known. This is always the case fo #### Capability Minters -In certain situations it is required that an issuer delegates issuing capabilities to someone else. In this case the following approach can be used. +In certain situations it is required that an issuer delegates issuing and revoking capabilities to someone else. In this case the following approach can be used. Let's assume that the issuer defined an `AdminInterface` resource interface and a `Main` resource (besides the `Counter` and `HasCount` from previous examples). ```cadence public resource interface AdminInterface { - fun createCountCap(): @Capability<&{HasCount}> + fun createCountCap(): Capability<&{HasCount}> + fun revokeCountCap(capabilityID: UInt64): Bool } public resource Main : AdminInterface { - fun createCountCap(): @Capability<&{HasCount}> { - return <- self.account.getCapability<&{HasCount}>(/storage/counter) + fun createCountCap(): Capability<&{HasCount}> { + return self.account.getCapability<&{HasCount}>(/storage/counter) + } + + fun revokeCountCap(capabilityID: UInt64): Bool { + if let capCon = self.account.getController(capabilityID: capabilityID) { + if capCon.Type != Type<&{HasCount}>() { + return false // we have only delegated the issuance/revocation of &{HasCount} capabilities + } + return capCon.revoke() + } + return false } } ``` -The issuer can then store a `Main` resource in their storage and give the capability to call it to a trusted party. The trusted party can then create `&{HasCount} `capabilities at will. +The issuer can then store a `Main` resource in their storage and give the capability to call it to a trusted party. The trusted party can then create and revoke `&{HasCount}` capabilities at will. ```cadence issuer.save(<-create Main(), to: /storage/counterMinter) @@ -330,7 +321,54 @@ let countMinterCap <- issuer.getCapability(/storage/counterMinter) countMinterCap //give this to a trusted party ``` -It is worth noting that everytime the `AdminInterface` is used a CapCon is created in the issuers storage taking up some resources. +It is worth noting that every time the `AdminInterface` is used a CapCon is created in the issuers storage taking up some resources. Revoking capabilities does not free any storage. + +## Issues not addressed in this FLIP + +### Unexpected revocation + +If the issuer creates a capability **A** and gives it to Alice and creates a capability **B** and gives it to Bob. Alice then copies her capability creating **A'** and gives it to Charlie, creating the following situation: + +- Alice: has **A** +- Bob: has **B** +- Charlie: has **A'** + +If the issuer decides to revoke **B**, Bob will no longer be able to use his capability. If the issuer revokes **A** Alice will not be able to use her capability, but perhaps unexpected to Charlie, he will also not be able to use **A'**. + +Addressing this issue so that it would be clearer to Charlie that he received a capability that is a copy of Alice's, and not his own instance is not in the scope of this FLIP. + +This could perhaps be addressed by: +- adding extra descriptors to capabilities (names) +- off chain tracking of capabilities + +Or can be addressed if the issuer creates a wrapper for the capability that the receiver unwraps. + +```cadence +pub resource WrappedCapability { + priv let unwraper: Address + priv let cap: Capability<&AnyResource> + + pub fun unwrap(unwraper: AuthAccount): Capability<&AnyResource>? { + if unwraper.address == self.unwraper { + return self.cap + } + return nil + } + + pub fun capabilityAddress(): Address { + return self.cap.address + } + + pub fun unwraperAddress(): Address { + return self.unwraper + } + + init(unwraper: Address, cap: Capability<&AnyResource>){ + self.unwraper = unwraper + self.cap =cap + } +} +``` ## Sources From 8f08496e3b67f74f4f650c88f0428b8e88a3fb3f Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Mon, 26 Sep 2022 21:18:05 +0200 Subject: [PATCH 07/44] More examples and some API changes --- flips/20220203-capability-controllers.md | 82 ++++++++++++++++++------ 1 file changed, 62 insertions(+), 20 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index bb12de5eb..208fe76db 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -172,15 +172,15 @@ The definition of the `CapabilityController` . ```cadence // CapabilityController can be retrieved via: -// - AuthAccount.getControllers(path: StoragePath): [CapabilityController] -// - AuthAccount.getController(capabilityID: UInt64): CapabilityController? +// - AuthAccount::getControllers(path: StoragePath): [CapabilityController] +// - AuthAccount::getController(capabilityID: UInt64): CapabilityController? struct CapabilityController { // The block height when the capability was created. let issueHeight: UInt64 - // The Type of the capability + // The Type of the capability. let CapabilityType: Type - // The id of the related capability - // This is the UUID of the created capability + // The id of the related capability. + // This is the UUID of the created capability. let capabilityID: UInt64 // Is the capability revoked. @@ -188,15 +188,11 @@ struct CapabilityController { // The target storage path. fun target(): StoragePath // Revoke the capability. - // Returns true if successfully revoked. - fun revoke(): Bool - // Restore the capability. - // Returns true if successfully restored. - fun restore(): Bool + fun revoke() // Retarget the capability. - // Returns if successfully restored. - // This would move the CapCon from one CapCon array to another - fun retarget(target: StoragePath): Bool + // This would move the CapCon from one CapCon array to another. + // If the new target is not a valid target this will panic. + fun retarget(target: StoragePath) } ``` @@ -271,20 +267,36 @@ let countCap = issuer.issueCapability<&{HasCount}>(/storage/counter) issuer.save(countCap, to: /public/hasCount) ``` -Unlinking and relinking issued capabilities would change to getting a CapCon and calling the appropriate methods. +Unlinking and retargeting issued capabilities would change to getting a CapCon and calling the appropriate methods. ```cadence let capCon = issuer.getController(capabilityID: capabilityID) capCon.revoke() // or -capCon.restore() -// or -capCon.relink(target: /storage/counter2) +capCon.retarget(target: /storage/counter2) ``` This example assumes that the capability id is known. This is always the case for capabilities in the accounts public domain, since the account has access to those directly. For private capabilities that were given to someone else this can be achieved by keeping an on-chain or an off-chain list of capability ids and some extra identifying information (for example the address of the receiver of the capability). If no such list was kept, the issuer can use the information on the CapCons retrieved through `issuer.getControllers(path: StoragePath)`to find the right id. +#### Pet names for issued capabilities + +If needed the `account.getCapability(path)` can be wrapped into a smart contract function, so that the issuer can more easily keep track of what was issued. + +```cadence +// inside the Counter contract +access(account) petNames: {String; UInt64} + +access(account) issueHasCount(petName: String): Capability<&{HasCount}> { + // for brevity this function is not handling pet name collision + let cap = self.account.issueCapability<&{HasCount}>(/storage/counter) + self.petNames[petName] = cap.capabilityID + return cap +} +``` + +This allows the account to later access `petNames` when it needs to revoke/retarget a specific capability. This can also be used not just for pet names, but to add other metadata to issued capabilities. + #### Capability Minters In certain situations it is required that an issuer delegates issuing and revoking capabilities to someone else. In this case the following approach can be used. @@ -301,14 +313,13 @@ public resource Main : AdminInterface { return self.account.getCapability<&{HasCount}>(/storage/counter) } - fun revokeCountCap(capabilityID: UInt64): Bool { + fun revokeCountCap(capabilityID: UInt64) { if let capCon = self.account.getController(capabilityID: capabilityID) { if capCon.Type != Type<&{HasCount}>() { return false // we have only delegated the issuance/revocation of &{HasCount} capabilities } - return capCon.revoke() + capCon.revoke() } - return false } } ``` @@ -323,6 +334,37 @@ countMinterCap //give this to a trusted party It is worth noting that every time the `AdminInterface` is used a CapCon is created in the issuers storage taking up some resources. Revoking capabilities does not free any storage. +In this example the delegatee with the `Capability<&{AdminInterface}>` can revoke any capability of type `&{HasCount}` even those that someone else created. This is sometimes desired (IT admins with the capability to issue purchase_hardware capabilities should have the ability to revoke what other IT admins issued), but sometimes we would like the delegatee to only revoke what it issued. If that is the case, another resource can be created `ScopedMain` that serves to limit which capabilities can be revoked. + +```cadence +public resource ScopedMain : AdminInterface { + public delegatorTag: String + public inner: Capability<&{AdminInterface}> + public issued: {UInt64:Bool} + + public init(inner: Capability<&{AdminInterface}>, delegatorTag: String) { + self.delegatorTag = delegatorTag + self.inner = inner + self.issued = {} + } + + + fun createCountCap(): Capability<&{HasCount}> { + let capability = self.inner.createCountCap() + self.issued[capability.capabilityID] = true + return capability + } + + fun revokeCountCap(capabilityID: UInt64) { + if self.issued.containsKey(capabilityID) { + self.issued.remove(key: capabilityID) + self.inner.revokeCountCap() + } + } +} +``` + + ## Issues not addressed in this FLIP ### Unexpected revocation From 08afdd7e4ca2f51a96dbaefad690c3f2453918db Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Tue, 11 Oct 2022 16:27:35 +0200 Subject: [PATCH 08/44] remove some not relevant text --- flips/20220203-capability-controllers.md | 31 ------------------------ 1 file changed, 31 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index 208fe76db..17578cd24 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -256,8 +256,6 @@ let countRef = publicAccount.borrowCapability<&{HasCount}>(/public/hasCount)! countRef.count ``` -Consuming private capabilities would change in the way that capabilities are resources and must be stored as such. The borrowing part would be the same. - #### Changes for capability issuers There would be more change on the issuer's side. Most notably creating a public capability would look like this. @@ -383,35 +381,6 @@ This could perhaps be addressed by: - adding extra descriptors to capabilities (names) - off chain tracking of capabilities -Or can be addressed if the issuer creates a wrapper for the capability that the receiver unwraps. - -```cadence -pub resource WrappedCapability { - priv let unwraper: Address - priv let cap: Capability<&AnyResource> - - pub fun unwrap(unwraper: AuthAccount): Capability<&AnyResource>? { - if unwraper.address == self.unwraper { - return self.cap - } - return nil - } - - pub fun capabilityAddress(): Address { - return self.cap.address - } - - pub fun unwraperAddress(): Address { - return self.unwraper - } - - init(unwraper: Address, cap: Capability<&AnyResource>){ - self.unwraper = unwraper - self.cap =cap - } -} -``` - ## Sources 1. Miller, Mark & Yee, Ka-ping & Shapiro, Jonathan. (2003). Capability Myths Demolished. From 84f44227ec19305961cc23d2f80130687b5131d0 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Tue, 18 Oct 2022 18:09:40 +0200 Subject: [PATCH 09/44] update CapCons --- flips/20220203-capability-controllers.md | 95 +++++++++++++++++++----- 1 file changed, 77 insertions(+), 18 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index 17578cd24..be34b720b 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -1,10 +1,9 @@ -# Capability controllers +| status: Proposed +| flip: [798](https://github.com/onflow/flow/pull/798) +| author: Janez Podhostnik (janez.podhostnik@dapperlabs.com) +| updated: 2022-10-18 -| Status | Proposed | -| :------------ | :------------------------------------------------- | -| **FLIP #** | [798](https://github.com/onflow/flow/pull/798) | -| **Author(s)** | Janez Podhostnik (janez.podhostnik@dapperlabs.com) | -| **Updated** | 2022-08-22 | +# Capability controllers ## Preface @@ -38,7 +37,7 @@ issuer.save(<-create Counter(count: 42), to: /storage/counter) ### Public Capabilities -To allow anyone (read) access to the `count` field on the counter, the _issuer_ needs to create a public typed link at a chosen path that points to their stored counter resource. +To allow anyone (read) access to the `count` field on the counter (during a transaction), the _issuer_ needs to create a public typed link at a chosen path that points to their stored counter resource. ```cadence issuer.link<&{HasCount}>(/public/hasCount, target: /storage/counter) @@ -147,19 +146,8 @@ The suggested change addresses these pain points by: - Making it easier to create new capabilities (with individual links). - Offering a way to iterate through capabilities on a storage path. - Removing the need to have a `/private/`domain. -- Changing the `/public/` domain to be able to store capabilities (and only capabilities). - Introducing Capability Controllers (CapCons) that handle management of capabilities. -### Accounts public domain as a capability storage - -This part of the change proposes that accounts can store capabilities in their public domain. Capabilities would be borrowed by anyone that needs to use them. The `borrowCapability `method would be added to the PublicAccount which would be equivalent to how we currently get the capability then call `borrow` on it. - -```cadence -let publicAccount = getAccount(issuerAddress) -let countCap = publicAccount.borrowCapability<&{HasCount}>(/public/hasCount)! -countRef.count -``` - ### Capability Controllers (CapCons) Each Capability would have an associated CapCon that would be created together with the Capability (Capabilities and Capability Controllers are in a 1 to 1 relation). The data associated with CapCons would be stored in arrays, so that each storage path on an account has an array of CapCons of Capabilities issued from that storage path. @@ -226,6 +214,26 @@ fun getLinkTarget(_ path: CapabilityPath): Path? This would remove all references to `Link`s. +### Borrow Capability + +. The `borrowCapability `method would be added to the PublicAccount which would be a shorthand to how we currently get the capability then call `borrow` on it. + +```cadence +// this: + +var publicAccount = getAccount(issuerAddress) +var cap = getCapability<&{CounterContract.HasCount}>(/public/counter) +var countCap = cap.borrow()! +countCap.count + + +// become this: + +let publicAccount = getAccount(issuerAddress) +let countCap = publicAccount.borrowCapability<&{HasCount}>(/public/hasCount)! +countRef.count +``` + ### Impact of the solution #### Changes for capability consumers @@ -362,6 +370,57 @@ public resource ScopedMain : AdminInterface { } ``` +## Deployment options + +Just making a breaking change by removing the linking API and adding the CapCons API might cause some headaches. In order to make the transition to CapCons smother, the CapCons API could temporarily work in parallel with the current linking API. + +The rollout process would have 2 steps: + +1. Add CapCons and still have linking. +2. After a transition period remove linking. + +To make this FLIP compatible with this rollout, the requirement to removing the private domain needs to be addressed While both APIs are still active, the private domain still needs to exist in order to do private linking for private capabilities. The private domain can be removed at step 2. + +During step 1. all existing links should also become CapCons (most likely with a storage migration). The linking API will still be available, but will actually operate with CapCons in the background. Below is a implementation of the linking API with the CapCons API. + +```cadence +// link +// issuer.link(publicOrPrivatePath, target: storagePath) + +let cap = issuer.issueCapability(storagePath) +issuer.save(cap, to: publicOrPrivatePath) +``` + +```cadence +// unlink +// issuer.unlink(publicOrPrivatePath) + +let cap = issuer.getCapability(publicOrPrivatePath) +let capabilityID = cap.capabilityID +let capCon = issuer.getController(capabilityID: capabilityID) + +capCon.revoke() +``` + +```cadence +// relink +// issuer.unlink(publicOrPrivatePath) +// issuer.link<&{CounterContract.HasCount}>(publicOrPrivatePath, target: storagePath2) + +// 1. unlink + +let cap = issuer.getCapability(publicOrPrivatePath) +let capabilityID = cap.capabilityID +let capCon = issuer.getController(capabilityID: capabilityID) + +capCon.revoke() + +// 2. link + +let cap = issuer.issueCapability(storagePath2) +issuer.save(cap, to: publicOrPrivatePath) +``` + ## Issues not addressed in this FLIP From 5254aad44ca4e02f0bc81c70e2b416563e7748b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Mon, 24 Oct 2022 11:52:26 -0700 Subject: [PATCH 10/44] fix b14135748e0cac3b80920a235b200d6da8785ee9 --- flips/20220203-capability-controllers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index be34b720b..b1509a049 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -7,7 +7,7 @@ ## Preface -Cadence encourages a [capability-based security](d be clearer to Charlie that he received a capability t) model as described on the Flow [doc site](https://docs.onflow.org/cadence/language/capability-based-access-control). Capabilities are themselves a new concept that most Cadence programmers need to understand, but the API for syntax around Capabilities (especially the notion of “links” and “linking”), and the associated concepts of the public and private storage domains, lead to Capabilities being even more confusing and awkward to use. This proposal suggests that we could get rid of links entirely, and replace them with Capability Controllers (henceforth referred to as CapCons) which could make Capabilities easier to understand, and easier to work with. +Cadence encourages a [capability-based security](https://en.wikipedia.org/wiki/Capability-based_security) model as described on the Flow [doc site](https://docs.onflow.org/cadence/language/capability-based-access-control). Capabilities are themselves a new concept that most Cadence programmers need to understand, but the API for syntax around Capabilities (especially the notion of “links” and “linking”), and the associated concepts of the public and private storage domains, lead to Capabilities being even more confusing and awkward to use. This proposal suggests that we could get rid of links entirely, and replace them with Capability Controllers (henceforth referred to as CapCons) which could make Capabilities easier to understand, and easier to work with. The following is a quick refresher of the current state of the Capabilities API (from the [flow doc site](https://docs.onflow.org/cadence/language/capability-based-access-control)). From 6a663396751a5509313acc1695fe94c956a41b26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Wed, 9 Nov 2022 14:54:54 -0800 Subject: [PATCH 11/44] Apply suggestions from code review Co-authored-by: Robert E. Davidson III <45945043+robert-e-davidson3@users.noreply.github.com> --- flips/20220203-capability-controllers.md | 42 ++++++++++++------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index b1509a049..56baf289f 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -145,7 +145,7 @@ The suggested change addresses these pain points by: - Removing (or abstracting away) the concept of links. - Making it easier to create new capabilities (with individual links). - Offering a way to iterate through capabilities on a storage path. -- Removing the need to have a `/private/`domain. +- Removing the need to have a `/private` domain. - Introducing Capability Controllers (CapCons) that handle management of capabilities. ### Capability Controllers (CapCons) @@ -166,13 +166,13 @@ struct CapabilityController { // The block height when the capability was created. let issueHeight: UInt64 // The Type of the capability. - let CapabilityType: Type + let capabilityType: Type // The id of the related capability. // This is the UUID of the created capability. let capabilityID: UInt64 // Is the capability revoked. - fun isRevoked(): Bool + let isRevoked: Bool // The target storage path. fun target(): StoragePath // Revoke the capability. @@ -184,7 +184,7 @@ struct CapabilityController { } ``` -The auth account would get new methods to get CapCons in order to manage capabilities: +The `AuthAccount` would get new methods to get CapCons in order to manage capabilities: ```cadence // Get all capability controllers for capabilities that target this storage path @@ -216,13 +216,13 @@ This would remove all references to `Link`s. ### Borrow Capability -. The `borrowCapability `method would be added to the PublicAccount which would be a shorthand to how we currently get the capability then call `borrow` on it. +The `borrowCapability` method would be added to the `PublicAccount` which would be a shorthand to how we currently get the capability then call `borrow` on it. ```cadence // this: var publicAccount = getAccount(issuerAddress) -var cap = getCapability<&{CounterContract.HasCount}>(/public/counter) +var cap = publicAccount.getCapability<&{CounterContract.HasCount}>(/public/counter) var countCap = cap.borrow()! countCap.count @@ -230,7 +230,7 @@ countCap.count // become this: let publicAccount = getAccount(issuerAddress) -let countCap = publicAccount.borrowCapability<&{HasCount}>(/public/hasCount)! +let countRef = publicAccount.borrowCapability<&{HasCount}>(/public/hasCount)! countRef.count ``` @@ -310,11 +310,11 @@ In certain situations it is required that an issuer delegates issuing and revoki Let's assume that the issuer defined an `AdminInterface` resource interface and a `Main` resource (besides the `Counter` and `HasCount` from previous examples). ```cadence -public resource interface AdminInterface { +pub resource interface AdminInterface { fun createCountCap(): Capability<&{HasCount}> fun revokeCountCap(capabilityID: UInt64): Bool } -public resource Main : AdminInterface { +pub resource Main : AdminInterface { fun createCountCap(): Capability<&{HasCount}> { return self.account.getCapability<&{HasCount}>(/storage/counter) } @@ -335,20 +335,20 @@ The issuer can then store a `Main` resource in their storage and give the capabi ```cadence issuer.save(<-create Main(), to: /storage/counterMinter) let countMinterCap <- issuer.getCapability(/storage/counterMinter) -countMinterCap //give this to a trusted party +countMinterCap // give this to a trusted party ``` -It is worth noting that every time the `AdminInterface` is used a CapCon is created in the issuers storage taking up some resources. Revoking capabilities does not free any storage. +It is worth noting that every time the `AdminInterface` is used, a CapCon is created in the issuer's storage, taking up some resources. Therefore revoking capabilities does not free any storage. In this example the delegatee with the `Capability<&{AdminInterface}>` can revoke any capability of type `&{HasCount}` even those that someone else created. This is sometimes desired (IT admins with the capability to issue purchase_hardware capabilities should have the ability to revoke what other IT admins issued), but sometimes we would like the delegatee to only revoke what it issued. If that is the case, another resource can be created `ScopedMain` that serves to limit which capabilities can be revoked. ```cadence -public resource ScopedMain : AdminInterface { - public delegatorTag: String - public inner: Capability<&{AdminInterface}> - public issued: {UInt64:Bool} +pub resource ScopedMain : AdminInterface { + pub delegatorTag: String + pub inner: Capability<&{AdminInterface}> + pub issued: {UInt64:Bool} - public init(inner: Capability<&{AdminInterface}>, delegatorTag: String) { + pub init(inner: Capability<&{AdminInterface}>, delegatorTag: String) { self.delegatorTag = delegatorTag self.inner = inner self.issued = {} @@ -372,16 +372,16 @@ public resource ScopedMain : AdminInterface { ## Deployment options -Just making a breaking change by removing the linking API and adding the CapCons API might cause some headaches. In order to make the transition to CapCons smother, the CapCons API could temporarily work in parallel with the current linking API. +Just making a breaking change by removing the linking API and adding the CapCons API might cause some headaches. In order to make the transition to CapCons smoother, the CapCons API could temporarily work in parallel with the current linking API. The rollout process would have 2 steps: 1. Add CapCons and still have linking. 2. After a transition period remove linking. -To make this FLIP compatible with this rollout, the requirement to removing the private domain needs to be addressed While both APIs are still active, the private domain still needs to exist in order to do private linking for private capabilities. The private domain can be removed at step 2. +To make this FLIP compatible with this rollout, the requirement of removing the private domain needs to be addressed. While both APIs are still active, the private domain still needs to exist in order to do private linking for private capabilities. The private domain can be removed at step 2. -During step 1. all existing links should also become CapCons (most likely with a storage migration). The linking API will still be available, but will actually operate with CapCons in the background. Below is a implementation of the linking API with the CapCons API. +During step 1, all existing links should also become CapCons (most likely with a storage migration). The linking API will still be available, but will actually operate with CapCons in the background. Below is an implementation of the linking API with the CapCons API. ```cadence // link @@ -426,7 +426,7 @@ issuer.save(cap, to: publicOrPrivatePath) ### Unexpected revocation -If the issuer creates a capability **A** and gives it to Alice and creates a capability **B** and gives it to Bob. Alice then copies her capability creating **A'** and gives it to Charlie, creating the following situation: +Let's say the issuer creates a capability **A** and gives it to Alice then creates a capability **B** and gives it to Bob. Alice then copies her capability creating **A'** and gives it to Charlie, creating the following situation: - Alice: has **A** - Bob: has **B** @@ -434,7 +434,7 @@ If the issuer creates a capability **A** and gives it to Alice and creates a cap If the issuer decides to revoke **B**, Bob will no longer be able to use his capability. If the issuer revokes **A** Alice will not be able to use her capability, but perhaps unexpected to Charlie, he will also not be able to use **A'**. -Addressing this issue so that it would be clearer to Charlie that he received a capability that is a copy of Alice's, and not his own instance is not in the scope of this FLIP. +Addressing this issue so that it would be clearer to Charlie that he received a capability that is a copy of Alice's, and not his own instance, is not in the scope of this FLIP. This could perhaps be addressed by: - adding extra descriptors to capabilities (names) From b358e189c1131e6b12a0681e09539c4fcd8345aa Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Tue, 15 Nov 2022 12:39:39 +0100 Subject: [PATCH 12/44] cap cons changes --- flips/20220203-capability-controllers.md | 159 ++++++++++++----------- 1 file changed, 84 insertions(+), 75 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index 56baf289f..414152336 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -13,7 +13,7 @@ The following is a quick refresher of the current state of the Capabilities API ### Example Resource definition -In the following examples let's assume that the following interface and resource type that implements the interface are defined. +In the following examples, let's assume that the following interface and resource type that implements the interface are defined. ```cadence pub resource interface HasCount { @@ -128,15 +128,15 @@ Capabilities are a value type. This means that any capability that an account ha ## Problem statement -There are two potential pain points in the current capability API. +The current capability API has a few pain points: -The first one is that capabilities/linking/revoking/redirecting are hard to understand for new developers that are coming to Flow, as that is a lot of new concepts to grasp before one can get started. +- The concepts capabilities/linking/revoking/redirecting are hard to understand for new developers that are coming to Flow, as that is a lot of new concepts to grasp before one can get started. -The second is that issuing and managing private capabilities that can be revoked at a granular level, by creating a custom private linked path for each capability, is difficult. +- Issuing and managing private capabilities that can be revoked at a granular level, by creating a custom private linked path for each capability, is difficult. - Unless the path name includes some hints, there is no way to know when a path was created, thus making it more difficult to remember why a certain capability was issued. + - If an old path (that has been unlinked) is accidentally reused and re-linked, this will revive a capability that is supposed to be revoked. -- Copying a capability is easy (since it is a value type) but doing so might give unintended access to third parties. ## Suggested change @@ -150,18 +150,13 @@ The suggested change addresses these pain points by: ### Capability Controllers (CapCons) -Each Capability would have an associated CapCon that would be created together with the Capability (Capabilities and Capability Controllers are in a 1 to 1 relation). The data associated with CapCons would be stored in arrays, so that each storage path on an account has an array of CapCons of Capabilities issued from that storage path. - -CapCons are a non-storable object. +Each Capability would have an associated CapCon that would be responsible for managing the Capability. The CapCon would be created when the Capability is issued (created). If the Capability is copied (since it is a value type) it shares the CapCon with the original (i.e.: calling revoke on the CapCon revokes all copies of the associated capability). -Capabilities also have an address field and a target field. The target field is the storage path of the targeted object. Creating a capability from a capability should be illegal. +The data associated with CapCons would be stored in arrays, so that each storage path on an account has an array of CapCons of Capabilities issued from that storage path. CapCons are a non-storable object (similar to AuthAccounts). -The definition of the `CapabilityController` . +The definition of the `CapabilityController`. ```cadence -// CapabilityController can be retrieved via: -// - AuthAccount::getControllers(path: StoragePath): [CapabilityController] -// - AuthAccount::getController(capabilityID: UInt64): CapabilityController? struct CapabilityController { // The block height when the capability was created. let issueHeight: UInt64 @@ -169,69 +164,84 @@ struct CapabilityController { let capabilityType: Type // The id of the related capability. // This is the UUID of the created capability. + // All copies of the same capability will have the same UUID let capabilityID: UInt64 - // Is the capability revoked. let isRevoked: Bool - // The target storage path. + + // Returns the targeted storage path of the capability. fun target(): StoragePath - // Revoke the capability. + // Revoke the capability making it no longer usable. + // When borrowing from a revoked capability the borrow returns nil. fun revoke() // Retarget the capability. - // This would move the CapCon from one CapCon array to another. + // This moves the CapCon from one CapCon array to another. // If the new target is not a valid target this will panic. fun retarget(target: StoragePath) } ``` -The `AuthAccount` would get new methods to get CapCons in order to manage capabilities: - -```cadence -// Get all capability controllers for capabilities that target this storage path -fun getControllers(path: StoragePath): [CapabilityController] -// Get capability controller for capability with the specified id -// If the id does not reference an existing capability -// or the capability does not target a storage path on this address, return nil -fun getController(capabilityID: UInt64): CapabilityController? -``` - -Some methods would be removed from the `AuthAccount` object as they are no longer needed: - -```cadence -fun link(_ newCapabilityPath: CapabilityPath, target: Path): Capability? -fun getLinkTarget(_ path: CapabilityPath): Path? -fun unlink(_ path: CapabilityPath) -``` - -The method `getCapability` would be renamed to `issueCapability `to reflect the fact that a new capability is created every time. +The capability related methods in the `AuthAccount` and in the `PublicAccount` would be moved to a `capabilities` namespace, similar to how the contract methods are in the [contracts namespace](https://developers.flow.com/cadence/language/accounts). -And also from the PublicAccount: +The `AuthAccount` would get new methods to create or get or iterate through capabilities and to get or iterate through CapCons in order to manage capabilities. Methods used for (un)linking would be removed as they are no longer needed. ```cadence -fun getCapability(_ path: PublicPath): Capability -fun getLinkTarget(_ path: CapabilityPath): Path? +struct AuthAccount { + // ... + // removed: fun link(_ newCapabilityPath: CapabilityPath, target: Path): Capability? + // removed: fun getLinkTarget(_ path: CapabilityPath): Path? + // removed: fun unlink(_ path: CapabilityPath) + // moved & renamed: fun getCapability(_ path: CapabilityPath): Capability + // moved & renamed: fun forEachPublic(_ function: ((PublicPath, Type): Bool)) + let capabilities: AuthAccount.Capabilities + struct Capabilities { + // get returns the capability at the public path. + fun get(_ path: PublicPath): Capability + // borrow is equivalent to `get(path).borrow()` + fun borrow(_ path: PublicPath): T? + // For each iterates through all the public capabilities of the public account. + // If function returns false, the iteration ends. + fun forEach(_ function: ((PublicPath, Type): Bool)) + + // Get capability controller for capability with the specified id + // If the id does not reference an existing capability + // or the capability does not target a storage path on this address, return nil + fun getController(byCapabilityID: UInt64): CapabilityController? + + // Get all capability controllers for capabilities that target this storage path + fun getControllers(forPath: StoragePath): [CapabilityController] + + // Iterate through all capability controllers for capabilities that target this storage path. + // Returning false from the function stops the iteration. + fun forEachController(forPath: StoragePath, function: ((CapabilityController): Bool)) + + // Issue/create a new capability. + fun issue(_ path: StoragePath): Capability + } +} ``` -This would remove all references to `Link`s. - -### Borrow Capability +The `PublicAccount` would get similar changes, but only for capabilities, as CapCons are not meant to be accessible outside of `AuthAccount`. -The `borrowCapability` method would be added to the `PublicAccount` which would be a shorthand to how we currently get the capability then call `borrow` on it. ```cadence -// this: - -var publicAccount = getAccount(issuerAddress) -var cap = publicAccount.getCapability<&{CounterContract.HasCount}>(/public/counter) -var countCap = cap.borrow()! -countCap.count - - -// become this: - -let publicAccount = getAccount(issuerAddress) -let countRef = publicAccount.borrowCapability<&{HasCount}>(/public/hasCount)! -countRef.count +struct PublicAccount { + // ... + // removed: fun getCapability(_ path: PublicPath): Capability + // removed: fun getLinkTarget(_ path: CapabilityPath): Path? + // removed: fun forEachPublic(_ function: ((PublicPath, Type): Bool)) + + let capabilities: PublicAccount.Capabilities + struct Capabilities { + // get returns the capability at the public path. + fun get(_ path: PublicPath): Capability + // borrow is equivalent to `get(path).borrow()` + fun borrow(_ path: PublicPath): T? + // For each iterates through all the public capabilities of the public account. + // If function returns false, the iteration ends. + fun forEach(_ function: ((PublicPath, Type): Bool)) + } +} ``` ### Impact of the solution @@ -251,16 +261,16 @@ Would change to: ```cadence let publicAccount = getAccount(issuerAddress) -let countCap = publicAccount.borrow>(/public/hasCount)! +let countCap = publicAccount.capabilities.get<&{HasCount}>(/public/hasCount) let countRef = countCap.borrow()! countRef.count ``` -Or using the `borrowCapability `shorthand: +Or using the `borrow` shorthand: ```cadence let publicAccount = getAccount(issuerAddress) -let countRef = publicAccount.borrowCapability<&{HasCount}>(/public/hasCount)! +let countRef = publicAccount.capabilities.borrow<&{HasCount}>(/public/hasCount)! countRef.count ``` @@ -269,25 +279,25 @@ countRef.count There would be more change on the issuer's side. Most notably creating a public capability would look like this. ```cadence -let countCap = issuer.issueCapability<&{HasCount}>(/storage/counter) +let countCap = issuer.capabilities.issue<&{HasCount}>(/storage/counter) issuer.save(countCap, to: /public/hasCount) ``` Unlinking and retargeting issued capabilities would change to getting a CapCon and calling the appropriate methods. ```cadence -let capCon = issuer.getController(capabilityID: capabilityID) +let capCon = issuer.capabilities.getController(byCapabilityID: capabilityID) capCon.revoke() // or capCon.retarget(target: /storage/counter2) ``` -This example assumes that the capability id is known. This is always the case for capabilities in the accounts public domain, since the account has access to those directly. For private capabilities that were given to someone else this can be achieved by keeping an on-chain or an off-chain list of capability ids and some extra identifying information (for example the address of the receiver of the capability). If no such list was kept, the issuer can use the information on the CapCons retrieved through `issuer.getControllers(path: StoragePath)`to find the right id. +This example assumes that the capability id is known. This is always the case for capabilities in the accounts public domain, since the account has access to those directly. For private capabilities that were given to someone else this can be achieved by keeping an on-chain or an off-chain list of capability ids and some extra identifying information (for example the address of the receiver of the capability). If no such list was kept, the issuer can use the information on the CapCons retrieved through `issuer.capabilities.getControllers(path: StoragePath)`to find the right id. #### Pet names for issued capabilities -If needed the `account.getCapability(path)` can be wrapped into a smart contract function, so that the issuer can more easily keep track of what was issued. +If needed the `account.capabilities.issue(path)` can be wrapped into a smart contract function, so that the issuer can more easily keep track of what was issued. ```cadence // inside the Counter contract @@ -295,7 +305,7 @@ access(account) petNames: {String; UInt64} access(account) issueHasCount(petName: String): Capability<&{HasCount}> { // for brevity this function is not handling pet name collision - let cap = self.account.issueCapability<&{HasCount}>(/storage/counter) + let cap = self.account.capabilities.issue<&{HasCount}>(/storage/counter) self.petNames[petName] = cap.capabilityID return cap } @@ -316,11 +326,11 @@ pub resource interface AdminInterface { } pub resource Main : AdminInterface { fun createCountCap(): Capability<&{HasCount}> { - return self.account.getCapability<&{HasCount}>(/storage/counter) + return self.account.capabilities.issue<&{HasCount}>(/storage/counter) } fun revokeCountCap(capabilityID: UInt64) { - if let capCon = self.account.getController(capabilityID: capabilityID) { + if let capCon = self.account.capabilities.getController(byCapabilityID: capabilityID) { if capCon.Type != Type<&{HasCount}>() { return false // we have only delegated the issuance/revocation of &{HasCount} capabilities } @@ -334,7 +344,7 @@ The issuer can then store a `Main` resource in their storage and give the capabi ```cadence issuer.save(<-create Main(), to: /storage/counterMinter) -let countMinterCap <- issuer.getCapability(/storage/counterMinter) +let countMinterCap <- issuer.capabilities.issue<&{AdminInterface}>(/storage/counterMinter) countMinterCap // give this to a trusted party ``` @@ -387,7 +397,7 @@ During step 1, all existing links should also become CapCons (most likely with a // link // issuer.link(publicOrPrivatePath, target: storagePath) -let cap = issuer.issueCapability(storagePath) +let cap = issuer.capabilities.issue(storagePath) issuer.save(cap, to: publicOrPrivatePath) ``` @@ -395,9 +405,9 @@ issuer.save(cap, to: publicOrPrivatePath) // unlink // issuer.unlink(publicOrPrivatePath) -let cap = issuer.getCapability(publicOrPrivatePath) +let cap = issuer.capabilities.get(publicOrPrivatePath) let capabilityID = cap.capabilityID -let capCon = issuer.getController(capabilityID: capabilityID) +let capCon = issuer.capabilities.getController(capabilityID: capabilityID) capCon.revoke() ``` @@ -409,19 +419,18 @@ capCon.revoke() // 1. unlink -let cap = issuer.getCapability(publicOrPrivatePath) +let cap = issuer.capabilities.get(publicOrPrivatePath) let capabilityID = cap.capabilityID -let capCon = issuer.getController(capabilityID: capabilityID) +let capCon = issuer.capabilities.getController(capabilityID: capabilityID) capCon.revoke() // 2. link -let cap = issuer.issueCapability(storagePath2) +let cap = issuer.capabilities.issue(storagePath2) issuer.save(cap, to: publicOrPrivatePath) ``` - ## Issues not addressed in this FLIP ### Unexpected revocation From c336d65fae77a1ce11513d3e199f8f19bcf390c6 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Thu, 17 Nov 2022 16:54:42 +0100 Subject: [PATCH 13/44] get returns optional --- flips/20220203-capability-controllers.md | 30 ++++++++++++------------ 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index 414152336..2e7a9eb88 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -160,8 +160,8 @@ The definition of the `CapabilityController`. struct CapabilityController { // The block height when the capability was created. let issueHeight: UInt64 - // The Type of the capability. - let capabilityType: Type + // The Type of the capability, i.e.: the T in Capability. + let borrowType: Type // The id of the related capability. // This is the UUID of the created capability. // All copies of the same capability will have the same UUID @@ -195,9 +195,9 @@ struct AuthAccount { // moved & renamed: fun forEachPublic(_ function: ((PublicPath, Type): Bool)) let capabilities: AuthAccount.Capabilities struct Capabilities { - // get returns the capability at the public path. - fun get(_ path: PublicPath): Capability - // borrow is equivalent to `get(path).borrow()` + // get returns the capability at the public path, if one was stored there. + fun get(_ path: PublicPath): Capability? + // borrow is equivalent to `get(path)!.borrow()` if `get(path)` exists `nil` otherwise fun borrow(_ path: PublicPath): T? // For each iterates through all the public capabilities of the public account. // If function returns false, the iteration ends. @@ -233,9 +233,9 @@ struct PublicAccount { let capabilities: PublicAccount.Capabilities struct Capabilities { - // get returns the capability at the public path. - fun get(_ path: PublicPath): Capability - // borrow is equivalent to `get(path).borrow()` + // get returns the capability at the public path, if one was stored there. + fun get(_ path: PublicPath): Capability? + // borrow is equivalent to `get(path)!.borrow()` if `get(path)` exists `nil` otherwise fun borrow(_ path: PublicPath): T? // For each iterates through all the public capabilities of the public account. // If function returns false, the iteration ends. @@ -261,7 +261,7 @@ Would change to: ```cadence let publicAccount = getAccount(issuerAddress) -let countCap = publicAccount.capabilities.get<&{HasCount}>(/public/hasCount) +let countCap = publicAccount.capabilities.get<&{HasCount}>(/public/hasCount)! let countRef = countCap.borrow()! countRef.count ``` @@ -401,13 +401,13 @@ let cap = issuer.capabilities.issue(storagePath) issuer.save(cap, to: publicOrPrivatePath) ``` -```cadence +```cadence // unlink // issuer.unlink(publicOrPrivatePath) -let cap = issuer.capabilities.get(publicOrPrivatePath) +let cap = issuer.capabilities.get(publicOrPrivatePath)! let capabilityID = cap.capabilityID -let capCon = issuer.capabilities.getController(capabilityID: capabilityID) +let capCon = issuer.capabilities.getController(byCapabilityID: capabilityID) capCon.revoke() ``` @@ -419,9 +419,9 @@ capCon.revoke() // 1. unlink -let cap = issuer.capabilities.get(publicOrPrivatePath) +let cap = issuer.capabilities.get(publicOrPrivatePath)! let capabilityID = cap.capabilityID -let capCon = issuer.capabilities.getController(capabilityID: capabilityID) +let capCon = issuer.capabilities.getController(byCapabilityID: capabilityID) capCon.revoke() @@ -443,7 +443,7 @@ Let's say the issuer creates a capability **A** and gives it to Alice then creat If the issuer decides to revoke **B**, Bob will no longer be able to use his capability. If the issuer revokes **A** Alice will not be able to use her capability, but perhaps unexpected to Charlie, he will also not be able to use **A'**. -Addressing this issue so that it would be clearer to Charlie that he received a capability that is a copy of Alice's, and not his own instance, is not in the scope of this FLIP. +Addressing this issue so that it would be clearer to Charlie that he received a capability that is a copy of Alice's, and not his own instance, is not in the scope of this FLIP. This could perhaps be addressed by: - adding extra descriptors to capabilities (names) From 08c5696cc38b667020898a93f21f08c6435e8c3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Wed, 23 Nov 2022 15:25:49 -0800 Subject: [PATCH 14/44] fix typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Álvaro Lillo Igualada --- flips/20220203-capability-controllers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index 2e7a9eb88..4c57abfb1 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -301,7 +301,7 @@ If needed the `account.capabilities.issue(path)` can be wrapped into a smart ```cadence // inside the Counter contract -access(account) petNames: {String; UInt64} +access(account) petNames: {String: UInt64} access(account) issueHasCount(petName: String): Capability<&{HasCount}> { // for brevity this function is not handling pet name collision From f11550e5b029eae1307ccaa050f7570197be2de7 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Fri, 25 Nov 2022 19:39:39 +0100 Subject: [PATCH 15/44] capcons capability ids --- flips/20220203-capability-controllers.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index 4c57abfb1..2583faecc 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -152,6 +152,8 @@ The suggested change addresses these pain points by: Each Capability would have an associated CapCon that would be responsible for managing the Capability. The CapCon would be created when the Capability is issued (created). If the Capability is copied (since it is a value type) it shares the CapCon with the original (i.e.: calling revoke on the CapCon revokes all copies of the associated capability). +A CapCon and all the copies of the Capability that CapCon controls would have the same ID. Capability/CapCon ids are unique per account. + The data associated with CapCons would be stored in arrays, so that each storage path on an account has an array of CapCons of Capabilities issued from that storage path. CapCons are a non-storable object (similar to AuthAccounts). The definition of the `CapabilityController`. @@ -181,6 +183,8 @@ struct CapabilityController { } ``` +Capabilities would expose the new id field `let id: UInt64` in addition to the account field they already expose. + The capability related methods in the `AuthAccount` and in the `PublicAccount` would be moved to a `capabilities` namespace, similar to how the contract methods are in the [contracts namespace](https://developers.flow.com/cadence/language/accounts). The `AuthAccount` would get new methods to create or get or iterate through capabilities and to get or iterate through CapCons in order to manage capabilities. Methods used for (un)linking would be removed as they are no longer needed. From 4530d4794c39e423b0a7845e28e0c8da287b39de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Tue, 24 Jan 2023 10:08:58 -0800 Subject: [PATCH 16/44] Apply suggestions from code review --- flips/20220203-capability-controllers.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index 2583faecc..2125c3744 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -180,6 +180,8 @@ struct CapabilityController { // This moves the CapCon from one CapCon array to another. // If the new target is not a valid target this will panic. fun retarget(target: StoragePath) + + fun getCapability(): Capability? } ``` @@ -201,7 +203,9 @@ struct AuthAccount { struct Capabilities { // get returns the capability at the public path, if one was stored there. fun get(_ path: PublicPath): Capability? - // borrow is equivalent to `get(path)!.borrow()` if `get(path)` exists `nil` otherwise + // borrow gets the capability at the given path, and borrows the capability if it exists. + // Returns `nil` if the capability does not exist or cannot be borrowed using the given type. + // The function is equivalent to `get(path)?.borrow()`. fun borrow(_ path: PublicPath): T? // For each iterates through all the public capabilities of the public account. // If function returns false, the iteration ends. @@ -239,7 +243,9 @@ struct PublicAccount { struct Capabilities { // get returns the capability at the public path, if one was stored there. fun get(_ path: PublicPath): Capability? - // borrow is equivalent to `get(path)!.borrow()` if `get(path)` exists `nil` otherwise + // borrow gets the capability at the given path, and borrows the capability if it exists. + // Returns `nil` if the capability does not exist or cannot be borrowed using the given type. + // The function is equivalent to `get(path)?.borrow()`. fun borrow(_ path: PublicPath): T? // For each iterates through all the public capabilities of the public account. // If function returns false, the iteration ends. @@ -335,7 +341,7 @@ pub resource Main : AdminInterface { fun revokeCountCap(capabilityID: UInt64) { if let capCon = self.account.capabilities.getController(byCapabilityID: capabilityID) { - if capCon.Type != Type<&{HasCount}>() { + if capCon.borrowType != Type<&{HasCount}>() { return false // we have only delegated the issuance/revocation of &{HasCount} capabilities } capCon.revoke() From 1b1abe13f5dd6a2cdf32f39eadffbc286d6588e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Tue, 24 Jan 2023 15:31:40 -0800 Subject: [PATCH 17/44] improve formatting --- flips/20220203-capability-controllers.md | 311 ++++++++++++++--------- 1 file changed, 193 insertions(+), 118 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index 2125c3744..4b6559a12 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -7,9 +7,19 @@ ## Preface -Cadence encourages a [capability-based security](https://en.wikipedia.org/wiki/Capability-based_security) model as described on the Flow [doc site](https://docs.onflow.org/cadence/language/capability-based-access-control). Capabilities are themselves a new concept that most Cadence programmers need to understand, but the API for syntax around Capabilities (especially the notion of “links” and “linking”), and the associated concepts of the public and private storage domains, lead to Capabilities being even more confusing and awkward to use. This proposal suggests that we could get rid of links entirely, and replace them with Capability Controllers (henceforth referred to as CapCons) which could make Capabilities easier to understand, and easier to work with. +Cadence encourages a [capability-based security](https://en.wikipedia.org/wiki/Capability-based_security) model +as described on the Flow [doc site](https://docs.onflow.org/cadence/language/capability-based-access-control). +Capabilities are themselves a new concept that most Cadence programmers need to understand, +but the API for syntax around Capabilities (especially the notion of “links” and “linking”), +and the associated concepts of the public and private storage domains, +lead to Capabilities being even more confusing and awkward to use. -The following is a quick refresher of the current state of the Capabilities API (from the [flow doc site](https://docs.onflow.org/cadence/language/capability-based-access-control)). +This proposal suggests that we could get rid of links entirely, +and replace them with Capability Controllers (henceforth referred to as CapCons), +which could make Capabilities easier to understand, and easier to work with. + +The following is a quick refresher of the current state of the Capabilities API +(from the [Cadence documentation](https://docs.onflow.org/cadence/language/capability-based-access-control)). ### Example Resource definition @@ -17,15 +27,15 @@ In the following examples, let's assume that the following interface and resourc ```cadence pub resource interface HasCount { - pub count: Int + pub count: Int } pub resource Counter: HasCount { - pub var count: Int + pub var count: Int - pub init(count: Int) { - self.count = count - } + pub init(count: Int) { + self.count = count + } } ``` @@ -37,14 +47,16 @@ issuer.save(<-create Counter(count: 42), to: /storage/counter) ### Public Capabilities -To allow anyone (read) access to the `count` field on the counter (during a transaction), the _issuer_ needs to create a public typed link at a chosen path that points to their stored counter resource. +To allow anyone (read) access to the `count` field on the counter (during a transaction), +the _issuer_ needs to create a public typed link at a chosen path that points to their stored counter resource. ```cadence issuer.link<&{HasCount}>(/public/hasCount, target: /storage/counter) ``` -Anyone can now read the `count` of the counter resource via the `HasCount` interface. This can be done by +Anyone can now read the `count` of the counter resource via the `HasCount` interface. +This can be done by - getting the `PublicAccount` object of the issuer (using the issuers address), - then getting a typed capability from the chosen path, - then finally calling borrow on that capability to get a reference to the instance of the counter (constrained by the `HasCount` interface) @@ -58,7 +70,8 @@ countRef.count ### Private Capabilities -To allow only certain accounts/resources (read) access to the `count` field on the counter, the _issuer_ (of type `AuthAccount`) needs to create a private typed link at a chosen path that points to their stored counter resource. +To allow only certain accounts/resources (read) access to the `count` field on the counter, +the _issuer_ (of type `AuthAccount`) needs to create a private typed link at a chosen path that points to their stored counter resource. ```cadence issuer.link<&{HasCount}>(/private/hasCount, target: /storage/counter) @@ -69,20 +82,20 @@ The _receivingParty_ (`AuthAccount`) needs to offer a public way to receive `&{H ```cadence // this would probably be defined on the same contract as `HasCount` pub resource interface HasCountReceiverPublic { - pub fun addCapability(cap: Capability<&{HasCount}>) + pub fun addCapability(cap: Capability<&{HasCount}>) } pub resource HasCountReceiver: HasCountReceiverPublic { - pub var hasCountCapability: Capability<&{HasCount}>? + pub var hasCountCapability: Capability<&{HasCount}>? - init() { - self.hasCountCapability = nil - } + init() { + self.hasCountCapability = nil + } - pub fun addCapability(cap: Capability<&{HasCount}>) { - self.hasCountCapability = cap - } + pub fun addCapability(cap: Capability<&{HasCount}>) { + self.hasCountCapability = cap + } } //... @@ -90,8 +103,10 @@ pub resource HasCountReceiver: HasCountReceiverPublic { let hasCountReceiver <- HasCountReceiver() receivingParty.save(<-hasCountReceiver, to: /storage/hasCountReceiver) -receivingParty.link<&{HasCountReceiverPublic}>(/public/hasCountReceiver, - target: /storage/hasCountReceive) +receivingParty.link<&{HasCountReceiverPublic}>( + /public/hasCountReceiver, + target: /storage/hasCountReceive +) ``` With this in place the _issuer_ can create a capability from its private link and send it to this receiver. @@ -116,27 +131,41 @@ There are two requirements of the capability system that must be satisfied. Revocation can be currently done by using the `unlink` function. -In the public example this would mean calling `issuer.unlink<&{HasCount}>(/public/hasCount)` which would invalidate (break) all capabilities created from this public path (both those created before unlink was called and those created after unlink was called). +In the public example this would mean calling `issuer.unlink<&{HasCount}>(/public/hasCount)`, +which would invalidate (break) all capabilities created from this public path +(both those created before unlink was called and those created after unlink was called). -In the private example the call would change to `issuer.unlink<&{HasCount}>(/private/hasCount)`. It is important to note that if the issuer wants to have the ability to revoke/redirect capabilities in a more granular way (instead of doing them all at once), the solution is to create multiple private linked paths (e.g. `/private/hasCountAlice`, `/private/hasCountBob`). +In the private example the call would change to `issuer.unlink<&{HasCount}>(/private/hasCount)`. +It is important to note that if the issuer wants to have the ability to revoke/redirect capabilities in a more granular way +(instead of doing them all at once), +the solution is to create multiple private linked paths (e.g. `/private/hasCountAlice`, `/private/hasCountBob`). -If a path that was unlinked is linked again all the capabilities created from that path resume working. This can be used to redirect a capability to a different object, but can also be dangerous if done unintentionally, reviving a previously revoked capability. +If a path that was unlinked is linked again all the capabilities created from that path resume working. +This can be used to redirect a capability to a different object, +but can also be dangerous if done unintentionally, +reviving a previously revoked capability. ### Capabilities are values -Capabilities are a value type. This means that any capability that an account has access to can be copied, and potentially given to someone else. The copied capability will use the link on the same path as the original capability +Capabilities are a value type. This means that any capability that an account has access to can be copied, +and potentially given to someone else. +The copied capability will use the link on the same path as the original capability. ## Problem statement The current capability API has a few pain points: -- The concepts capabilities/linking/revoking/redirecting are hard to understand for new developers that are coming to Flow, as that is a lot of new concepts to grasp before one can get started. +- The concepts capabilities/linking/revoking/redirecting are hard to understand for new developers that are coming to Flow, +as that is a lot of new concepts to grasp before one can get started. -- Issuing and managing private capabilities that can be revoked at a granular level, by creating a custom private linked path for each capability, is difficult. +- Issuing and managing private capabilities that can be revoked at a granular level, + by creating a custom private linked path for each capability, is difficult. -- Unless the path name includes some hints, there is no way to know when a path was created, thus making it more difficult to remember why a certain capability was issued. +- Unless the path name includes some hints, there is no way to know when a path was created, + thus making it more difficult to remember why a certain capability was issued. -- If an old path (that has been unlinked) is accidentally reused and re-linked, this will revive a capability that is supposed to be revoked. +- If an old path (that has been unlinked) is accidentally reused and re-linked, + this will revive a capability that is supposed to be revoked. ## Suggested change @@ -150,107 +179,129 @@ The suggested change addresses these pain points by: ### Capability Controllers (CapCons) -Each Capability would have an associated CapCon that would be responsible for managing the Capability. The CapCon would be created when the Capability is issued (created). If the Capability is copied (since it is a value type) it shares the CapCon with the original (i.e.: calling revoke on the CapCon revokes all copies of the associated capability). +Each Capability would have an associated CapCon that would be responsible for managing the Capability. +The CapCon would be created when the Capability is issued (created). +If the Capability is copied (since it is a value type) it shares the CapCon with the original +(i.e., calling revoke on the CapCon revokes all copies of the associated capability). -A CapCon and all the copies of the Capability that CapCon controls would have the same ID. Capability/CapCon ids are unique per account. +A CapCon and all the copies of the Capability that CapCon controls would have the same ID. +Capability/CapCon ids are unique per account. -The data associated with CapCons would be stored in arrays, so that each storage path on an account has an array of CapCons of Capabilities issued from that storage path. CapCons are a non-storable object (similar to AuthAccounts). +The data associated with CapCons would be stored in arrays, +so that each storage path on an account has an array of CapCons of Capabilities issued from that storage path. +CapCons are a non-storable object (similar to AuthAccounts). The definition of the `CapabilityController`. ```cadence struct CapabilityController { - // The block height when the capability was created. - let issueHeight: UInt64 - // The Type of the capability, i.e.: the T in Capability. - let borrowType: Type - // The id of the related capability. - // This is the UUID of the created capability. - // All copies of the same capability will have the same UUID - let capabilityID: UInt64 - // Is the capability revoked. - let isRevoked: Bool - - // Returns the targeted storage path of the capability. - fun target(): StoragePath - // Revoke the capability making it no longer usable. - // When borrowing from a revoked capability the borrow returns nil. - fun revoke() - // Retarget the capability. - // This moves the CapCon from one CapCon array to another. - // If the new target is not a valid target this will panic. - fun retarget(target: StoragePath) + // The block height when the capability was created. + let issueHeight: UInt64 + + // The Type of the capability, i.e.: the T in Capability. + let borrowType: Type + + // The id of the related capability. + // This is the UUID of the created capability. + // All copies of the same capability will have the same UUID + let capabilityID: UInt64 + + // Is the capability revoked. + let isRevoked: Bool + + // Returns the targeted storage path of the capability. + fun target(): StoragePath + + // Revoke the capability making it no longer usable. + // When borrowing from a revoked capability the borrow returns nil. + fun revoke() + + // Retarget the capability. + // This moves the CapCon from one CapCon array to another. + // If the new target is not a valid target this will panic. + fun retarget(target: StoragePath) fun getCapability(): Capability? } ``` -Capabilities would expose the new id field `let id: UInt64` in addition to the account field they already expose. +Capabilities would expose the new field `let id: UInt64`, in addition to the account field they already expose. -The capability related methods in the `AuthAccount` and in the `PublicAccount` would be moved to a `capabilities` namespace, similar to how the contract methods are in the [contracts namespace](https://developers.flow.com/cadence/language/accounts). +The capability related methods in the `AuthAccount` and in the `PublicAccount` would be moved to a `capabilities` namespace, +similar to how the contract methods are in the [contracts namespace](https://developers.flow.com/cadence/language/accounts). -The `AuthAccount` would get new methods to create or get or iterate through capabilities and to get or iterate through CapCons in order to manage capabilities. Methods used for (un)linking would be removed as they are no longer needed. +The `AuthAccount` would get new methods to create or get or iterate through capabilities, +and to get or iterate through CapCons in order to manage capabilities. +Methods used for (un)linking would be removed as they are no longer needed. ```cadence struct AuthAccount { - // ... - // removed: fun link(_ newCapabilityPath: CapabilityPath, target: Path): Capability? - // removed: fun getLinkTarget(_ path: CapabilityPath): Path? - // removed: fun unlink(_ path: CapabilityPath) - // moved & renamed: fun getCapability(_ path: CapabilityPath): Capability - // moved & renamed: fun forEachPublic(_ function: ((PublicPath, Type): Bool)) - let capabilities: AuthAccount.Capabilities - struct Capabilities { - // get returns the capability at the public path, if one was stored there. - fun get(_ path: PublicPath): Capability? - // borrow gets the capability at the given path, and borrows the capability if it exists. - // Returns `nil` if the capability does not exist or cannot be borrowed using the given type. - // The function is equivalent to `get(path)?.borrow()`. - fun borrow(_ path: PublicPath): T? - // For each iterates through all the public capabilities of the public account. - // If function returns false, the iteration ends. - fun forEach(_ function: ((PublicPath, Type): Bool)) - - // Get capability controller for capability with the specified id - // If the id does not reference an existing capability - // or the capability does not target a storage path on this address, return nil - fun getController(byCapabilityID: UInt64): CapabilityController? - - // Get all capability controllers for capabilities that target this storage path - fun getControllers(forPath: StoragePath): [CapabilityController] - - // Iterate through all capability controllers for capabilities that target this storage path. - // Returning false from the function stops the iteration. - fun forEachController(forPath: StoragePath, function: ((CapabilityController): Bool)) - - // Issue/create a new capability. - fun issue(_ path: StoragePath): Capability - } + // ... + // removed: fun link(_ newCapabilityPath: CapabilityPath, target: Path): Capability? + // removed: fun getLinkTarget(_ path: CapabilityPath): Path? + // removed: fun unlink(_ path: CapabilityPath) + // moved & renamed: fun getCapability(_ path: CapabilityPath): Capability + // moved & renamed: fun forEachPublic(_ function: ((PublicPath, Type): Bool)) + + let capabilities: AuthAccount.Capabilities + + struct Capabilities { + // get returns the capability at the public path, if one was stored there. + fun get(_ path: PublicPath): Capability? + + // borrow gets the capability at the given path, and borrows the capability if it exists. + // Returns `nil` if the capability does not exist or cannot be borrowed using the given type. + // The function is equivalent to `get(path)?.borrow()`. + fun borrow(_ path: PublicPath): T? + + // For each iterates through all the public capabilities of the public account. + // If function returns false, the iteration ends. + fun forEach(_ function: ((PublicPath, Type): Bool)) + + // Get capability controller for capability with the specified id + // If the id does not reference an existing capability + // or the capability does not target a storage path on this address, return nil + fun getController(byCapabilityID: UInt64): CapabilityController? + + // Get all capability controllers for capabilities that target this storage path + fun getControllers(forPath: StoragePath): [CapabilityController] + + // Iterate through all capability controllers for capabilities that target this storage path. + // Returning false from the function stops the iteration. + fun forEachController(forPath: StoragePath, function: ((CapabilityController): Bool)) + + // Issue/create a new capability. + fun issue(_ path: StoragePath): Capability + } } ``` -The `PublicAccount` would get similar changes, but only for capabilities, as CapCons are not meant to be accessible outside of `AuthAccount`. +The `PublicAccount` would get similar changes, but only for capabilities, +as CapCons are not meant to be accessible outside of `AuthAccount`. ```cadence struct PublicAccount { - // ... - // removed: fun getCapability(_ path: PublicPath): Capability - // removed: fun getLinkTarget(_ path: CapabilityPath): Path? - // removed: fun forEachPublic(_ function: ((PublicPath, Type): Bool)) - - let capabilities: PublicAccount.Capabilities - struct Capabilities { - // get returns the capability at the public path, if one was stored there. - fun get(_ path: PublicPath): Capability? - // borrow gets the capability at the given path, and borrows the capability if it exists. - // Returns `nil` if the capability does not exist or cannot be borrowed using the given type. - // The function is equivalent to `get(path)?.borrow()`. - fun borrow(_ path: PublicPath): T? - // For each iterates through all the public capabilities of the public account. - // If function returns false, the iteration ends. - fun forEach(_ function: ((PublicPath, Type): Bool)) - } + // ... + // removed: fun getCapability(_ path: PublicPath): Capability + // removed: fun getLinkTarget(_ path: CapabilityPath): Path? + // removed: fun forEachPublic(_ function: ((PublicPath, Type): Bool)) + + let capabilities: PublicAccount.Capabilities + + struct Capabilities { + // get returns the capability at the public path, if one was stored there. + fun get(_ path: PublicPath): Capability? + + // borrow gets the capability at the given path, and borrows the capability if it exists. + // Returns `nil` if the capability does not exist or cannot be borrowed using the given type. + // The function is equivalent to `get(path)?.borrow()`. + fun borrow(_ path: PublicPath): T? + + // For each iterates through all the public capabilities of the public account. + // If function returns false, the iteration ends. + fun forEach(_ function: ((PublicPath, Type): Bool)) + } } ``` @@ -303,11 +354,18 @@ capCon.revoke() capCon.retarget(target: /storage/counter2) ``` -This example assumes that the capability id is known. This is always the case for capabilities in the accounts public domain, since the account has access to those directly. For private capabilities that were given to someone else this can be achieved by keeping an on-chain or an off-chain list of capability ids and some extra identifying information (for example the address of the receiver of the capability). If no such list was kept, the issuer can use the information on the CapCons retrieved through `issuer.capabilities.getControllers(path: StoragePath)`to find the right id. +This example assumes that the capability id is known. +This is always the case for capabilities in the accounts public domain, since the account has access to those directly. +For private capabilities that were given to someone else this can be achieved +by keeping an on-chain or an off-chain list of capability ids and some extra identifying information +(for example the address of the receiver of the capability). +If no such list was kept, the issuer can use the information on the CapCons, +retrieved through `issuer.capabilities.getControllers(path: StoragePath)`, to find the right id. #### Pet names for issued capabilities -If needed the `account.capabilities.issue(path)` can be wrapped into a smart contract function, so that the issuer can more easily keep track of what was issued. +If needed the `account.capabilities.issue(path)` can be wrapped into a smart contract function, +so that the issuer can more easily keep track of what was issued. ```cadence // inside the Counter contract @@ -321,11 +379,13 @@ access(account) issueHasCount(petName: String): Capability<&{HasCount}> { } ``` -This allows the account to later access `petNames` when it needs to revoke/retarget a specific capability. This can also be used not just for pet names, but to add other metadata to issued capabilities. +This allows the account to later access `petNames` when it needs to revoke/retarget a specific capability. +This can also be used not just for pet names, but to add other metadata to issued capabilities. #### Capability Minters -In certain situations it is required that an issuer delegates issuing and revoking capabilities to someone else. In this case the following approach can be used. +In certain situations it is required that an issuer delegates issuing and revoking capabilities to someone else. +In this case the following approach can be used. Let's assume that the issuer defined an `AdminInterface` resource interface and a `Main` resource (besides the `Counter` and `HasCount` from previous examples). @@ -350,7 +410,8 @@ pub resource Main : AdminInterface { } ``` -The issuer can then store a `Main` resource in their storage and give the capability to call it to a trusted party. The trusted party can then create and revoke `&{HasCount}` capabilities at will. +The issuer can then store a `Main` resource in their storage and give the capability to call it to a trusted party. +The trusted party can then create and revoke `&{HasCount}` capabilities at will. ```cadence issuer.save(<-create Main(), to: /storage/counterMinter) @@ -358,9 +419,15 @@ let countMinterCap <- issuer.capabilities.issue<&{AdminInterface}>(/storage/coun countMinterCap // give this to a trusted party ``` -It is worth noting that every time the `AdminInterface` is used, a CapCon is created in the issuer's storage, taking up some resources. Therefore revoking capabilities does not free any storage. +It is worth noting that every time the `AdminInterface` is used, a CapCon is created in the issuer's storage, taking up some resources. +Therefore revoking capabilities does not free any storage. -In this example the delegatee with the `Capability<&{AdminInterface}>` can revoke any capability of type `&{HasCount}` even those that someone else created. This is sometimes desired (IT admins with the capability to issue purchase_hardware capabilities should have the ability to revoke what other IT admins issued), but sometimes we would like the delegatee to only revoke what it issued. If that is the case, another resource can be created `ScopedMain` that serves to limit which capabilities can be revoked. +In this example the delegatee with the `Capability<&{AdminInterface}>` can revoke any capability of type `&{HasCount}`, +even those that someone else created. +This is sometimes desired – for example, +IT admins with the capability to issue purchase_hardware capabilities should have the ability to revoke what other IT admins issued. +However, sometimes we would like the delegatee to only revoke what it issued. +If that is the case, another resource can be created `ScopedMain` that serves to limit which capabilities can be revoked. ```cadence pub resource ScopedMain : AdminInterface { @@ -392,16 +459,21 @@ pub resource ScopedMain : AdminInterface { ## Deployment options -Just making a breaking change by removing the linking API and adding the CapCons API might cause some headaches. In order to make the transition to CapCons smoother, the CapCons API could temporarily work in parallel with the current linking API. +Just making a breaking change by removing the linking API and adding the CapCons API might cause some headaches. +In order to make the transition to CapCons smoother, the CapCons API could temporarily work in parallel with the current linking API. The rollout process would have 2 steps: 1. Add CapCons and still have linking. 2. After a transition period remove linking. -To make this FLIP compatible with this rollout, the requirement of removing the private domain needs to be addressed. While both APIs are still active, the private domain still needs to exist in order to do private linking for private capabilities. The private domain can be removed at step 2. +To make this FLIP compatible with this rollout, the requirement of removing the private domain needs to be addressed. +While both APIs are still active, the private domain still needs to exist in order to do private linking for private capabilities. +The private domain can be removed at step 2. -During step 1, all existing links should also become CapCons (most likely with a storage migration). The linking API will still be available, but will actually operate with CapCons in the background. Below is an implementation of the linking API with the CapCons API. +During step 1, all existing links should also become CapCons (most likely with a storage migration). +The linking API will still be available, but will actually operate with CapCons in the background. +Below is an implementation of the linking API with the CapCons API. ```cadence // link @@ -445,15 +517,18 @@ issuer.save(cap, to: publicOrPrivatePath) ### Unexpected revocation -Let's say the issuer creates a capability **A** and gives it to Alice then creates a capability **B** and gives it to Bob. Alice then copies her capability creating **A'** and gives it to Charlie, creating the following situation: +Let's say the issuer creates a capability **A** and gives it to Alice then creates a capability **B** and gives it to Bob. +Alice then copies her capability creating **A'** and gives it to Charlie, creating the following situation: - Alice: has **A** - Bob: has **B** - Charlie: has **A'** -If the issuer decides to revoke **B**, Bob will no longer be able to use his capability. If the issuer revokes **A** Alice will not be able to use her capability, but perhaps unexpected to Charlie, he will also not be able to use **A'**. +If the issuer decides to revoke **B**, Bob will no longer be able to use his capability. +If the issuer revokes **A** Alice will not be able to use her capability, but perhaps unexpected to Charlie, he will also not be able to use **A'**. -Addressing this issue so that it would be clearer to Charlie that he received a capability that is a copy of Alice's, and not his own instance, is not in the scope of this FLIP. +Addressing this issue so that it would be clearer to Charlie that he received a capability that is a copy of Alice's, +and not his own instance, is not in the scope of this FLIP. This could perhaps be addressed by: - adding extra descriptors to capabilities (names) From 0a705fbc86f0d02c7045eb6df413c075628752fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Tue, 24 Jan 2023 15:51:37 -0800 Subject: [PATCH 18/44] improve comments --- flips/20220203-capability-controllers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index 4b6559a12..cebb74209 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -255,7 +255,7 @@ struct AuthAccount { fun borrow(_ path: PublicPath): T? // For each iterates through all the public capabilities of the public account. - // If function returns false, the iteration ends. + // Returning false from the function stops the iteration. fun forEach(_ function: ((PublicPath, Type): Bool)) // Get capability controller for capability with the specified id @@ -299,7 +299,7 @@ struct PublicAccount { fun borrow(_ path: PublicPath): T? // For each iterates through all the public capabilities of the public account. - // If function returns false, the iteration ends. + // Returning false from the function stops the iteration. fun forEach(_ function: ((PublicPath, Type): Bool)) } } From a34c3f52aede7d02288be62db6d6a1806227da17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Tue, 24 Jan 2023 15:55:28 -0800 Subject: [PATCH 19/44] fix uses of Capability's ID field --- flips/20220203-capability-controllers.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index cebb74209..dc545d667 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -374,7 +374,7 @@ access(account) petNames: {String: UInt64} access(account) issueHasCount(petName: String): Capability<&{HasCount}> { // for brevity this function is not handling pet name collision let cap = self.account.capabilities.issue<&{HasCount}>(/storage/counter) - self.petNames[petName] = cap.capabilityID + self.petNames[petName] = cap.id return cap } ``` @@ -444,7 +444,7 @@ pub resource ScopedMain : AdminInterface { fun createCountCap(): Capability<&{HasCount}> { let capability = self.inner.createCountCap() - self.issued[capability.capabilityID] = true + self.issued[capability.id] = true return capability } @@ -488,8 +488,7 @@ issuer.save(cap, to: publicOrPrivatePath) // issuer.unlink(publicOrPrivatePath) let cap = issuer.capabilities.get(publicOrPrivatePath)! -let capabilityID = cap.capabilityID -let capCon = issuer.capabilities.getController(byCapabilityID: capabilityID) +let capCon = issuer.capabilities.getController(byCapabilityID: cap.id) capCon.revoke() ``` @@ -502,8 +501,7 @@ capCon.revoke() // 1. unlink let cap = issuer.capabilities.get(publicOrPrivatePath)! -let capabilityID = cap.capabilityID -let capCon = issuer.capabilities.getController(byCapabilityID: capabilityID) +let capCon = issuer.capabilities.getController(byCapabilityID: cap.id) capCon.revoke() From fe89f8575d622b4c0b959bd0e6e2c0f919592dc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Fri, 27 Jan 2023 16:33:38 -0800 Subject: [PATCH 20/44] use docstring syntax --- flips/20220203-capability-controllers.md | 62 ++++++++++++------------ 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index dc545d667..342ce0024 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -195,30 +195,30 @@ The definition of the `CapabilityController`. ```cadence struct CapabilityController { - // The block height when the capability was created. + /// The block height when the capability was created. let issueHeight: UInt64 - // The Type of the capability, i.e.: the T in Capability. + /// The Type of the capability, i.e.: the T in Capability. let borrowType: Type - // The id of the related capability. - // This is the UUID of the created capability. - // All copies of the same capability will have the same UUID + /// The id of the related capability. + /// This is the UUID of the created capability. + /// All copies of the same capability will have the same UUID let capabilityID: UInt64 - // Is the capability revoked. + /// Is the capability revoked. let isRevoked: Bool - // Returns the targeted storage path of the capability. + /// Returns the targeted storage path of the capability. fun target(): StoragePath - // Revoke the capability making it no longer usable. - // When borrowing from a revoked capability the borrow returns nil. + /// Revoke the capability making it no longer usable. + /// When borrowing from a revoked capability the borrow returns nil. fun revoke() - // Retarget the capability. - // This moves the CapCon from one CapCon array to another. - // If the new target is not a valid target this will panic. + /// Retarget the capability. + /// This moves the CapCon from one CapCon array to another. + /// If the new target is not a valid target this will panic. fun retarget(target: StoragePath) fun getCapability(): Capability? @@ -246,31 +246,31 @@ struct AuthAccount { let capabilities: AuthAccount.Capabilities struct Capabilities { - // get returns the capability at the public path, if one was stored there. + /// get returns the capability at the public path, if one was stored there. fun get(_ path: PublicPath): Capability? - // borrow gets the capability at the given path, and borrows the capability if it exists. - // Returns `nil` if the capability does not exist or cannot be borrowed using the given type. - // The function is equivalent to `get(path)?.borrow()`. + /// borrow gets the capability at the given path, and borrows the capability if it exists. + /// Returns `nil` if the capability does not exist or cannot be borrowed using the given type. + /// The function is equivalent to `get(path)?.borrow()`. fun borrow(_ path: PublicPath): T? - // For each iterates through all the public capabilities of the public account. - // Returning false from the function stops the iteration. + /// For each iterates through all the public capabilities of the public account. + /// Returning false from the function stops the iteration. fun forEach(_ function: ((PublicPath, Type): Bool)) - // Get capability controller for capability with the specified id - // If the id does not reference an existing capability - // or the capability does not target a storage path on this address, return nil + /// Get capability controller for capability with the specified id + /// If the id does not reference an existing capability + /// or the capability does not target a storage path on this address, return nil fun getController(byCapabilityID: UInt64): CapabilityController? - // Get all capability controllers for capabilities that target this storage path + /// Get all capability controllers for capabilities that target this storage path fun getControllers(forPath: StoragePath): [CapabilityController] - // Iterate through all capability controllers for capabilities that target this storage path. - // Returning false from the function stops the iteration. + /// Iterate through all capability controllers for capabilities that target this storage path. + /// Returning false from the function stops the iteration. fun forEachController(forPath: StoragePath, function: ((CapabilityController): Bool)) - // Issue/create a new capability. + /// Issue/create a new capability. fun issue(_ path: StoragePath): Capability } } @@ -290,16 +290,16 @@ struct PublicAccount { let capabilities: PublicAccount.Capabilities struct Capabilities { - // get returns the capability at the public path, if one was stored there. + /// get returns the capability at the public path, if one was stored there. fun get(_ path: PublicPath): Capability? - // borrow gets the capability at the given path, and borrows the capability if it exists. - // Returns `nil` if the capability does not exist or cannot be borrowed using the given type. - // The function is equivalent to `get(path)?.borrow()`. + /// borrow gets the capability at the given path, and borrows the capability if it exists. + /// Returns `nil` if the capability does not exist or cannot be borrowed using the given type. + /// The function is equivalent to `get(path)?.borrow()`. fun borrow(_ path: PublicPath): T? - // For each iterates through all the public capabilities of the public account. - // Returning false from the function stops the iteration. + /// For each iterates through all the public capabilities of the public account. + /// Returning false from the function stops the iteration. fun forEach(_ function: ((PublicPath, Type): Bool)) } } From 2a468fc0f49223be10cf65e6c2df2d6fcaf23463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Thu, 2 Feb 2023 14:13:06 -0800 Subject: [PATCH 21/44] remove CapabilityController.getCapability, maybe add later if needed --- flips/20220203-capability-controllers.md | 1 - 1 file changed, 1 deletion(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index 342ce0024..6b56442d2 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -221,7 +221,6 @@ struct CapabilityController { /// If the new target is not a valid target this will panic. fun retarget(target: StoragePath) - fun getCapability(): Capability? } ``` From fd39cb59f28d93cb0f43bc721fe968d23db1c0fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Thu, 2 Feb 2023 14:14:41 -0800 Subject: [PATCH 22/44] clarify comment of retarget function --- flips/20220203-capability-controllers.md | 1 - 1 file changed, 1 deletion(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index 6b56442d2..d6514be73 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -218,7 +218,6 @@ struct CapabilityController { /// Retarget the capability. /// This moves the CapCon from one CapCon array to another. - /// If the new target is not a valid target this will panic. fun retarget(target: StoragePath) } From b5b0417883f15c1152ac933e1b023b6161d0d82d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Thu, 2 Feb 2023 14:16:32 -0800 Subject: [PATCH 23/44] return references to capability controllers instead of owned types --- flips/20220203-capability-controllers.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index d6514be73..e22748f2d 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -259,14 +259,14 @@ struct AuthAccount { /// Get capability controller for capability with the specified id /// If the id does not reference an existing capability /// or the capability does not target a storage path on this address, return nil - fun getController(byCapabilityID: UInt64): CapabilityController? + fun getController(byCapabilityID: UInt64): &CapabilityController? /// Get all capability controllers for capabilities that target this storage path - fun getControllers(forPath: StoragePath): [CapabilityController] + fun getControllers(forPath: StoragePath): [&CapabilityController] /// Iterate through all capability controllers for capabilities that target this storage path. /// Returning false from the function stops the iteration. - fun forEachController(forPath: StoragePath, function: ((CapabilityController): Bool)) + fun forEachController(forPath: StoragePath, function: ((&CapabilityController): Bool)) /// Issue/create a new capability. fun issue(_ path: StoragePath): Capability From d3bfc4a4de6e981c573b71983690797e87e63087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Wed, 8 Feb 2023 11:44:26 -0800 Subject: [PATCH 24/44] capability IDs are not UUIDs --- flips/20220203-capability-controllers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index e22748f2d..eff720c5b 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -202,8 +202,8 @@ struct CapabilityController { let borrowType: Type /// The id of the related capability. - /// This is the UUID of the created capability. - /// All copies of the same capability will have the same UUID + /// This is the ID of the created capability. + /// All copies of the same capability will have the same ID let capabilityID: UInt64 /// Is the capability revoked. From 5611f6a3f0ef909192e5ab0562ad637875939a35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Mon, 13 Feb 2023 16:42:47 -0800 Subject: [PATCH 25/44] Replace revoke with delete --- flips/20220203-capability-controllers.md | 45 +++++++++++++----------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index eff720c5b..08e1b7f0a 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -129,7 +129,7 @@ There are two requirements of the capability system that must be satisfied. - **Revocation**: Any capability must be revocable by the issuer. - **Redirection**: The issuer should have the ability to redirect the capability to a different (compatible) object. -Revocation can be currently done by using the `unlink` function. +Capabilities can currently be revoked by using the `unlink` function. In the public example this would mean calling `issuer.unlink<&{HasCount}>(/public/hasCount)`, which would invalidate (break) all capabilities created from this public path @@ -182,7 +182,7 @@ The suggested change addresses these pain points by: Each Capability would have an associated CapCon that would be responsible for managing the Capability. The CapCon would be created when the Capability is issued (created). If the Capability is copied (since it is a value type) it shares the CapCon with the original -(i.e., calling revoke on the CapCon revokes all copies of the associated capability). +(i.e., calling `delete` on the CapCon revokes all copies of the associated capability). A CapCon and all the copies of the Capability that CapCon controls would have the same ID. Capability/CapCon ids are unique per account. @@ -195,26 +195,31 @@ The definition of the `CapabilityController`. ```cadence struct CapabilityController { - /// The block height when the capability was created. + /// The block height when the controlled capability was created. let issueHeight: UInt64 - /// The Type of the capability, i.e.: the T in Capability. + /// The type of the controlled capability, i.e. the T in `Capability`. let borrowType: Type - /// The id of the related capability. - /// This is the ID of the created capability. - /// All copies of the same capability will have the same ID + /// The identifier of the controlled capability. + /// All copies of a capability have the same ID. let capabilityID: UInt64 - /// Is the capability revoked. - let isRevoked: Bool - - /// Returns the targeted storage path of the capability. + /// Returns the targeted storage path of the controlled capability. fun target(): StoragePath - - /// Revoke the capability making it no longer usable. - /// When borrowing from a revoked capability the borrow returns nil. - fun revoke() + + /// Delete this capability controller, + /// and disable the controlled capability and its copies. + /// + /// The controller will be deleted from storage, + /// but the controlled capability and its copies remain. + /// + /// Once this function returns, the controller is no longer usable, + /// all further operations on the controller will panic. + /// + /// Borrowing from the controlled capability or its copies will return nil. + /// + fun delete() /// Retarget the capability. /// This moves the CapCon from one CapCon array to another. @@ -402,7 +407,7 @@ pub resource Main : AdminInterface { if capCon.borrowType != Type<&{HasCount}>() { return false // we have only delegated the issuance/revocation of &{HasCount} capabilities } - capCon.revoke() + capCon.delete() } } } @@ -417,8 +422,8 @@ let countMinterCap <- issuer.capabilities.issue<&{AdminInterface}>(/storage/coun countMinterCap // give this to a trusted party ``` -It is worth noting that every time the `AdminInterface` is used, a CapCon is created in the issuer's storage, taking up some resources. -Therefore revoking capabilities does not free any storage. +It is worth noting that every time the `AdminInterface` is used, a CapCon is created in the issuer's storage, taking up some storage space. +Revoking a capability by deleting its controller frees up the storage for the controller, but not for the capability and its copies. In this example the delegatee with the `Capability<&{AdminInterface}>` can revoke any capability of type `&{HasCount}`, even those that someone else created. @@ -488,7 +493,7 @@ issuer.save(cap, to: publicOrPrivatePath) let cap = issuer.capabilities.get(publicOrPrivatePath)! let capCon = issuer.capabilities.getController(byCapabilityID: cap.id) -capCon.revoke() +capCon.delete() ``` ```cadence @@ -501,7 +506,7 @@ capCon.revoke() let cap = issuer.capabilities.get(publicOrPrivatePath)! let capCon = issuer.capabilities.getController(byCapabilityID: cap.id) -capCon.revoke() +capCon.delete() // 2. link From 1974801c9fbca8122a73c7f95542a00cd5d0f25f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Wed, 15 Feb 2023 16:21:01 -0800 Subject: [PATCH 26/44] new Account.capabilities.borrow function needs type bound --- flips/20220203-capability-controllers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index 08e1b7f0a..c0668305e 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -255,7 +255,7 @@ struct AuthAccount { /// borrow gets the capability at the given path, and borrows the capability if it exists. /// Returns `nil` if the capability does not exist or cannot be borrowed using the given type. /// The function is equivalent to `get(path)?.borrow()`. - fun borrow(_ path: PublicPath): T? + fun borrow(_ path: PublicPath): T? /// For each iterates through all the public capabilities of the public account. /// Returning false from the function stops the iteration. From dfb411186320a33d273f329b4e74d07835ccf3f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Thu, 16 Feb 2023 13:09:12 -0800 Subject: [PATCH 27/44] clarify behaviour of AuthAccount.Capabilities.borrow --- flips/20220203-capability-controllers.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index c0668305e..c0ce28971 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -253,8 +253,14 @@ struct AuthAccount { fun get(_ path: PublicPath): Capability? /// borrow gets the capability at the given path, and borrows the capability if it exists. - /// Returns `nil` if the capability does not exist or cannot be borrowed using the given type. - /// The function is equivalent to `get(path)?.borrow()`. + /// Returns a reference to the object targeted by the capability. + /// + /// If no object is stored at the target path, the function returns nil. + /// + /// If there is an object stored, a reference is returned as an optional, provided it can be borrowed using the given type. + /// If the stored object cannot be borrowed using the given type, the function panics. + /// + /// The function is equivalent to `get(path)?.borrow()`. fun borrow(_ path: PublicPath): T? /// For each iterates through all the public capabilities of the public account. From 635b061d06eeee75abb0beb69f0cb81481e30b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Tue, 14 Mar 2023 16:22:25 -0700 Subject: [PATCH 28/44] integrate account capabilities --- flips/20220203-capability-controllers.md | 322 +++++++++++++---------- 1 file changed, 178 insertions(+), 144 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index c0ce28971..f792e0b93 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -7,18 +7,18 @@ ## Preface -Cadence encourages a [capability-based security](https://en.wikipedia.org/wiki/Capability-based_security) model -as described on the Flow [doc site](https://docs.onflow.org/cadence/language/capability-based-access-control). -Capabilities are themselves a new concept that most Cadence programmers need to understand, -but the API for syntax around Capabilities (especially the notion of “links” and “linking”), -and the associated concepts of the public and private storage domains, -lead to Capabilities being even more confusing and awkward to use. - -This proposal suggests that we could get rid of links entirely, -and replace them with Capability Controllers (henceforth referred to as CapCons), +Cadence encourages a [capability-based security](https://en.wikipedia.org/wiki/Capability-based_security) model +as described on the Flow [doc site](https://docs.onflow.org/cadence/language/capability-based-access-control). +Capabilities are themselves a new concept that most Cadence programmers need to understand, +but the API for syntax around Capabilities (especially the notion of “links” and “linking”), +and the associated concepts of the public and private storage domains, +lead to Capabilities being even more confusing and awkward to use. + +This proposal suggests that we could get rid of links entirely, +and replace them with Capability Controllers (henceforth referred to as CapCons), which could make Capabilities easier to understand, and easier to work with. -The following is a quick refresher of the current state of the Capabilities API +The following is a quick refresher of the current state of the Capabilities API (from the [Cadence documentation](https://docs.onflow.org/cadence/language/capability-based-access-control)). ### Example Resource definition @@ -47,14 +47,14 @@ issuer.save(<-create Counter(count: 42), to: /storage/counter) ### Public Capabilities -To allow anyone (read) access to the `count` field on the counter (during a transaction), +To allow anyone (read) access to the `count` field on the counter (during a transaction), the _issuer_ needs to create a public typed link at a chosen path that points to their stored counter resource. ```cadence issuer.link<&{HasCount}>(/public/hasCount, target: /storage/counter) ``` -Anyone can now read the `count` of the counter resource via the `HasCount` interface. +Anyone can now read the `count` of the counter resource via the `HasCount` interface. This can be done by - getting the `PublicAccount` object of the issuer (using the issuers address), @@ -70,7 +70,7 @@ countRef.count ### Private Capabilities -To allow only certain accounts/resources (read) access to the `count` field on the counter, +To allow only certain accounts/resources (read) access to the `count` field on the counter, the _issuer_ (of type `AuthAccount`) needs to create a private typed link at a chosen path that points to their stored counter resource. ```cadence @@ -132,39 +132,39 @@ There are two requirements of the capability system that must be satisfied. Capabilities can currently be revoked by using the `unlink` function. In the public example this would mean calling `issuer.unlink<&{HasCount}>(/public/hasCount)`, -which would invalidate (break) all capabilities created from this public path +which would invalidate (break) all capabilities created from this public path (both those created before unlink was called and those created after unlink was called). -In the private example the call would change to `issuer.unlink<&{HasCount}>(/private/hasCount)`. -It is important to note that if the issuer wants to have the ability to revoke/redirect capabilities in a more granular way -(instead of doing them all at once), +In the private example the call would change to `issuer.unlink<&{HasCount}>(/private/hasCount)`. +It is important to note that if the issuer wants to have the ability to revoke/redirect capabilities in a more granular way +(instead of doing them all at once), the solution is to create multiple private linked paths (e.g. `/private/hasCountAlice`, `/private/hasCountBob`). -If a path that was unlinked is linked again all the capabilities created from that path resume working. -This can be used to redirect a capability to a different object, -but can also be dangerous if done unintentionally, +If a path that was unlinked is linked again all the capabilities created from that path resume working. +This can be used to redirect a capability to a different object, +but can also be dangerous if done unintentionally, reviving a previously revoked capability. ### Capabilities are values -Capabilities are a value type. This means that any capability that an account has access to can be copied, -and potentially given to someone else. +Capabilities are a value type. This means that any capability that an account has access to can be copied, +and potentially given to someone else. The copied capability will use the link on the same path as the original capability. ## Problem statement -The current capability API has a few pain points: +The current capability API has a few pain points: -- The concepts capabilities/linking/revoking/redirecting are hard to understand for new developers that are coming to Flow, +- The concepts capabilities/linking/revoking/redirecting are hard to understand for new developers that are coming to Flow, as that is a lot of new concepts to grasp before one can get started. -- Issuing and managing private capabilities that can be revoked at a granular level, +- Issuing and managing private capabilities that can be revoked at a granular level, by creating a custom private linked path for each capability, is difficult. -- Unless the path name includes some hints, there is no way to know when a path was created, +- Unless the path name includes some hints, there is no way to know when a path was created, thus making it more difficult to remember why a certain capability was issued. -- If an old path (that has been unlinked) is accidentally reused and re-linked, +- If an old path (that has been unlinked) is accidentally reused and re-linked, this will revive a capability that is supposed to be revoked. ## Suggested change @@ -179,34 +179,36 @@ The suggested change addresses these pain points by: ### Capability Controllers (CapCons) -Each Capability would have an associated CapCon that would be responsible for managing the Capability. -The CapCon would be created when the Capability is issued (created). -If the Capability is copied (since it is a value type) it shares the CapCon with the original +Each Capability has an associated CapCon that is responsible for managing the Capability. +The CapCon is created when the Capability is issued. +When the Capability is copied (since it is a value type), +it shares the CapCon with the original capability (i.e., calling `delete` on the CapCon revokes all copies of the associated capability). -A CapCon and all the copies of the Capability that CapCon controls would have the same ID. -Capability/CapCon ids are unique per account. +The Capability, and all copies of that Capability, have the same ID. +Capability IDs are unique per account. -The data associated with CapCons would be stored in arrays, -so that each storage path on an account has an array of CapCons of Capabilities issued from that storage path. -CapCons are a non-storable object (similar to AuthAccounts). +CapCons are not storable (similar to e.g. AuthAccounts). -The definition of the `CapabilityController`. +There are two kinds of capabilities: +- Storage capability: Targets a storage path in an account +- Account capabilities: Targets an account. + This kind was introduced in [FLIP 53](https://github.com/onflow/flips/pull/53). + +Therefore, there are two kinds of Capability Controllers: +`StorageCapabilityController` and `AccountCapabilityController`. ```cadence -struct CapabilityController { +struct StorageCapabilityController { /// The block height when the controlled capability was created. let issueHeight: UInt64 - + /// The type of the controlled capability, i.e. the T in `Capability`. let borrowType: Type - + /// The identifier of the controlled capability. /// All copies of a capability have the same ID. let capabilityID: UInt64 - - /// Returns the targeted storage path of the controlled capability. - fun target(): StoragePath /// Delete this capability controller, /// and disable the controlled capability and its copies. @@ -220,103 +222,135 @@ struct CapabilityController { /// Borrowing from the controlled capability or its copies will return nil. /// fun delete() - + + /// Returns the targeted storage path of the controlled capability. + fun target(): StoragePath + /// Retarget the capability. /// This moves the CapCon from one CapCon array to another. fun retarget(target: StoragePath) +} +``` + +```cadence +struct AccountCapabilityController { + /// The block height when the controlled capability was created. + let issueHeight: UInt64 + + /// The type of the controlled capability, i.e. the T in `Capability`. + let borrowType: Type + + /// The identifier of the controlled capability. + /// All copies of a capability have the same ID. + let capabilityID: UInt64 + /// Delete this capability controller, + /// and disable the controlled capability and its copies. + /// + /// The controller will be deleted from storage, + /// but the controlled capability and its copies remain. + /// + /// Once this function returns, the controller is no longer usable, + /// all further operations on the controller will panic. + /// + /// Borrowing from the controlled capability or its copies will return nil. + /// + fun delete() } ``` -Capabilities would expose the new field `let id: UInt64`, in addition to the account field they already expose. +### Capabilities + +The `Capability` type is extended with the new field `let id: UInt64`. -The capability related methods in the `AuthAccount` and in the `PublicAccount` would be moved to a `capabilities` namespace, -similar to how the contract methods are in the [contracts namespace](https://developers.flow.com/cadence/language/accounts). +### AuthAccount Type -The `AuthAccount` would get new methods to create or get or iterate through capabilities, -and to get or iterate through CapCons in order to manage capabilities. -Methods used for (un)linking would be removed as they are no longer needed. +The capability management functions in `AuthAccount` will be grouped in nested objects, +similar to how the contract management functions are in a nested [`contracts` object](https://developers.flow.com/cadence/language/accounts). + +Functions related to linking will be removed, as they are no longer needed. ```cadence struct AuthAccount { // ... // removed: fun link(_ newCapabilityPath: CapabilityPath, target: Path): Capability? - // removed: fun getLinkTarget(_ path: CapabilityPath): Path? // removed: fun unlink(_ path: CapabilityPath) - // moved & renamed: fun getCapability(_ path: CapabilityPath): Capability - // moved & renamed: fun forEachPublic(_ function: ((PublicPath, Type): Bool)) - - let capabilities: AuthAccount.Capabilities - - struct Capabilities { - /// get returns the capability at the public path, if one was stored there. - fun get(_ path: PublicPath): Capability? - - /// borrow gets the capability at the given path, and borrows the capability if it exists. - /// Returns a reference to the object targeted by the capability. - /// - /// If no object is stored at the target path, the function returns nil. - /// - /// If there is an object stored, a reference is returned as an optional, provided it can be borrowed using the given type. - /// If the stored object cannot be borrowed using the given type, the function panics. - /// - /// The function is equivalent to `get(path)?.borrow()`. + // removed: fun getLinkTarget(_ path: CapabilityPath): Path? + // moved and renamed: fun getCapability(_ path: CapabilityPath): Capability + + let storageCapabilities: &AuthAccount.StorageCapabilities + let accountCapabilities: &AuthAccount.AccountCapabilities + + struct StorageCapabilities { + + /// get returns the storage capability at the given path, if one was stored there. + fun get(_ path: PublicPath): Capability? + + /// borrow gets the storage capability at the given path, and borrows the capability if it exists. + /// Returns nil if the capability does not exist or cannot be borrowed using the given type. + /// The function is equivalent to `getCapability(path)?.borrow()`. fun borrow(_ path: PublicPath): T? - - /// For each iterates through all the public capabilities of the public account. + + /// Get the storage capability controller for the capability with the specified ID. + /// Returns nil if the ID does not reference an existing storage capability. + fun getController(byCapabilityID: UInt64): &StorageCapabilityController? + + /// Get all storage capability controllers for capabilities that target this storage path + fun getControllers(forPath: StoragePath): [&StorageCapabilityController] + + /// Iterate through all storage capability controllers for capabilities that target this storage path. /// Returning false from the function stops the iteration. - fun forEach(_ function: ((PublicPath, Type): Bool)) + fun forEachController(forPath: StoragePath, function: ((&StorageCapabilityController): Bool)) - /// Get capability controller for capability with the specified id - /// If the id does not reference an existing capability - /// or the capability does not target a storage path on this address, return nil - fun getController(byCapabilityID: UInt64): &CapabilityController? + /// Issue/create a new storage capability. + fun issue(_ path: StoragePath): Capability + } - /// Get all capability controllers for capabilities that target this storage path - fun getControllers(forPath: StoragePath): [&CapabilityController] + struct AccountCapabilities { - /// Iterate through all capability controllers for capabilities that target this storage path. + /// Get capability controller for capability with the specified ID. + /// Returns nil if the ID does not reference an existing account capability. + fun getController(byCapabilityID: UInt64): &AccountCapabilityController? + + /// Get all capability controllers for all account capabilities. + fun getControllers(): [&AccountCapabilityController] + + /// Iterate through all account capability controllers for all account capabilities. /// Returning false from the function stops the iteration. - fun forEachController(forPath: StoragePath, function: ((&CapabilityController): Bool)) + fun forEachController(_ function: ((&AccountCapabilityController): Bool)) - /// Issue/create a new capability. - fun issue(_ path: StoragePath): Capability + /// Issue/create a new account capability. + fun issue(): Capability<&AuthAccount> } } ``` -The `PublicAccount` would get similar changes, but only for capabilities, -as CapCons are not meant to be accessible outside of `AuthAccount`. - +### PublicAccount Type ```cadence struct PublicAccount { // ... - // removed: fun getCapability(_ path: PublicPath): Capability // removed: fun getLinkTarget(_ path: CapabilityPath): Path? - // removed: fun forEachPublic(_ function: ((PublicPath, Type): Bool)) + // moved and renamed: fun getCapability(_ path: PublicPath): Capability - let capabilities: PublicAccount.Capabilities - - struct Capabilities { - /// get returns the capability at the public path, if one was stored there. - fun get(_ path: PublicPath): Capability? + let storageCapabilities: &PublicAccount.StorageCapabilities - /// borrow gets the capability at the given path, and borrows the capability if it exists. - /// Returns `nil` if the capability does not exist or cannot be borrowed using the given type. - /// The function is equivalent to `get(path)?.borrow()`. - fun borrow(_ path: PublicPath): T? + struct StorageCapabilities { - /// For each iterates through all the public capabilities of the public account. - /// Returning false from the function stops the iteration. - fun forEach(_ function: ((PublicPath, Type): Bool)) + /// get returns the storage capability at the given path, if one was stored there. + fun get(_ path: PublicPath): Capability? + + /// borrow gets the storage capability at the given path, and borrows the capability if it exists. + /// Returns nil if the capability does not exist or cannot be borrowed using the given type. + /// The function is equivalent to `getCapability(path)?.borrow()`. + fun borrow(_ path: PublicPath): T? } } ``` -### Impact of the solution +## Impact of the solution -#### Changes for capability consumers +### Changes for capability consumers The following pattern: @@ -331,49 +365,51 @@ Would change to: ```cadence let publicAccount = getAccount(issuerAddress) -let countCap = publicAccount.capabilities.get<&{HasCount}>(/public/hasCount)! +let countCap = publicAccount.getCapability<&{HasCount}>(/public/hasCount)! let countRef = countCap.borrow()! countRef.count ``` -Or using the `borrow` shorthand: +(Note how the `getCapability` function returns an optional now.) + +Or using the `borrowCapability` shorthand: ```cadence let publicAccount = getAccount(issuerAddress) -let countRef = publicAccount.capabilities.borrow<&{HasCount}>(/public/hasCount)! +let countRef = publicAccount.borrowCapability<&{HasCount}>(/public/hasCount)! countRef.count ``` -#### Changes for capability issuers +### Changes for capability issuers There would be more change on the issuer's side. Most notably creating a public capability would look like this. ```cadence -let countCap = issuer.capabilities.issue<&{HasCount}>(/storage/counter) +let countCap = issuer.storageCapabilities.issue<&{HasCount}>(/storage/counter) issuer.save(countCap, to: /public/hasCount) ``` -Unlinking and retargeting issued capabilities would change to getting a CapCon and calling the appropriate methods. +Unlinking and retargeting issued capabilities would change to getting a CapCon and calling the appropriate functions. ```cadence -let capCon = issuer.capabilities.getController(byCapabilityID: capabilityID) +let capCon = issuer.storageCapabilities.getController(byCapabilityID: capabilityID) capCon.revoke() // or capCon.retarget(target: /storage/counter2) ``` -This example assumes that the capability id is known. -This is always the case for capabilities in the accounts public domain, since the account has access to those directly. -For private capabilities that were given to someone else this can be achieved -by keeping an on-chain or an off-chain list of capability ids and some extra identifying information -(for example the address of the receiver of the capability). +This example assumes that the capability ID is known. +This is always the case for capabilities in the accounts public domain, since the account has access to those directly. +For private capabilities that were given to someone else this can be achieved +by keeping an on-chain or an off-chain list of capability ids and some extra identifying information +(for example the address of the receiver of the capability). If no such list was kept, the issuer can use the information on the CapCons, -retrieved through `issuer.capabilities.getControllers(path: StoragePath)`, to find the right id. +retrieved through e.g. `issuer.storageCapabilities.getControllers(path: StoragePath)`, to find the right ID. -#### Pet names for issued capabilities +### Pet names for issued capabilities -If needed the `account.capabilities.issue(path)` can be wrapped into a smart contract function, +If needed the capability issuing functions can be wrapped into a contract function, so that the issuer can more easily keep track of what was issued. ```cadence @@ -382,18 +418,18 @@ access(account) petNames: {String: UInt64} access(account) issueHasCount(petName: String): Capability<&{HasCount}> { // for brevity this function is not handling pet name collision - let cap = self.account.capabilities.issue<&{HasCount}>(/storage/counter) + let cap = self.account.storageCapabilities.issue<&{HasCount}>(/storage/counter) self.petNames[petName] = cap.id return cap } ``` -This allows the account to later access `petNames` when it needs to revoke/retarget a specific capability. +This allows the account to later access `petNames` when it needs to revoke/retarget a specific capability. This can also be used not just for pet names, but to add other metadata to issued capabilities. -#### Capability Minters +### Capability Minters -In certain situations it is required that an issuer delegates issuing and revoking capabilities to someone else. +In certain situations it is required that an issuer delegates issuing and revoking capabilities to someone else. In this case the following approach can be used. Let's assume that the issuer defined an `AdminInterface` resource interface and a `Main` resource (besides the `Counter` and `HasCount` from previous examples). @@ -405,11 +441,11 @@ pub resource interface AdminInterface { } pub resource Main : AdminInterface { fun createCountCap(): Capability<&{HasCount}> { - return self.account.capabilities.issue<&{HasCount}>(/storage/counter) + return self.account.storageCapabilities.issue<&{HasCount}>(/storage/counter) } fun revokeCountCap(capabilityID: UInt64) { - if let capCon = self.account.capabilities.getController(byCapabilityID: capabilityID) { + if let capCon = self.account.storageCapabilities.getController(byCapabilityID: capabilityID) { if capCon.borrowType != Type<&{HasCount}>() { return false // we have only delegated the issuance/revocation of &{HasCount} capabilities } @@ -419,23 +455,23 @@ pub resource Main : AdminInterface { } ``` -The issuer can then store a `Main` resource in their storage and give the capability to call it to a trusted party. +The issuer can then store a `Main` resource in their storage and give the capability to call it to a trusted party. The trusted party can then create and revoke `&{HasCount}` capabilities at will. ```cadence issuer.save(<-create Main(), to: /storage/counterMinter) -let countMinterCap <- issuer.capabilities.issue<&{AdminInterface}>(/storage/counterMinter) +let countMinterCap <- issuer.storageCapabilities.issue<&{AdminInterface}>(/storage/counterMinter) countMinterCap // give this to a trusted party ``` -It is worth noting that every time the `AdminInterface` is used, a CapCon is created in the issuer's storage, taking up some storage space. +It is worth noting that every time the `AdminInterface` is used, a CapCon is created in the issuer's storage, taking up some storage space. Revoking a capability by deleting its controller frees up the storage for the controller, but not for the capability and its copies. In this example the delegatee with the `Capability<&{AdminInterface}>` can revoke any capability of type `&{HasCount}`, -even those that someone else created. -This is sometimes desired – for example, -IT admins with the capability to issue purchase_hardware capabilities should have the ability to revoke what other IT admins issued. -However, sometimes we would like the delegatee to only revoke what it issued. +even those that someone else created. +This is sometimes desired – for example, +IT admins with the capability to issue purchase_hardware capabilities should have the ability to revoke what other IT admins issued. +However, sometimes we would like the delegatee to only revoke what it issued. If that is the case, another resource can be created `ScopedMain` that serves to limit which capabilities can be revoked. ```cadence @@ -468,7 +504,7 @@ pub resource ScopedMain : AdminInterface { ## Deployment options -Just making a breaking change by removing the linking API and adding the CapCons API might cause some headaches. +Just making a breaking change by removing the linking API and adding the CapCons API might cause some headaches. In order to make the transition to CapCons smoother, the CapCons API could temporarily work in parallel with the current linking API. The rollout process would have 2 steps: @@ -476,19 +512,19 @@ The rollout process would have 2 steps: 1. Add CapCons and still have linking. 2. After a transition period remove linking. -To make this FLIP compatible with this rollout, the requirement of removing the private domain needs to be addressed. -While both APIs are still active, the private domain still needs to exist in order to do private linking for private capabilities. +To make this FLIP compatible with this rollout, the requirement of removing the private domain needs to be addressed. +While both APIs are still active, the private domain still needs to exist in order to do private linking for private capabilities. The private domain can be removed at step 2. -During step 1, all existing links should also become CapCons (most likely with a storage migration). -The linking API will still be available, but will actually operate with CapCons in the background. +During step 1, all existing links should also become CapCons (most likely with a storage migration). +The linking API will still be available, but will actually operate with CapCons in the background. Below is an implementation of the linking API with the CapCons API. ```cadence // link // issuer.link(publicOrPrivatePath, target: storagePath) -let cap = issuer.capabilities.issue(storagePath) +let cap = issuer.storageCapabilities.issue(storagePath) issuer.save(cap, to: publicOrPrivatePath) ``` @@ -496,9 +532,8 @@ issuer.save(cap, to: publicOrPrivatePath) // unlink // issuer.unlink(publicOrPrivatePath) -let cap = issuer.capabilities.get(publicOrPrivatePath)! -let capCon = issuer.capabilities.getController(byCapabilityID: cap.id) - +let cap = issuer.storageCapabilities.get(publicOrPrivatePath)! +let capCon = issuer.storageCapabilities.getController(byCapabilityID: cap.id) capCon.delete() ``` @@ -509,14 +544,13 @@ capCon.delete() // 1. unlink -let cap = issuer.capabilities.get(publicOrPrivatePath)! -let capCon = issuer.capabilities.getController(byCapabilityID: cap.id) - +let cap = issuer.storageCapabilities.get(publicOrPrivatePath)! +let capCon = issuer.storageCapabilities.getController(byCapabilityID: cap.id) capCon.delete() // 2. link -let cap = issuer.capabilities.issue(storagePath2) +let cap = issuer.storageCapabilities.issue(storagePath2) issuer.save(cap, to: publicOrPrivatePath) ``` @@ -524,17 +558,17 @@ issuer.save(cap, to: publicOrPrivatePath) ### Unexpected revocation -Let's say the issuer creates a capability **A** and gives it to Alice then creates a capability **B** and gives it to Bob. +Let's say the issuer creates a capability **A** and gives it to Alice then creates a capability **B** and gives it to Bob. Alice then copies her capability creating **A'** and gives it to Charlie, creating the following situation: - Alice: has **A** - Bob: has **B** - Charlie: has **A'** -If the issuer decides to revoke **B**, Bob will no longer be able to use his capability. +If the issuer decides to revoke **B**, Bob will no longer be able to use his capability. If the issuer revokes **A** Alice will not be able to use her capability, but perhaps unexpected to Charlie, he will also not be able to use **A'**. -Addressing this issue so that it would be clearer to Charlie that he received a capability that is a copy of Alice's, +Addressing this issue so that it would be clearer to Charlie that he received a capability that is a copy of Alice's, and not his own instance, is not in the scope of this FLIP. This could perhaps be addressed by: From 0bd9d6b2c56c66d4e50beac31d948beffad9b234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Tue, 14 Mar 2023 16:35:25 -0700 Subject: [PATCH 29/44] remove issue height field --- flips/20220203-capability-controllers.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index f792e0b93..a1729e99d 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -200,8 +200,6 @@ Therefore, there are two kinds of Capability Controllers: ```cadence struct StorageCapabilityController { - /// The block height when the controlled capability was created. - let issueHeight: UInt64 /// The type of the controlled capability, i.e. the T in `Capability`. let borrowType: Type @@ -234,8 +232,6 @@ struct StorageCapabilityController { ```cadence struct AccountCapabilityController { - /// The block height when the controlled capability was created. - let issueHeight: UInt64 /// The type of the controlled capability, i.e. the T in `Capability`. let borrowType: Type From 4dc251c32ef8b91348bef3deff8e8b1c103509f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Tue, 14 Mar 2023 16:35:35 -0700 Subject: [PATCH 30/44] describe migration of existing data --- flips/20220203-capability-controllers.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index a1729e99d..57b2d59c3 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -550,6 +550,21 @@ let cap = issuer.storageCapabilities.issue(storagePath2) issuer.save(cap, to: publicOrPrivatePath) ``` +## Migration of existing data + +Existing links and capabilities need to be migrated. + +All existing storage links and all storage capabilities will get grouped by path, +and a CapCon with a unique ID will be generated for each group. + +This behaviour is similar to how unlinking revokes and relinking retargets all capabilities with the same path. + +For all existing account links and account capabilities +a separate CapCon with a unique ID will be generated. + +Capabilities do not currently have an ID. +Existing capabilities will get assigned the ID of the associated CapCon. + ## Issues not addressed in this FLIP ### Unexpected revocation From 51fa67b38393810e0b9f0e86c2d1eae8724f7560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Fri, 24 Mar 2023 15:22:44 -0700 Subject: [PATCH 31/44] make AccountCapabilities.issue future-proof --- flips/20220203-capability-controllers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index 57b2d59c3..c95c158d2 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -316,7 +316,7 @@ struct AuthAccount { fun forEachController(_ function: ((&AccountCapabilityController): Bool)) /// Issue/create a new account capability. - fun issue(): Capability<&AuthAccount> + fun issue(): Capability } } ``` From 8099abef5fc60787678647dd38ec9e7de7e0cb81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Fri, 24 Mar 2023 15:46:19 -0700 Subject: [PATCH 32/44] update front matter --- flips/20220203-capability-controllers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index c95c158d2..dcd734aee 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -1,7 +1,7 @@ | status: Proposed | flip: [798](https://github.com/onflow/flow/pull/798) -| author: Janez Podhostnik (janez.podhostnik@dapperlabs.com) -| updated: 2022-10-18 +| author: Janez Podhostnik (janez.podhostnik@dapperlabs.com), Bastian Müller (bastian@dapperlabs.com) +| updated: 2023-03-24 # Capability controllers From 6cdbe3cb34607a0b3017d6b581ecf04364f435da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Tue, 4 Apr 2023 13:26:16 -0700 Subject: [PATCH 33/44] fix docstrings and examples --- flips/20220203-capability-controllers.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index dcd734aee..a90f01b4e 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -284,7 +284,7 @@ struct AuthAccount { /// borrow gets the storage capability at the given path, and borrows the capability if it exists. /// Returns nil if the capability does not exist or cannot be borrowed using the given type. - /// The function is equivalent to `getCapability(path)?.borrow()`. + /// The function is equivalent to `get(path)?.borrow()`. fun borrow(_ path: PublicPath): T? /// Get the storage capability controller for the capability with the specified ID. @@ -338,7 +338,7 @@ struct PublicAccount { /// borrow gets the storage capability at the given path, and borrows the capability if it exists. /// Returns nil if the capability does not exist or cannot be borrowed using the given type. - /// The function is equivalent to `getCapability(path)?.borrow()`. + /// The function is equivalent to `get(path)?.borrow()`. fun borrow(_ path: PublicPath): T? } } @@ -361,18 +361,18 @@ Would change to: ```cadence let publicAccount = getAccount(issuerAddress) -let countCap = publicAccount.getCapability<&{HasCount}>(/public/hasCount)! +let countCap = publicAccount.storageCapabilities.get<&{HasCount}>(/public/hasCount)! let countRef = countCap.borrow()! countRef.count ``` -(Note how the `getCapability` function returns an optional now.) +(Note how the `get` function returns an optional now.) -Or using the `borrowCapability` shorthand: +Or using the `borrow` convenience function: ```cadence let publicAccount = getAccount(issuerAddress) -let countRef = publicAccount.borrowCapability<&{HasCount}>(/public/hasCount)! +let countRef = publicAccount.storageCapabilities.borrow<&{HasCount}>(/public/hasCount)! countRef.count ``` From 10229d66c2644905359b760df026f5b2cf863e7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Thu, 13 Apr 2023 16:58:08 -0700 Subject: [PATCH 34/44] add publishing/unpublishing functions, revert get to getCapability --- flips/20220203-capability-controllers.md | 110 +++++++++++++---------- 1 file changed, 61 insertions(+), 49 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index a90f01b4e..ee19a4f59 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -199,14 +199,14 @@ Therefore, there are two kinds of Capability Controllers: `StorageCapabilityController` and `AccountCapabilityController`. ```cadence -struct StorageCapabilityController { +pub struct StorageCapabilityController { /// The type of the controlled capability, i.e. the T in `Capability`. - let borrowType: Type + pub let borrowType: Type /// The identifier of the controlled capability. /// All copies of a capability have the same ID. - let capabilityID: UInt64 + pub let capabilityID: UInt64 /// Delete this capability controller, /// and disable the controlled capability and its copies. @@ -219,26 +219,26 @@ struct StorageCapabilityController { /// /// Borrowing from the controlled capability or its copies will return nil. /// - fun delete() + pub fun delete() /// Returns the targeted storage path of the controlled capability. - fun target(): StoragePath + pub fun target(): StoragePath /// Retarget the capability. /// This moves the CapCon from one CapCon array to another. - fun retarget(target: StoragePath) + pub fun retarget(target: StoragePath) } ``` ```cadence -struct AccountCapabilityController { +pub struct AccountCapabilityController { /// The type of the controlled capability, i.e. the T in `Capability`. - let borrowType: Type + pub let borrowType: Type /// The identifier of the controlled capability. /// All copies of a capability have the same ID. - let capabilityID: UInt64 + pub let capabilityID: UInt64 /// Delete this capability controller, /// and disable the controlled capability and its copies. @@ -251,7 +251,7 @@ struct AccountCapabilityController { /// /// Borrowing from the controlled capability or its copies will return nil. /// - fun delete() + pub fun delete() } ``` @@ -267,56 +267,71 @@ similar to how the contract management functions are in a nested [`contracts` ob Functions related to linking will be removed, as they are no longer needed. ```cadence -struct AuthAccount { +pub struct AuthAccount { // ... // removed: fun link(_ newCapabilityPath: CapabilityPath, target: Path): Capability? // removed: fun unlink(_ path: CapabilityPath) // removed: fun getLinkTarget(_ path: CapabilityPath): Path? - // moved and renamed: fun getCapability(_ path: CapabilityPath): Capability + // changed: fun getCapability(_ path: CapabilityPath): Capability - let storageCapabilities: &AuthAccount.StorageCapabilities - let accountCapabilities: &AuthAccount.AccountCapabilities + /// Returns the capability at the given public path. + /// Returns nil if the capability does not exist, + /// or if the given type is not a supertype of the capability's borrow type. + pub fun getCapability(_ path: PublicPath): Capability? - struct StorageCapabilities { + /// Borrows the capability at the given public path. + /// Returns nil if the capability does not exist, or cannot be borrowed using the given type. + /// The function is equivalent to `getCapability(path)?.borrow()`. + pub fun borrowCapability(_ path: PublicPath): T? - /// get returns the storage capability at the given path, if one was stored there. - fun get(_ path: PublicPath): Capability? + pub let storageCapabilities: &AuthAccount.StorageCapabilities + pub let accountCapabilities: &AuthAccount.AccountCapabilities - /// borrow gets the storage capability at the given path, and borrows the capability if it exists. - /// Returns nil if the capability does not exist or cannot be borrowed using the given type. - /// The function is equivalent to `get(path)?.borrow()`. - fun borrow(_ path: PublicPath): T? + /// Publish the capability at the given public path. + /// + /// If there is already a capability published under the given path, the program aborts. + /// + /// The path must be a public path, i.e., only the domain `public` is allowed. + pub fun publishCapability(_ capability: Capability, at: PublicPath) + + /// Unpublishes the capability published at the given path. + /// + /// Returns the capability if one was published at the path. + /// Returns nil if no capability was published at the path. + pub fun unpublishCapability(_ path: PublicPath): Capability? + + pub struct StorageCapabilities { /// Get the storage capability controller for the capability with the specified ID. /// Returns nil if the ID does not reference an existing storage capability. - fun getController(byCapabilityID: UInt64): &StorageCapabilityController? + pub fun getController(byCapabilityID: UInt64): &StorageCapabilityController? /// Get all storage capability controllers for capabilities that target this storage path - fun getControllers(forPath: StoragePath): [&StorageCapabilityController] + pub fun getControllers(forPath: StoragePath): [&StorageCapabilityController] /// Iterate through all storage capability controllers for capabilities that target this storage path. /// Returning false from the function stops the iteration. - fun forEachController(forPath: StoragePath, function: ((&StorageCapabilityController): Bool)) + pub fun forEachController(forPath: StoragePath, function: ((&StorageCapabilityController): Bool)) /// Issue/create a new storage capability. - fun issue(_ path: StoragePath): Capability + pub fun issue(_ path: StoragePath): Capability } - struct AccountCapabilities { + pub struct AccountCapabilities { /// Get capability controller for capability with the specified ID. /// Returns nil if the ID does not reference an existing account capability. - fun getController(byCapabilityID: UInt64): &AccountCapabilityController? + pub fun getController(byCapabilityID: UInt64): &AccountCapabilityController? /// Get all capability controllers for all account capabilities. - fun getControllers(): [&AccountCapabilityController] + pub fun getControllers(): [&AccountCapabilityController] /// Iterate through all account capability controllers for all account capabilities. /// Returning false from the function stops the iteration. - fun forEachController(_ function: ((&AccountCapabilityController): Bool)) + pub fun forEachController(_ function: ((&AccountCapabilityController): Bool)) /// Issue/create a new account capability. - fun issue(): Capability + pub fun issue(): Capability } } ``` @@ -324,23 +339,20 @@ struct AuthAccount { ### PublicAccount Type ```cadence -struct PublicAccount { +pub struct PublicAccount { // ... // removed: fun getLinkTarget(_ path: CapabilityPath): Path? - // moved and renamed: fun getCapability(_ path: PublicPath): Capability - - let storageCapabilities: &PublicAccount.StorageCapabilities + // changed: fun getCapability(_ path: PublicPath): Capability - struct StorageCapabilities { + /// Returns the capability at the given public path. + /// Returns nil if the capability does not exist, + /// or if the given type is not a supertype of the capability's borrow type. + pub fun getCapability(_ path: PublicPath): Capability? - /// get returns the storage capability at the given path, if one was stored there. - fun get(_ path: PublicPath): Capability? - - /// borrow gets the storage capability at the given path, and borrows the capability if it exists. - /// Returns nil if the capability does not exist or cannot be borrowed using the given type. - /// The function is equivalent to `get(path)?.borrow()`. - fun borrow(_ path: PublicPath): T? - } + /// Borrows the capability at the given public path. + /// Returns nil if the capability does not exist, or cannot be borrowed using the given type. + /// The function is equivalent to `getCapability(path)?.borrow()`. + pub fun borrowCapability(_ path: PublicPath): T? } ``` @@ -361,18 +373,18 @@ Would change to: ```cadence let publicAccount = getAccount(issuerAddress) -let countCap = publicAccount.storageCapabilities.get<&{HasCount}>(/public/hasCount)! +let countCap = publicAccount.getCapability<&{HasCount}>(/public/hasCount)! let countRef = countCap.borrow()! countRef.count ``` -(Note how the `get` function returns an optional now.) +(Note how the `getCapability` function returns an optional now.) -Or using the `borrow` convenience function: +Or using the new `borrowCapability` convenience function: ```cadence let publicAccount = getAccount(issuerAddress) -let countRef = publicAccount.storageCapabilities.borrow<&{HasCount}>(/public/hasCount)! +let countRef = publicAccount.borrowCapability<&{HasCount}>(/public/hasCount)! countRef.count ``` @@ -528,7 +540,7 @@ issuer.save(cap, to: publicOrPrivatePath) // unlink // issuer.unlink(publicOrPrivatePath) -let cap = issuer.storageCapabilities.get(publicOrPrivatePath)! +let cap = issuer.getCapability(publicOrPrivatePath)! let capCon = issuer.storageCapabilities.getController(byCapabilityID: cap.id) capCon.delete() ``` @@ -540,7 +552,7 @@ capCon.delete() // 1. unlink -let cap = issuer.storageCapabilities.get(publicOrPrivatePath)! +let cap = issuer.getCapability(publicOrPrivatePath)! let capCon = issuer.storageCapabilities.getController(byCapabilityID: cap.id) capCon.delete() From 9ae0fb17fced1888e4a74e7237a92b2fcbe38822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Fri, 14 Apr 2023 11:18:32 -0700 Subject: [PATCH 35/44] refactor capability related functionality back into a common nested type. switch save calls to publish --- flips/20220203-capability-controllers.md | 126 +++++++++++++---------- 1 file changed, 70 insertions(+), 56 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index ee19a4f59..a48e444d9 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -272,33 +272,40 @@ pub struct AuthAccount { // removed: fun link(_ newCapabilityPath: CapabilityPath, target: Path): Capability? // removed: fun unlink(_ path: CapabilityPath) // removed: fun getLinkTarget(_ path: CapabilityPath): Path? - // changed: fun getCapability(_ path: CapabilityPath): Capability - - /// Returns the capability at the given public path. - /// Returns nil if the capability does not exist, - /// or if the given type is not a supertype of the capability's borrow type. - pub fun getCapability(_ path: PublicPath): Capability? - - /// Borrows the capability at the given public path. - /// Returns nil if the capability does not exist, or cannot be borrowed using the given type. - /// The function is equivalent to `getCapability(path)?.borrow()`. - pub fun borrowCapability(_ path: PublicPath): T? - - pub let storageCapabilities: &AuthAccount.StorageCapabilities - pub let accountCapabilities: &AuthAccount.AccountCapabilities - - /// Publish the capability at the given public path. - /// - /// If there is already a capability published under the given path, the program aborts. - /// - /// The path must be a public path, i.e., only the domain `public` is allowed. - pub fun publishCapability(_ capability: Capability, at: PublicPath) - - /// Unpublishes the capability published at the given path. - /// - /// Returns the capability if one was published at the path. - /// Returns nil if no capability was published at the path. - pub fun unpublishCapability(_ path: PublicPath): Capability? + // moved and renamed: + // old: fun getCapability(_ path: CapabilityPath): Capability + // new: fun Capabilities.get(_ path: CapabilityPath): Capability? + + pub let capabilities: &AuthAccount.Capabilities + + pub struct Capabilities { + + /// Returns the capability at the given public path. + /// Returns nil if the capability does not exist, + /// or if the given type is not a supertype of the capability's borrow type. + pub fun get(_ path: PublicPath): Capability? + + /// Borrows the capability at the given public path. + /// Returns nil if the capability does not exist, or cannot be borrowed using the given type. + /// The function is equivalent to `get(path)?.borrow()`. + pub fun borrow(_ path: PublicPath): T? + + pub let storage: &AuthAccount.StorageCapabilities + pub let account: &AuthAccount.AccountCapabilities + + /// Publish the capability at the given public path. + /// + /// If there is already a capability published under the given path, the program aborts. + /// + /// The path must be a public path, i.e., only the domain `public` is allowed. + pub fun publish(_ capability: Capability, at: PublicPath) + + /// Unpublish the capability published at the given path. + /// + /// Returns the capability if one was published at the path. + /// Returns nil if no capability was published at the path. + pub fun unpublish(_ path: PublicPath): Capability? + } pub struct StorageCapabilities { @@ -342,17 +349,24 @@ pub struct AuthAccount { pub struct PublicAccount { // ... // removed: fun getLinkTarget(_ path: CapabilityPath): Path? - // changed: fun getCapability(_ path: PublicPath): Capability + // moved and renamed: + // old: fun getCapability(_ path: CapabilityPath): Capability + // new: fun Capabilities.get(_ path: CapabilityPath): Capability? - /// Returns the capability at the given public path. - /// Returns nil if the capability does not exist, - /// or if the given type is not a supertype of the capability's borrow type. - pub fun getCapability(_ path: PublicPath): Capability? + pub let capabilities: &PublicAccount.Capabilities - /// Borrows the capability at the given public path. - /// Returns nil if the capability does not exist, or cannot be borrowed using the given type. - /// The function is equivalent to `getCapability(path)?.borrow()`. - pub fun borrowCapability(_ path: PublicPath): T? + pub struct Capabilities { + + /// Returns the capability at the given public path. + /// Returns nil if the capability does not exist, + /// or if the given type is not a supertype of the capability's borrow type. + pub fun get(_ path: PublicPath): Capability? + + /// Borrows the capability at the given public path. + /// Returns nil if the capability does not exist, or cannot be borrowed using the given type. + /// The function is equivalent to `get(path)?.borrow()`. + pub fun borrow(_ path: PublicPath): T? + } } ``` @@ -373,18 +387,18 @@ Would change to: ```cadence let publicAccount = getAccount(issuerAddress) -let countCap = publicAccount.getCapability<&{HasCount}>(/public/hasCount)! +let countCap = publicAccount.capabilities.get<&{HasCount}>(/public/hasCount)! let countRef = countCap.borrow()! countRef.count ``` -(Note how the `getCapability` function returns an optional now.) +(Note how the `get` function returns an optional now.) -Or using the new `borrowCapability` convenience function: +Or using the new `borrow` convenience function: ```cadence let publicAccount = getAccount(issuerAddress) -let countRef = publicAccount.borrowCapability<&{HasCount}>(/public/hasCount)! +let countRef = publicAccount.capabilities.borrow<&{HasCount}>(/public/hasCount)! countRef.count ``` @@ -393,14 +407,14 @@ countRef.count There would be more change on the issuer's side. Most notably creating a public capability would look like this. ```cadence -let countCap = issuer.storageCapabilities.issue<&{HasCount}>(/storage/counter) -issuer.save(countCap, to: /public/hasCount) +let countCap = issuer.capabilities.storage.issue<&{HasCount}>(/storage/counter) +issuer.capabilities.publish(countCap, to: /public/hasCount) ``` Unlinking and retargeting issued capabilities would change to getting a CapCon and calling the appropriate functions. ```cadence -let capCon = issuer.storageCapabilities.getController(byCapabilityID: capabilityID) +let capCon = issuer.capabilities.storage.getController(byCapabilityID: capabilityID) capCon.revoke() // or @@ -413,7 +427,7 @@ For private capabilities that were given to someone else this can be achieved by keeping an on-chain or an off-chain list of capability ids and some extra identifying information (for example the address of the receiver of the capability). If no such list was kept, the issuer can use the information on the CapCons, -retrieved through e.g. `issuer.storageCapabilities.getControllers(path: StoragePath)`, to find the right ID. +retrieved through e.g. `issuer.capabilities.storage.getControllers(path: StoragePath)`, to find the right ID. ### Pet names for issued capabilities @@ -426,7 +440,7 @@ access(account) petNames: {String: UInt64} access(account) issueHasCount(petName: String): Capability<&{HasCount}> { // for brevity this function is not handling pet name collision - let cap = self.account.storageCapabilities.issue<&{HasCount}>(/storage/counter) + let cap = self.account.capabilities.storage.issue<&{HasCount}>(/storage/counter) self.petNames[petName] = cap.id return cap } @@ -449,11 +463,11 @@ pub resource interface AdminInterface { } pub resource Main : AdminInterface { fun createCountCap(): Capability<&{HasCount}> { - return self.account.storageCapabilities.issue<&{HasCount}>(/storage/counter) + return self.account.capabilities.storage.issue<&{HasCount}>(/storage/counter) } fun revokeCountCap(capabilityID: UInt64) { - if let capCon = self.account.storageCapabilities.getController(byCapabilityID: capabilityID) { + if let capCon = self.account.capabilities.storage.getController(byCapabilityID: capabilityID) { if capCon.borrowType != Type<&{HasCount}>() { return false // we have only delegated the issuance/revocation of &{HasCount} capabilities } @@ -468,7 +482,7 @@ The trusted party can then create and revoke `&{HasCount}` capabilities at will. ```cadence issuer.save(<-create Main(), to: /storage/counterMinter) -let countMinterCap <- issuer.storageCapabilities.issue<&{AdminInterface}>(/storage/counterMinter) +let countMinterCap <- issuer.capabilities.storage.issue<&{AdminInterface}>(/storage/counterMinter) countMinterCap // give this to a trusted party ``` @@ -532,16 +546,16 @@ Below is an implementation of the linking API with the CapCons API. // link // issuer.link(publicOrPrivatePath, target: storagePath) -let cap = issuer.storageCapabilities.issue(storagePath) -issuer.save(cap, to: publicOrPrivatePath) +let cap = issuer.capabilities.storage.issue(storagePath) +issuer.capabilities.publish(cap, to: publicOrPrivatePath) ``` ```cadence // unlink // issuer.unlink(publicOrPrivatePath) -let cap = issuer.getCapability(publicOrPrivatePath)! -let capCon = issuer.storageCapabilities.getController(byCapabilityID: cap.id) +let cap = issuer.capabilities.get(publicOrPrivatePath)! +let capCon = issuer.capabilities.storage.getController(byCapabilityID: cap.id) capCon.delete() ``` @@ -552,14 +566,14 @@ capCon.delete() // 1. unlink -let cap = issuer.getCapability(publicOrPrivatePath)! -let capCon = issuer.storageCapabilities.getController(byCapabilityID: cap.id) +let cap = issuer.capabilities.get(publicOrPrivatePath)! +let capCon = issuer.capabilities.storage.getController(byCapabilityID: cap.id) capCon.delete() // 2. link -let cap = issuer.storageCapabilities.issue(storagePath2) -issuer.save(cap, to: publicOrPrivatePath) +let cap = issuer.capabilities.storage.issue(storagePath2) +issuer.capabilities.publish(cap, to: publicOrPrivatePath) ``` ## Migration of existing data From 27fc82fede5b9cf9da6aaeca375fefe193c5ac94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Fri, 14 Apr 2023 12:40:40 -0700 Subject: [PATCH 36/44] reorder AuthAccount.Capabilities members --- flips/20220203-capability-controllers.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index a48e444d9..f6dba24ce 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -280,6 +280,9 @@ pub struct AuthAccount { pub struct Capabilities { + pub let storage: &AuthAccount.StorageCapabilities + pub let account: &AuthAccount.AccountCapabilities + /// Returns the capability at the given public path. /// Returns nil if the capability does not exist, /// or if the given type is not a supertype of the capability's borrow type. @@ -290,9 +293,6 @@ pub struct AuthAccount { /// The function is equivalent to `get(path)?.borrow()`. pub fun borrow(_ path: PublicPath): T? - pub let storage: &AuthAccount.StorageCapabilities - pub let account: &AuthAccount.AccountCapabilities - /// Publish the capability at the given public path. /// /// If there is already a capability published under the given path, the program aborts. From 968ca782394ccde9909d43a35c19db5ae5f2e9df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Thu, 20 Apr 2023 10:57:44 -0700 Subject: [PATCH 37/44] improve StorageCapabilityController.retarget function --- flips/20220203-capability-controllers.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index f6dba24ce..7047d30b5 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -224,9 +224,9 @@ pub struct StorageCapabilityController { /// Returns the targeted storage path of the controlled capability. pub fun target(): StoragePath - /// Retarget the capability. - /// This moves the CapCon from one CapCon array to another. - pub fun retarget(target: StoragePath) + /// Retarget the controlled capability to the given storage path. + /// The path may be different or the same as the current path. + pub fun retarget(_ target: StoragePath) } ``` From 509b4853e956bc10801d70db5a51ad6fdeb246c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Fri, 21 Apr 2023 15:24:01 -0700 Subject: [PATCH 38/44] remove argument label for function parameter of AuthAccount.StorageCapabilities.forEachController --- flips/20220203-capability-controllers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index 7047d30b5..618dab647 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -318,7 +318,7 @@ pub struct AuthAccount { /// Iterate through all storage capability controllers for capabilities that target this storage path. /// Returning false from the function stops the iteration. - pub fun forEachController(forPath: StoragePath, function: ((&StorageCapabilityController): Bool)) + pub fun forEachController(forPath: StoragePath, _ function: ((&StorageCapabilityController): Bool)) /// Issue/create a new storage capability. pub fun issue(_ path: StoragePath): Capability From eb76d8a988d7bd96b5c6f2fe4498cec2631de183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Fri, 28 Apr 2023 14:30:54 -0700 Subject: [PATCH 39/44] improve type bound of AuthAccount.AccountCapabilities.issue, make supertype --- flips/20220203-capability-controllers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index 618dab647..062527bd0 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -338,7 +338,7 @@ pub struct AuthAccount { pub fun forEachController(_ function: ((&AccountCapabilityController): Bool)) /// Issue/create a new account capability. - pub fun issue(): Capability + pub fun issue(): Capability } } ``` From 9a4f57dd8c184a24e4e4503fef84c2e357541f53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Thu, 4 May 2023 15:44:43 -0700 Subject: [PATCH 40/44] update deployment and migration --- flips/20220203-capability-controllers.md | 77 ++++++++---------------- 1 file changed, 26 insertions(+), 51 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index 062527bd0..f1e3aa8e1 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -1,7 +1,7 @@ | status: Proposed | flip: [798](https://github.com/onflow/flow/pull/798) | author: Janez Podhostnik (janez.podhostnik@dapperlabs.com), Bastian Müller (bastian@dapperlabs.com) -| updated: 2023-03-24 +| updated: 2023-05-04 # Capability controllers @@ -524,66 +524,41 @@ pub resource ScopedMain : AdminInterface { } ``` -## Deployment options +## Deployment -Just making a breaking change by removing the linking API and adding the CapCons API might cause some headaches. -In order to make the transition to CapCons smoother, the CapCons API could temporarily work in parallel with the current linking API. +Replacing the linking API with the CapCons API is a breaking change. +In order to make the transition to CapCons as smooth as possible, +the CapCons API is going to be introduced before the linking API is going to be removed. +This transition period will allow developers to migrate links to CapCons +and migrate from using the linking API to the CapCons API. -The rollout process would have 2 steps: +During the transition period, both APIs will work side-by-side, +as two separate and alternative APIs for creating, accessing, and managing capabilities. +This means that links cannot be managed using the CapCons API, +and vice-versa, CapCons cannot be managed using the linking API. -1. Add CapCons and still have linking. -2. After a transition period remove linking. - -To make this FLIP compatible with this rollout, the requirement of removing the private domain needs to be addressed. -While both APIs are still active, the private domain still needs to exist in order to do private linking for private capabilities. -The private domain can be removed at step 2. - -During step 1, all existing links should also become CapCons (most likely with a storage migration). -The linking API will still be available, but will actually operate with CapCons in the background. -Below is an implementation of the linking API with the CapCons API. - -```cadence -// link -// issuer.link(publicOrPrivatePath, target: storagePath) - -let cap = issuer.capabilities.storage.issue(storagePath) -issuer.capabilities.publish(cap, to: publicOrPrivatePath) -``` - -```cadence -// unlink -// issuer.unlink(publicOrPrivatePath) - -let cap = issuer.capabilities.get(publicOrPrivatePath)! -let capCon = issuer.capabilities.storage.getController(byCapabilityID: cap.id) -capCon.delete() -``` - -```cadence -// relink -// issuer.unlink(publicOrPrivatePath) -// issuer.link<&{CounterContract.HasCount}>(publicOrPrivatePath, target: storagePath2) - -// 1. unlink - -let cap = issuer.capabilities.get(publicOrPrivatePath)! -let capCon = issuer.capabilities.storage.getController(byCapabilityID: cap.id) -capCon.delete() - -// 2. link - -let cap = issuer.capabilities.storage.issue(storagePath2) -issuer.capabilities.publish(cap, to: publicOrPrivatePath) -``` +However, to provide some support for backward-compatibility, +capabilities which are published using the CapCons API +will be accessible through the linking API function `getCapability`. ## Migration of existing data Existing links and capabilities need to be migrated. -All existing storage links and all storage capabilities will get grouped by path, +All existing storage links and all storage capabilities will get grouped by *target storage path* (`/storage`), and a CapCon with a unique ID will be generated for each group. -This behaviour is similar to how unlinking revokes and relinking retargets all capabilities with the same path. +To determine the target storage path, the link's *source capability path* (`/public` or `/private`) is followed +until a storage path is encountered. + +If the link is broken at the time of migration, e.g. because of a broken link, +then the capability cannot be successfully migrated. +In that case, the capability is assigned a unique capability ID, +which is effectively equivalent to a CapCon having existed for the capability at some point, but it was deleted. + +Note that it is not necessary for the storage path to contain any value at the time of migration. + +Public links (`/public`) are replaced with capabilities. For all existing account links and account capabilities a separate CapCon with a unique ID will be generated. From 0c34ed40464b162ba9a6f85291ff3ab4ef953935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Thu, 4 May 2023 15:59:40 -0700 Subject: [PATCH 41/44] clarify storability of controllers --- flips/20220203-capability-controllers.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index f1e3aa8e1..d3b279ef8 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -188,7 +188,10 @@ it shares the CapCon with the original capability The Capability, and all copies of that Capability, have the same ID. Capability IDs are unique per account. -CapCons are not storable (similar to e.g. AuthAccounts). +CapCons are stored inside of accounts. +However, they are not first-class values: +their creation, storage, and deletion is managed by the language, +through the usage of the CapCon API as described below. There are two kinds of capabilities: - Storage capability: Targets a storage path in an account From a1ed4433ce6e5c847f201e6be1567a2fb96f1995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Thu, 4 May 2023 16:30:45 -0700 Subject: [PATCH 42/44] add tag field to controllers, replace pet names section with one for the controller tag --- flips/20220203-capability-controllers.md | 31 +++++++++++------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index d3b279ef8..2b4afe33a 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -204,6 +204,11 @@ Therefore, there are two kinds of Capability Controllers: ```cadence pub struct StorageCapabilityController { + /// An arbitrary "tag" for the controller. + /// For example, it could be used to describe the purpose of the capability. + /// Empty by default. + pub var tag: String + /// The type of the controlled capability, i.e. the T in `Capability`. pub let borrowType: Type @@ -236,6 +241,11 @@ pub struct StorageCapabilityController { ```cadence pub struct AccountCapabilityController { + /// An arbitrary "tag" for the controller. + /// For example, it could be used to describe the purpose of the capability. + /// Empty by default. + pub var tag: String + /// The type of the controlled capability, i.e. the T in `Capability`. pub let borrowType: Type @@ -432,25 +442,12 @@ by keeping an on-chain or an off-chain list of capability ids and some extra ide If no such list was kept, the issuer can use the information on the CapCons, retrieved through e.g. `issuer.capabilities.storage.getControllers(path: StoragePath)`, to find the right ID. -### Pet names for issued capabilities - -If needed the capability issuing functions can be wrapped into a contract function, -so that the issuer can more easily keep track of what was issued. +### Tagging of issued capabilities -```cadence -// inside the Counter contract -access(account) petNames: {String: UInt64} - -access(account) issueHasCount(petName: String): Capability<&{HasCount}> { - // for brevity this function is not handling pet name collision - let cap = self.account.capabilities.storage.issue<&{HasCount}>(/storage/counter) - self.petNames[petName] = cap.id - return cap -} -``` +Capability Controllers can be tagged with an arbitrary string through the `tag` field. -This allows the account to later access `petNames` when it needs to revoke/retarget a specific capability. -This can also be used not just for pet names, but to add other metadata to issued capabilities. +For example, it can be used to describe the purpose or reason for the capability, +or it can be used to give the capability a [petname](https://en.wikipedia.org/wiki/Petname). ### Capability Minters From 3ee30df4021a17ae8dcb4af9caccb5b2e35dc118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Thu, 11 May 2023 13:17:30 -0700 Subject: [PATCH 43/44] clarify behaviour of controller iteration --- flips/20220203-capability-controllers.md | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index 2b4afe33a..ec005d3dc 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -329,8 +329,16 @@ pub struct AuthAccount { /// Get all storage capability controllers for capabilities that target this storage path pub fun getControllers(forPath: StoragePath): [&StorageCapabilityController] - /// Iterate through all storage capability controllers for capabilities that target this storage path. - /// Returning false from the function stops the iteration. + /// Iterate over all storage capability controllers for capabilities that target this storage path, + /// passing a reference to each controller to the provided callback function. + /// + /// Iteration is stopped early if the callback function returns `false`. + /// + /// If a new storage capability controller is issued for the path, + /// an existing storage capability controller for the path is deleted, + /// or a storage capability controller is retargeted from or to the path, + /// then the callback must stop iteration by returning false. + /// Otherwise, iteration aborts. pub fun forEachController(forPath: StoragePath, _ function: ((&StorageCapabilityController): Bool)) /// Issue/create a new storage capability. @@ -346,8 +354,15 @@ pub struct AuthAccount { /// Get all capability controllers for all account capabilities. pub fun getControllers(): [&AccountCapabilityController] - /// Iterate through all account capability controllers for all account capabilities. - /// Returning false from the function stops the iteration. + /// Iterate over all account capability controllers for all account capabilities, + /// passing a reference to each controller to the provided callback function. + /// + /// Iteration is stopped early if the callback function returns `false`. + /// + /// If a new account capability controller is issued for the account, + /// or an existing account capability controller for the account is deleted, + /// then the callback must stop iteration by returning false. + /// Otherwise, iteration aborts. pub fun forEachController(_ function: ((&AccountCapabilityController): Bool)) /// Issue/create a new account capability. From 8dba94ce628e2523e88dc8d4ac9ae438a5708084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Fri, 12 May 2023 10:44:40 -0700 Subject: [PATCH 44/44] Accept FLIP --- flips/20220203-capability-controllers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flips/20220203-capability-controllers.md b/flips/20220203-capability-controllers.md index ec005d3dc..7c63d5854 100644 --- a/flips/20220203-capability-controllers.md +++ b/flips/20220203-capability-controllers.md @@ -1,7 +1,7 @@ -| status: Proposed +| status: Accepted | flip: [798](https://github.com/onflow/flow/pull/798) | author: Janez Podhostnik (janez.podhostnik@dapperlabs.com), Bastian Müller (bastian@dapperlabs.com) -| updated: 2023-05-04 +| updated: 2023-05-12 # Capability controllers