Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

FLIP for publish and claim for capabilities #1122

Merged
merged 11 commits into from
Sep 30, 2022
Merged

Conversation

dsainati1
Copy link
Contributor

Adds a new proposal for a pair of API functions: publish and claim that are designed to address the capability bootstrapping use case detailed here onflow/cadence#1951


For contributor use:

  • Targeted PR against master branch
  • Linked to Github issue with discussion and accepted design OR link to spec that describes this work.
  • Updated relevant documentation
  • Re-reviewed Files changed in the Github PR explorer
  • Added appropriate labels

@dsainati1 dsainati1 added FLIP Flow Improvement Proposal Cadence labels Sep 12, 2022
@dsainati1 dsainati1 self-assigned this Sep 12, 2022
@dsainati1
Copy link
Contributor Author

cc @bjartek @bluesign as you were involved in the discussion on the original FLIP

@bluesign
Copy link
Contributor

bluesign commented Sep 12, 2022

Maybe allowing claim to both accounts (sender and receiver) can be good. ( if I publish to wrong address etc )

Also will this dictionary be available to user ? Some kind of seeing what I published, when I published can be good. If I published you something 1 month ago, if you didn't claim I want to be able to clean probably.

@bluesign
Copy link
Contributor

Also fun claim<T: Any>(_ name: String, provider: Address): T? is there a reason this is not something like: getAccount(provider).claim<T:Any>(_ name: String)

I think it will be more familiar if we somehow use getAccount.

PS: unless we will have some kind of listing for potential things I can claim. But then we are becoming full inbox.


transaction() {
prepare(acct: AuthAccount) {
let cap = acct.claim<Capability<MyIntf>>("yourCapability", provider: 0x1)
Copy link
Contributor

Choose a reason for hiding this comment

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

Have you considered a conveniene method to save the claim to a path directly? As that is what most people would want to do?

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.
Copy link
Contributor

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?

Copy link
Contributor

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:

  • When a new capability or resource is claimable
  • When it is claimed
  • If the claimable was cancelled

Copy link
Contributor

@janezpodhostnik janezpodhostnik Sep 13, 2022

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?

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?

Copy link
Contributor

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)

Copy link
Contributor

Choose a reason for hiding this comment

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

Coming back to this

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.

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

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)

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

Copy link
Contributor Author

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? claiming 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?

@dsainati1
Copy link
Contributor Author

Also fun claim<T: Any>(_ name: String, provider: Address): T? is there a reason this is not something like: getAccount(provider).claim<T:Any>(_ name: String)

I think it will be more familiar if we somehow use getAccount.

PS: unless we will have some kind of listing for potential things I can claim. But then we are becoming full inbox.

At the moment the account calling the claim function has to be the recipient account: recipient.claim<T>(name, provider). If the provider is the account on which the claim function is called, then we have no argument that tells us which account is actually claiming the object.

@bluesign
Copy link
Contributor

At the moment the account calling the claim function has to be the recipient account: recipient.claim(name, provider). If the provider is the account on which the claim function is called, then we have no argument that tells us which account is actually claiming the object.

Thanks, makes sense

@austinkline
Copy link
Contributor

Would it make sense to have a third function to revoke/cancel a claimable item as well? Or would the assumption be that you would instead just destroy the underlying capability/resource being claimed which would mean it can't happen anyway

@dsainati1
Copy link
Contributor Author

Thanks for the quick comments on this! Based on the discussion so far, it seems like we need two more additional pieces of functionality here:

  1. The ability to revoke a published object. This would only be possible if the calling account was the one who originally published it (obviously) and would return the resource/capability/value in question, while removing it from the publishing dictionary.

  2. More information about what is available to be claimed and by whom. In the interest of not becoming a full inbox, as @bluesign said, I am leaning towards having this information be mostly off-chain, perhaps using events like @austinkline suggested. We could emit an event when something is published containing its name and who it is for, an event when it is claimed, and an event if it is unpublished. This way users who can choose to submit transactions to claim objects based on these events, but the publishing dictionary is not inspectable on-chain at will, as this would break the abstraction that the publish/claim API is intended to provide.

@bluesign
Copy link
Contributor

In the events discussion, problem here is: this is privileged function ( claim ), so we cannot use it as a building block, enrich with contracts, etc. So events are for sure important, but also I think some kind of on-chain query would be also nice. At least some list of addresses, which I can claim something ( like a notification )

@dsainati1
Copy link
Contributor Author

I added an unpublish function that takes the name of the published value and removes it and returns it.

Comment on lines 66 to 67
it does not match, then `claim` returns `nil`. 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.
Copy link
Contributor

Choose a reason for hiding this comment

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

value can only be claimed once

Whats the reason for this? Is there any issue with claiming the same capability multiple times. why not just leave it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This would allow the API to support publishing/claiming resources easily. If we decide that we only want this to work for capabilities we could discuss removing this limitation.

@katelynsills
Copy link

I'm very supportive of this FLIP! It's a major improvement over the identity object proposal, and it doesn't encourage the msg.sender pattern. However, I think it introduces a major issue with spam and other abuse.

Spam and Abuse

In capability-based security, a lot of the security comes from the fact that everyone is disconnected from each other until they explicitly consent to connect (and that connection can be revoked.) This FLIP proposes to change that. Now everyone has a direct, irrevocable connection to everyone else. Granted, there are some protections: the Provider is paying all the storage costs for what they are sharing.

However, relying on storage costs alone to prevent spam and abuse will break as soon as the incentive for abuse > storage costs. For instance, imagine a motivated and coordinated harassment attack, where the Recipient keeps getting notifications where the string name is used to send threatening and abusive messages. Thus, there needs to be an easy way to revoke this connection between accounts.

Solutions

An easy solution would be to add a block method to the AuthAccount, which takes a sender address. However, that's still putting the burden on the user to actively block, rather than allowing them to affirmatively consent to interaction with specific other accounts.

I wonder, since this FLIP is supposed to only be about solving the bootstrap problem, if the publish/claim API should be much more limited. Specifically, the Recipient could publish only one capability per Provider. And that capability would be of a particular type: a private inbox. In this case, there's no need for a name that can be used to transmit abusive messages from the general public.

Here's how I imagine Dete's example use case would work:

Dete wants to share the pause capability with Bjarte. Bjarte, knowing that Dete is unlikely to spam or abuse him, creates an inbox capability specifically for Dete that allows Dete to send Bjarte anything with no rate limits. Bjarte publishes this for Dete, and Dete claims it. Now Bjarte can receive capabilities and resources from Dete without having to actively claim. We could imagine in the future that Bjarte's inbox actually decides how the received things are named and stored in Bjarte's account, thus giving Dete an attenuated authority to Bjarte's storage, restricted to the things Dete has sent as well as other rules (like subfolders or resources within collections).

As another use case, let's imagine that rather than Dete, the sender is someone that Bjarte doesn't trust much:

Bjarte would want to only allow the minimum access that is needed. He wants the ability to revoke, of course, but also perhaps it's a one time inbox so access is automatically cut off without having to send another transaction to revoke.

Complications and other thoughts

  • Since the boundary between a Provider and a Recipient is not asynchronous, if a Provider creates a transaction that starts executing the Recipient's inbox rules, there's the possibility for reentrancy and other malicious behavior by the Recipient. Requiring a claim in a later transaction is actually nicely asynchronous.
  • As @janezpodhostnik has pointed out, the inbox model, which is often used in the capability world, doesn't work very well if the recipient pays for storage, due to the ability to spam. Possible solution: the recipient needs to be able to specify rules that run prior to storing anything on their side and prior to any notifications.

At this point, I think we definitely need to solve the spam/abuse problem, and I would love to find a way to do that using the private inbox pattern, since that is the traditional capability pattern for this. However, the above complications might make that infeasible. Still, any step towards a private inbox model, especially allowing for attenuation (rules about who can send what when), might be a step in the right direction.

@dsainati1
Copy link
Contributor Author

@katelynsills thank you for the detailed comment! I hadn't considered the potential for spam and abuse when designing the API, so I appreciate that you pointed it out here. Based on my understanding of your analysis (and please correct me if I am wrong here!), the primary issue with the API currently is that it allows a Provider to send something to any Recipient, without the Recipient's explicit consent.

One potential option would be to reverse the responsibility relationship for who initiates the connection: instead of publish and claim, where the Provider initiates by publishing a value and the Recipient chooses to claim it later, we could perhaps have something like request and provide/fulfill where a Recipient requests a value of a specific type from a Provider, and the Provider later fulfills that request by sending the value to the Recipient. However this may still have the potential for spam, just in the other direction, with a Recipient spamming a provider with spurious requests.

Another solution to this would be the inverse of the block method you described; a permit or allow method that a potential Recipient can call to add a potential Provider to their allowlist. This way a Provider can only publish a value to a Recipient that has explicitly opted to allow them to do so, and the Recipient can revoke this permission at any time via a method that would remove the Provider from this allowlist. We could extend this further by allowing the Recipient to specify a type argument that specifies exactly what types of values they wish to receive from the Provider they are allowlisting, and potentially even an argument specifying how many values they wish to receive before automatically closing the connection.

This seems like a limited version of the private inbox paradigm you are suggesting, that also addresses the additional complications you mentioned: because the Recipient must still explicitly claim things that the Provider publishes to them, it maintains the asynchronous nature of the connection and also requires the Provider to pay for storage until such time as the Recipient consents to receive the value they are being sent.

Let me know your thoughts on this! It's very possible I have missed some more cases here.

@turbolent
Copy link
Member

@bluesign

Maybe allowing claim to both accounts (sender and receiver) can be good.

Do you mean that the sender is able to claim on behalf of the receiver, so they could unpublish the capability, e.g. to clean up?

Some kind of seeing what I published, when I published can be good.

Agreed! Some form of on-chain introspection would be great. We already have it for some on-chain data (e.g. storage iteration) and are currently adding it for other data (e.g. account keys) where it is missing.

Also fun claim<T: Any>(_ name: String, provider: Address): T? is there a reason this is not something like: getAccount(provider).claim<T:Any>(_ name: String)

I think it will be more familiar if we somehow use getAccount.

The claim function needs to be defined on AuthAccount, not PublicAccount, so that the claim function can verify the identity. If the claim function would be defined on PublicAccount, anyone could claim the capability, as anyone can call getAccount to get the PublicAccount.

@bluesign
Copy link
Contributor

Do you mean that the sender is able to claim on behalf of the receiver, so they could unpublish the capability, e.g. to clean up?

yeah but @dsainati1 already added unpublish, though I was just gonna comment on that I think recipient should be in that too. ( if I will send 100 people FLOAT for example, I cannot name them all unique probably )

@turbolent
Copy link
Member

@katelynsills

Good point! At first I assumed that such a system, given that it is a mailbox, would be similar to e.g. snail and electronic mail, which are opt-in by default. However, given that this FLIP proposes a system for a very specific and limited purpose, bootstrapping capabilities, requiring explicit permission from the receiver for the sender to publish something seems reasonable 👍

@turbolent
Copy link
Member

@bluesign

Do you mean that the sender is able to claim on behalf of the receiver, so they could unpublish the capability, e.g. to clean up?

yeah but @dsainati1 already added unpublish, though I was just gonna comment on that I think recipient should be in that too. ( if I will send 100 people FLOAT for example, I cannot name them all unique probably )

Ah, I didn't saw that yet. An unpublish function seems functionally equivalent to allowing claiming by the sender, but it seems slightly more explicit.

@dsainati1
Copy link
Contributor Author

if I will send 100 people FLOAT for example, I cannot name them all unique probably

I don't think this is the intended use case for this feature; if you want to send people tokens, for example, we'd much rather encourage you to use the existing Vault capabilities. By requiring people to give unique names to values they send through this API we can encourage it to be limited to distinct, personal exchanges rather than being a venue for broadcasting many identical values to many people.

@katelynsills
Copy link

But this is the whole point of publish/claim, so far: "one-on-one easy transfer with verified identity"

I've been assuming we were trying to solve the bootstrapping problem that Dete outlined. The user problem is: you want to give a capability to someone else, and you don't want to give anyone other than the Recipient access to the capability. You have no prior connection to the Recipient. The current solutions have major flaws:

  1. A co-signed transaction requires both signers to be online around the same time.
  2. A dropbox-like approach would let someone other than the Recipient get access.

Here's how Dete described the scenario:

I have a project that requires some amount of human oversight, maybe something as simple as a "pause" button that lets me block activity in the smart contract temporarily in response to a bug or market panic. As such, I have an administrator capability in my account that gives me access to call the "pause" function if necessary. In time, Bjarte joins my team, and I decide that it'd be useful if his account could also call the "pause" function in case I'm sleeping when the emergency happens. I'd like to send him a Capability for the same Admin object that I have. How do we get the Capability into his account securely?

The reason why Receivers don't work here is that they themselves fall prey to the bootstrapping problem. How does Bjarte give the receiver (a capability) specifically to Dete, and not anyone else? It's the same problem.

This FLIP really does a great job at solving this hard problem, in a way that doesn't get developers used to depending on msg.sender.

@bluesign
Copy link
Contributor

bluesign commented Sep 16, 2022

@katelynsills I think I explained this above like this: ( this is what @bjartek refers as Capability Receiver pattern )
https://developers.flow.com/cadence/design-patterns#capability-receiver. (anchor link is broken, need to search on page capability receiver )

this is already possible with:
0x2: set up account, put some resourceReceiver resource with function saveCapability(Capability)
0x2: set whitelist address: [0x1]
0x1: call saveCapability(Capability) with your capability (cap) ( 0x2: check if cap.address is in whitelist )

Problem with permit is, it is no different that capability receiver. You need account set up (permitted) before publishing.

Even Capability Receiver has benefit, you can decide if they can copy the capability or not.

@katelynsills
Copy link

katelynsills commented Sep 16, 2022

@katelynsills I think I explained this above like this: ( this is what @bjartek refers as Capability Receiver pattern )

Ah, good point @bluesign, thanks for the reminder!

Problem with permit is, it is no different that capability receiver. You need account set up (permitted) before publishing.

But there is an important difference, right? We don't want smart contract developers to be writing code or using code at the smart contract level that is gated on identity. That doesn't allow delegation, and it will lead developers to not understand what capabilities can do that ACLs can't.

If it's understood that permit/publish/claim is a platform functionality only for the initial bootstrapping, then we dodge that bullet. With this FLIP no smart contract developer is using msg.sender patterns in their code.

@bluesign
Copy link
Contributor

bluesign commented Sep 16, 2022

But there is an important difference, right? We don't want smart contract developers to be writing code or using code at the smart contract level that is gated on identity.

Yeah but if we limit this too much and not provide a benefit, people will continue using capability receiver and implement their ACL on top of that.

We need to beat capability receiver pattern, to get usage and acceptance from developers. If we give something super restricted like this, I am afraid they will not use basically.

If I have to permit things first I could as well set up a CapabilityReceiver.

Some analogy, we want people to drive slow, and then offering them slower car with worse interior.

@katelynsills
Copy link

Some analogy, we want people to drive slow, and then offering them slower car with worse interior.

@bluesign, that's a really good reminder that just saying "no" to developers isn't a good strategy. I'd be interested in your thoughts on how to engage other developers in seeing the really cool things that capabilities & safe, permissionless delegation can do. This FLIP seems like the wrong place for it, but that kind of discussion seems very valuable. I think allowing capabilities to have behaviors themselves and expanding beyond the facet pattern would help, but that's also outside the scope of this FLIP.

To continue your analogy, I think we should enforce a speed limit when we know it's a dead-end and there's a brick wall ahead (otherwise all our drivers are going to die in a fiery crash), but we also should tell the drivers that there's a high speed train right nearby that will let them go faster than any car would. And if the high speed train isn't built yet, we need to work on getting there :)

@janezpodhostnik
Copy link
Contributor

How do we currently do bootstrapping? We use Capability Receivers.

A short rundown of that (with S as the Sender a.k.a.: the flow account that wants to send/give a capability, and R the Receiver the flow account that is to receive this capability):

  1. off-chain: S notifies R that it wants to send a capability to it (with a specific type!)
  2. on-chain: R creates a capability receiver for that capability and exposes it publicly. R has to be careful with his implementation of the capability receiver since it is expose publicly and anyone can send a capability (potentially eating up space or confusing R on which capability is the right one)
  3. off-chain: S becomes aware that R has created a receiver. This could happen by:
    • R telling S off chain
    • R triggering a designated on chain event
    • S pooling R for the existence of the receiver
  4. on-chain: S sends the capability to R
  5. on-chain: cleanup:
    • R removes the receiver

What are the problems:

  • This is a lot of steps
  • Implementing a receiver correctly is not a given (and could be easily attacked if not correct). R might not know anything about receivers and how to correctly implemented and will just grab a recipe from somewhere, or worse, trust S to provide code on how to implement the receiver.
  • S needs feedback on R being ready before it can proceed

What I think the solution is:

The original proposal had just AuthAccount::Publish(value, name, account) and AuthAccount::Claim(name, address): value (and of course you need unpublish as well), let me talk about that for a bit (and lets assume the value is a capability only).

These functions change the bootstrapping pattern to:

  1. on-chain: S publishes a capability for R (lets say by default no events get emitted)
  2. off-chain: S notifies R that it published a capability
  3. on-chain: when R has time. R claims capability

Much simpler!

  • by step 2. S is done
  • before step 3. R doesn't have to do anything.
  • S doesn't have to clean up, no-one else can claim the capability besides R (even if claiming wouldn't automatically un-publish it). S can clean up to save space (or if S has changed its mind about sharing at which point it should also un-link the capability just to be safe)
  • R does not have to write any custom code, it only has to store the capability somewhere, so It can use it later.

At this point there were questions about spamming and weather or not to send a common event when something is published. Can R be spammed, by constantly publishing for R?

I don't think so.
It's not like there is a notification popping up (on-chain) for R every-time someone publishes something for R.
If S wants to let R know it published something S has to do that off-chain (and let the off-chain channels like gmail and slack deal with any spam 😃 ).
This is also the reason why there should not be a common chain event triggered when publishing. If there would be, people could rely on listening to them, at which point they could be spammed.
If S really wanted to it could still emit a (custom) event when publishing (maybe by wrapping the publish action in a smart contract function that also emits an event).

So in short I think something like this would be sufficient (I probably got the generic notation wrong) (these are all on AuthAccount):

fun publish<T: Any>(capability: Capability<T>, name: String, recipient: Address) // does not emit an event
fun unpublish(name: String, recipient: Address) 
fun claimFrom<T: Any>(name: String, provider: Address): Capability<T>? // does not un-publish

(I don't see the argument for needing to permit before something can be published

(ab)use for other things than bootstrapping

I don't think this is a very efficient way of sending things, especially if we limit this to only capabilities. The reason is that S must notify R off-chain that something is available to be picked up. If S and R intend to exchange a lot more stuff (than just one capability) maybe instead of going through bootstrapping each time, they should instead bootstrap a (pull) channel where S gives the capability to pull from the channel to R and for further sharing S just pushes more stuff to the channel.

How that channel looks like is up to S and R, but I would imagine that S would want to emit an on-chain event that R knows about when adding to the channel.

Some other (random) thoughts:

  • instead of publish/claim, wrap/unwrap could be used, where wrapping a capability into a WrappedCapability<T> makes it so that only the specified account can then unwrap it back into Capability<T>. This allows for the same pattern (of bootstrapping) but S had to provide its own way to serve WrappedCapability<T>s instead of publish providing this. On the other hand it might be more flexible, and allow for more patterns.
  • instead of publish/claim, we could use identity as in S can provide something to someone who can provide the identity of R (which is only R) but the problem with this is that the identity object would need to be sent into a function, and once it goes into a function S could capture it and pretend it is R.
  • If S knows for certain that R and only R can pick up the published capability, S can wrap the capability with a wrapper that basically says that "if anyone calls this R is to blame"

@katelynsills
Copy link

If we decide for sure that no events are emitted, and the potential spam doesn't clog up block explorer displays, it makes sense to me to leave out permit as a step 👍

@bjartek
Copy link
Contributor

bjartek commented Sep 20, 2022

This is also the reason why there should not be a common chain event triggered when publishing. If there would be, people could rely on listening to them, at which point they could be spammed.
If S really wanted to it could still emit a (custom) event when publishing (maybe by wrapping the publish action in a smart contract function that also emits an event).

I do not agree on this. I think a shared event should be emitted. Why is this "spamming" a problem here? Or is this something you claim? The benefits of emiting a shared event is numerous!

In .find we could listen to 1 event to know if a user has anything published to them, and then prompt them to accept it, or flow port could do the same.

Shared events also signal that an mutation has happend on chain and that is a documented best practise. We should use more shared events, not less. Visbility is important.

An alternative to a shared event that I could live with is if it was possible to ask onChain if I have something I can claim.

@pgebheim
Copy link
Contributor

If we decide for sure that no events are emitted, and the potential spam doesn't clog up block explorer displays, it makes sense to me to leave out permit as a step 👍

I feel significantly less concerned about this capability spam than you seem to be.

This is not equivilent to token spam in ethereum because it doesn't create the same attack surface where a user further gives approval to their whole account to a malicious contract. Critically, the attack that's most concerning is actually having a token in your possession that looks valuable but is malicious.

If massive spam is a problem then it would mean that spammers would just be making themselves less visible, since there would be lots of noise in block explorers.

Even on networks with low fees, I have public accounts that have received some token spam but it's not THAT high. In this case the EV of the attack is so low that id be willing to ignore it so long as there's significant dev advantage to having a standard event.

@dsainati1
Copy link
Contributor Author

Even if we decide to not emit events, I still think allowing an open connection between any two accounts by means of this API does still pose an issue that @katelynsills mentioned earlier:

For instance, imagine a motivated and coordinated harassment attack, where the Recipient keeps getting notifications where the string name is used to send threatening and abusive messages. Thus, there needs to be an easy way to revoke this connection between accounts.

If we don't want to require an initial permit step here in order to keep the happy path simple, there does still need to be some kind of block functionality or similar so that users do not have to be connected to abusive accounts if they do not wish to be.

@pgebheim
Copy link
Contributor

Does this actually need to be a protocol level concern?

Seems this type of blocking would be an application feature.

@janezpodhostnik
Copy link
Contributor

The accounts are not connected.
The Sender needs to notify the Recipient off-chain.

The only possible on chain connection they have, that would allow for abusive messaging is the common event emitted during publishing.

I imagine that event would contain:

  • a source address (Senders address)
  • a destination address (Receivers address)
  • the name of the provided value/capability
  • possibly the type provided

If this event would exists I imagine people would want to listen to anything that was published for them, which opens them to receive spam (with a custom message in the name field).
The workaround would be just listening to list of specific sources, but in this case I argue that its better to not have a common event and just let every sender define which event they are going to emit. If need be people can still opt to create a standard to follow where they would emit a certain event defined in a common contract somewhere.

@bjartek @pgebheim I still feel that there should not be a common (cadence-level) event emitted when publishing. But this is the only argument I have.

I can perhaps suggest that we add the feature without the common event and open a separate discussion/PR for the common event.

@bjartek
Copy link
Contributor

bjartek commented Sep 20, 2022

If this event would exists I imagine people would want to listen to anything that was published for them, which opens them to receive spam (with a custom message in the name field).

In my eyes this is a non-issue. There are numerous other ways a user can be spammed today. How many active users do not have a FLOAT account? A float user can be airdropped any NFT per the NFT standard.

In Lost-And-Found you can add any resource or an NFT and send to a user and add a message.

I have several more use cases where on chain "spam" is inevitable.

We need a common event or a way to onChain know if a user has something to claim for them.

@katelynsills
Copy link

For what it's worth, here's a real life example of the kind of abuse and harassment that I'm concerned about. Specifically, it's the name field that would be used to transmit a hateful message. An attack like this would prevent the recipient from being able to be use Flow at all.

One option that I haven't seen discussed yet is to keep the default open (anyone can publish to anyone, and every publish emits an event) but allow users to turn on an "Advanced Protection" mode in which they only accept published capabilities from accounts that they explicitly permit access. Seems like that could be the best of both worlds in terms of usability and security.

@bjartek
Copy link
Contributor

bjartek commented Sep 26, 2022

@katelynsills that is a really good example, and my point is that doing that is very easy already. And doing so in tools that is way easier available then this solution that is something that most likely only very technically savvy users/developers will user either way.

@dsainati1
Copy link
Contributor Author

my point is that doing that is very easy already

I am not convinced that this is a particularly compelling argument; just because a bad practice is commonplace already does not excuse us as designers if we contribute to the problem by adding another vector for harassment.

@dsainati1
Copy link
Contributor Author

We had a public meeting about this FLIP today (Sept 26) and we settled on a few specific steps forward here:

  1. Spam and harassment via events or messages is the responsibility of the user actor (e.g. the wallet) to manage, rather than the protocol, so we will remove the allowlist, permit and unpermit sections of this proposal in favor of these things being handled off-chain. We will likely suggest to wallet or other user actor developers that they do not display publishing events as-is to users, and allow users to restrict who they see events from.

  2. publish, claim, unpublish should all emit an event to signify that a capability has been published or removed, containing the name, type, and receiver (in the case of publish) and the name and removing account (in the case of claim and unpublish). This will give accounts the information they need to claim resources when they are published for them.

  3. For now, the API will continue to be restricted to capabilities. In the future we may decide to expand this to resources, but this would not be a breaking change (whereas going from resources now to capabilities in the future would be).

  4. claim should remove the capability being claimed from the publisher's dictionary. While capabilities can be duplicated, so it would not be semantically wrong to leave a copy present, leaving the capability in place would require manual cleanup on the part of the publisher that is not desirable. It would also encourage a pattern where receivers would repeatedly claim capabilities stored on the publisher's account whenever they need them, in order to avoid paying for the storage for that capability themselves.

Copy link
Member

@turbolent turbolent left a comment

Choose a reason for hiding this comment

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

LGTM! Just a couple questions regarding naming (minor)

flips/20220908-publish-claim-capability.md Outdated Show resolved Hide resolved
flips/20220908-publish-claim-capability.md Show resolved Hide resolved
Co-authored-by: Bastian Müller <bastian@axiomzen.co>
Copy link
Member

@turbolent turbolent left a comment

Choose a reason for hiding this comment

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

Nice!

@dsainati1 dsainati1 merged commit f4d9745 into master Sep 30, 2022
@peterargue peterargue deleted the publish-claim-flip branch January 17, 2023 22:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Cadence FLIP Flow Improvement Proposal
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants