-
Notifications
You must be signed in to change notification settings - Fork 164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
FLIP for publish
and claim
for capabilities
#1122
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
5fe3369
add design proposal for publish and claim
dsainati1 7f9a935
add some more details
dsainati1 786080c
Merge branch 'master' of github.com:onflow/flow
dsainati1 0d9b72d
add unpublish function
dsainati1 e7ba1e5
add permit/unpermit
dsainati1 93d2f5d
update FLIP to specify the API works only with Capabilities
dsainati1 3b00e7b
edit type params
dsainati1 9744533
update FLIP with meeting feedback
dsainati1 f82454f
publishing event includes recipient address
dsainati1 f4fc374
Update flips/20220908-publish-claim-capability.md
dsainati1 02bf15f
FLIP is accepted
dsainati1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
# Publish/Claim Capability | ||
|
||
| Status | (Accepted) | | ||
:-------------- |:---------------------------------------------------- | | ||
| **FLIP #** | [1122](https://github.com/onflow/flow/pull/1122) | | ||
| **Author(s)** | Daniel Sainati (daniel.sainati@dapperlabs.com) | | ||
| **Sponsor** | Daniel Sainati (daniel.sainati@dapperlabs.com) | | ||
| **Updated** | 2022-09-16 | | ||
| **Obsoletes** | https://github.com/onflow/flow/pull/945 | | ||
|
||
## Objective | ||
|
||
This adds two new functions that allow users to publish values for specific other | ||
users to claim, while ensuring that other users cannot claim these values. In particular, | ||
this dramatically simplifies the use case for bootstrapping capabilities. | ||
|
||
## Motivation | ||
|
||
https://github.com/onflow/flow/pull/945 proposed to add an `identity` resource-typed field | ||
on `AuthAccount` objects, to allow users to verify the identity of an account. In particular, | ||
this was intended to enable sharing values to specific users, and to simplify the capability bootstrapping | ||
usecase. In this use case, the owner of a resource, which we can call the Provider, | ||
wants to share a capability to that resource with another account, which we can call the Recipient. | ||
|
||
Currently the best way to do this in Cadence is for the Provider and the Recipient to co-sign a transaction | ||
in which the Provider creates the capability and then stores it in the Recipient's account. However, | ||
this is awkward because it requires the Provider and the Recipient to both sign the transaction within a specific | ||
time window, which can be difficult in highly asynchronous environments, e.g. collaboration between distant time-zones. | ||
|
||
This FLIP is designed to enable an alternative solution, in which the Provider can create the capability and place it somewhere | ||
where the Recipient can securely claim it at their leisure, without any pressure to do so within a specific time window and with | ||
no worry that someone else may come along and interfere. | ||
|
||
## User Benefit | ||
|
||
This will dramatically simplify the capability bootstrapping use case, as well as generally making it easier for users | ||
to share specific values directly with another account. | ||
|
||
## Design Proposal | ||
|
||
This adds a new field `inbox` to `AuthAccount`, that has a new `Inbox` struct type | ||
defined as a nested composite on `AuthAccount`: | ||
|
||
```cadence | ||
struct Inbox { | ||
fun publish(_ value: Capability, name: String, recipient: Address) | ||
|
||
fun unpublish<T : Any>(_ name: String): Capability<T>? | ||
|
||
fun claim<T: Any>(_ name: String, provider: Address): Capability<T>? | ||
} | ||
``` | ||
|
||
The `publish` function takes a `value` argument, a `name` string that identifies it, | ||
and a `recipient` address that specifies which account should be allowed to `claim` the | ||
published `value` later. When `publish` is called, `value` and its intended `recipient` are stored | ||
in a publishing dictionary on the calling account (not accessible to users), with the `name` as its key. | ||
Note that this means any values that have been `publish`ed but have yet to be `claim`ed will count towards | ||
the `publish`ing account's storage usage. | ||
|
||
The `claim` function takes the `name` of the value to be claimed and an `provider` address that | ||
specifies which account is providing the value, as well as a type argument `T` that specifies | ||
the type with which the value should be claimed. When called, `claim` will search the `provider`s | ||
publishing dictionary for the `name` key. If the key does not exist on the map, `claim` returns `nil`. | ||
|
||
If the key does exist, then `claim` compares the stored `recipient` (from the original call to `publish`) | ||
to see if it matches the address of the account calling `claim`. If it does not, then `claim` returns `nil`. | ||
If it does match, then the runtime type of the published `value` compared against the type argument `T`. If | ||
it does not match, then `claim` will produce an error. If it does match, `value` is removed from the `provider`'s dictionary and | ||
returned to the `claim` calling account. In effect, this means that a `publish`ed value can only be `claim`ed once. | ||
|
||
An example of how this might look, when bootstrapping a capability to a resource owned by 0x1: | ||
|
||
```cadence | ||
// transaction 1 (from the original resource owner 0x1) | ||
import MyIntf from 0x1 | ||
|
||
transaction() { | ||
prepare(acct: AuthAccount) { | ||
// create a capability to a resource stored at /storage/myResource. | ||
// With the changes proposed in https://github.com/onflow/flow/pull/798, this may | ||
// not involve linking to a concrete path, but at the moment the only way to create | ||
// a capability is via `link` | ||
let cap = acct.link<&MyIntf>(/private/myCapability, target: /storage/myResource) | ||
acct.inbox.publish(cap, name: "yourCapability", recipient: 0x2) | ||
} | ||
} | ||
``` | ||
|
||
```cadence | ||
// transaction 2 (from the reciever of the capability 0x2) | ||
import MyIntf from 0x1 | ||
|
||
transaction() { | ||
prepare(acct: AuthAccount) { | ||
let cap = acct.inbox.claim<Capability<MyIntf>>("yourCapability", provider: 0x1) | ||
// if we successfully obtain the capability, store it on our account | ||
if cap != nil { | ||
acct.save(cap, to: /storage/myCapability) | ||
} | ||
} | ||
} | ||
``` | ||
|
||
The `unpublish` function exists so that a provider can remove a published value from their storage, so that it | ||
stops taking up space if it goes un`claim`ed by its intended recipient. The calling account (the original provider | ||
of the value) calls `unpublish` with the same `name` that the value was originally stored with. If a value with that | ||
`name` is present in the account's publishing dictionary, and the provided type argument to `unpublish` is a supertype | ||
of that value's runtime type, then the function will return that value and remove it from the dictionary. Otherwise, | ||
the function returns `nil`. The type comparison is a force-cast like `load` and `claim`, so if the types do not match | ||
the program will fail. | ||
|
||
This FLIP also adds two new events to Cadence: | ||
|
||
```cadence | ||
event InboxValuePublished(provider: Address, recipient: Address, name: String, type: Type) | ||
turbolent marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
event InboxValueRemoved(provider: Address, remover: Address, name: String) | ||
``` | ||
|
||
These events are emitted whenever a value is added or removed from an inbox's publishing dictionary. When `publish` is called, | ||
`InboxValuePublished` is emitted containing the address of the `provider` and the `recipient`, the `name` the value was published with, and the runtime | ||
`type` it was published with. When a value is `claim`ed or `unpublish`ed, `InboxValueRemoved` is emitted, including the address the value was | ||
originally published from (`provider`), the address of the `remover` (in the case of `claim`, this is the recipient's address, in the case of | ||
`unpublish` it is the same as the `provider`), and the `name` of the removed event. | ||
|
||
### Alternatives Considered | ||
|
||
* The original proposal for this flip generalized `publish` and `claim` to work with resources, as well as Capabilities. | ||
While the generalized version was strictly more powerful, we felt that it was too likely to encourage users | ||
to write code in the `msg.sender` pattern instead of using it for the capability bootstrapping usecase. We have decided to | ||
restrict the API to capabilities for now, with the understanding that it is possible in the future to expand the API without breaking anything. | ||
|
||
### Best Practices | ||
|
||
* This will change the best practices for bootstrapping capabilities, which was previously only | ||
possible by cosigning a transaction. Instead, users should be encouraged to use the `publish`/`claim` pattern | ||
in order to handle any cases where they would like to pass a value directly from their account to a specific | ||
other. | ||
|
||
### Compatibility | ||
|
||
This will be backwards-compatible from the Cadence perspective, but will add a new field to | ||
every account (the dictionary of published objects). To support this we would need a storage migration | ||
to add empty dictionaries to all accounts during the spork that adds support for this feature. | ||
|
||
### User Impact | ||
|
||
* This should have no direct impact on users; it will not break any existing contracts. |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do I know what I can claim onChain if I cannot access it? Maybe add a method to be able to list what is there for you?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might also make sense to emit an event when something is initially added to be claimed as well? Or is that considered out of scope? Seems like something folks would want to track:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how valid this question is in the context of bootstrapping. When A is trying to give a capability specifically to B, does B really need on-chain information to help B claim the capability?
A probably doesn't need to package something for a random account that it doesn't have an off-chain connection to, or is there a use case I missing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another thing to add here is that if events need to be emitted, the publishing party can always write a wrapper function in a smart contract, that publishes and emits an event.
On the other hand, there is no way for the publisher to react when the value has been claimed. Maybe when publishing you could specify what happens when claiming? Some sort of callback. (that callback could emit an event or remove the published value)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coming back to this
Are you saying that events aren't necessary? Little confused by this. I specifically would advocate for a standardized event no matter what, the publisher should not be able to opt-out of that
A callback makes sense to me, otherwise the publisher would need to also listen to events and react to them in a separate transaction which doesn't feel right to me
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now we've settled on limiting this to just publishing/claiming capabilities, is a callback still necessary?
claim
ing a value automatically removes the publishing entry, so the publisher would not need to do that themselves. What is the use case (specifically as part of capability bootstrapping) for a callback here?