-
Notifications
You must be signed in to change notification settings - Fork 138
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
Exposing Function Call Access Key Info At Runtime #371
base: master
Are you sure you want to change the base?
Conversation
Your Render PR Server URL is https://nomicon-pr-371.onrender.com. Follow its progress at https://dashboard.render.com/static/srv-cb7i5doivq021t6nb67g. |
The contract runtime provides one host function called `access_key_info` which is callable in regular calls and views. | ||
|
||
```rust | ||
fn access_key_info ( |
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.
So this API can only be used to access info about a known key? What if a contract wants to know the actual set of keys associated with it? Maybe there is already an API for that or is that not an interesting use case?
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 could interesting to paginate keys, we just haven't had anyone coming up with an actual use case, yet. :)
And because the API for that would be significantly trickier to implement, we opted it out, for now.
I expect most calls to either look up the current access key (signer_account_pk()
already exists for that) or otherwise take an argument from outside that specifies which access keys to check. Listing all keys through rpc REST API has been supported for a long time, so the signer side should always be able to provide the list if necessary.
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.
And because the API for that would be significantly trickier to implement
To expand on this, we don't really have a way to iterate keys in our runtime.
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.
OK, I think I understand this API now. Given a pk, it allows a contract to look up various details about it. How the contract got hold of the pk is outside of its scope. That makes sense.
Thinking out loud. If we expect a lot of use cases to be of the style: access_key_info(signer_account_pk(), ...)
, would it make sense to (in future) offer an optimising API here: access_key_for_signer()
. This will mean that we save two copies of the public key. But I suppose that I am premature optimising here. And we can always introduce this API in the future if we think it is warranted.
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's important to note that the PK that can be passed in MUST belong to the contract account. If benji signs a txn with an access key where the contract is the receive, this won't return any info. The signer's account must be the contract.
allowance_ptr: u64, | ||
/// out: register to write the access key holder account id | ||
account_id_register_id: u64, | ||
/// out: register to write the method names as a comma separated list |
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.
as a comma separated list
This is problematic: ,
is a valid character in a method name, so this API creates possibility for ambiguity.
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.
Oh wow, I didn't realise it is technically an allowed character in a WASM method name. Very glad you point this out.
But that's interesting. When adding a function access key from within a contract, then the encoding used is a comma separated list, so this makes it impossible to create such a method access key this way. But that's just an artificial limitation from the host function implementation. From near CLI, it is possible to list methods with a ,
in it. And there is also nothing preventing for a contract with such a method name to be deployed. So this is a real concern.
The contract runtime provides one host function called `access_key_info` which is callable in regular calls and views. | ||
|
||
```rust | ||
fn access_key_info ( |
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.
And because the API for that would be significantly trickier to implement
To expand on this, we don't really have a way to iterate keys in our runtime.
/// out: pointer to where a u128 can be written by the host | ||
allowance_ptr: u64, | ||
/// out: register to write the access key holder account id | ||
account_id_register_id: u64, |
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 wonder if we should guarantee that this'll be a valid AccountId? Today we have some keys where the receiver_id
is borked and isn't actually an AccountId
. We are fixing that at the runtime level: near/nearcore#7139.
More specifically, if this fn queries for a key whose receiver isn't an AccountId, we can:
- return receiver as a string
- or pretend that the key doesn't actually exist.
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.
Good point. My gut feeling here says, if the access key exists, correct or not, this function should be able to reveal this. Otherwise code that checks if a key already exists before adding a new key like this will be buggy:
//...
signer_account_pk(pk_register_id);
let result = access_key_info (
u64::MAX,
pk_register_id,
&mut nonce as *mut u64 as _,
&mut allowance as *mut u128 as _,
account_register_id,
method_register_id,
);
match result {
0 => {
// key does not exist, so deploying should be possible, right?
signer_account_id(signer_register_id);
promise_batch_action_add_key_with_full_access(promise_idx, u64::MAX, signer_register_id, 0);
// oops, above promise can fail with `AddKeyAlreadyExists` :(
},
1 | 2 => {
// key exists, need to delete it first
promise_batch_action_delete_key(promise_idx, u64::MAX, pk_register_id);
signer_account_id(signer_register_id);
// now the new key can be added
promise_batch_action_add_key_with_full_access(promise_idx, u64::MAX, signer_register_id, 0);
}
}
Therefore, I tend towards returning a string and making it clear that it may not necessarily reflect a valid account id.
We also return non-existing but valid receiver account ids. And the returned id would only be invalid if the user (or someone else with full access) has previously put the wrong id there. So this extra caveat seems like a non-issue from a user's perspective.
The issue with method_names seems significant to me. It seems like there's no way out of serializing variable number of variable-length keys. My gut feeling is that we should probably bite the bullet and just return borsh of the whole access key. |
@matklad I guess you are right. Do you think we can sneak it in as part of this NEP, making the precedence for borsh being used in the runtime API? Or should this go through a separate NEP with full discussion and getting everyone with a stake involved? I was hoping to avoid too much overhead here but it feels like adding borsh to the spec crosses a line that we should not rush over too quickly. |
How about pagination for method names then? One function call can return how many method names there are. And then another takes an index specifying which method name to return. To reduce multiple context switches, in future we could introduce an API similar to https://linux.die.net/man/2/writev where the contract can provide the runtime a list of pointers to get back a list of method names in a single call. But we can keep it simple for now and just require the contract to call the function once per method name. |
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.
Thanks for the proposal. I would like to understand the following:
- The motivation talks about making developer experience better. Could you provide a specific example of why this host function is needed?
- What happens if we change
AccessKey
in the future? (for example we implement feat(runtime): Add delegate keys #342) How does that affect this host function?
@bowenwang1996 I can speak to the first point. As stated in the governance post: For a real world example of why this would help, i’ll explain a use case. Imagine you have a contract that allows users to “purchase” function call access keys that can do different things. The user must pay for the access key allowance when the key is created. An example of this is the linkdrop contract. When the key is used up / deleted, dApps might want to refund users for any allowance that was unspent. Currently, there is no way to do this. In addition, when creating the key, often times, the dev will know how much GAS will be attached to the function calls and they might want to dynamically calculate the pessimistic allowance that the key must have and to do this, they need the current GAS price. In addition, @mattlockyer had shared his opinions in the post as well: An abstract but fundamental use case would be some form of meta transaction. I want Alice to fund an access key, that is added to a contract she doesn’t control, such that Bob can call a method 10 times. Currently, there is no way to check the unused allowance of the key from inside the contract runtime, AND there is no way to check the current gas price, so crude approximations have to be used that often overestimate how much gas was actually spent. Panics and reversions further complicate this scenario, since gas is used, but no state is changed. This forces the developer to never panic on an assertion, and instead write a crude approximate of used gas to state and return false, before exiting the method. If the protocol allowed us to see the allowance, method_names and receiver_id of access keys, there could be more use cases opened up. |
I see. So basically you want Alice to pay for Bob's transactions. There is a separate proposal on meta transactions, which could provide in my opinion a better solution to this problem. Are there any other examples or use cases for this feature? |
I believe in the proposal you mentioned, Alice still needs to execute the transaction. Using access keys, the way they were intended, someone else can hold the key and execute the transactions when they want. The nuance being the client in control of the key that's executing the transaction. This proposal allows developers to examine and deal with the function call parameters of access keys that may have been added to their contract and distributed to clients, so you can fine tune access control and measure the refund of allowances that go unused. |
@bowenwang1996 Valid question, I see two directions to deal with such cases.
I am torn between the two. The first option may lead to inconsistent API, as we add more and more functions over time. The second option tries predict the future, which usually doesn't work. To make it more tangible, the second option could roughly look like this: (Also incorporating @akhi3030 suggestion to paginate methods.)
I think this would allow to cleanly add all kinds of new access keys. But who knows what the future brings. |
Here is another user asking for this functionality this morning: |
Hey @BenKurrek – thank you for submitting this NEP. In order to move this NEP to the review stage, can you please add a reference implementation? |
Hi @BenKurrek @jakmeier (or anyone else) – are you still interested in moving this NEP forward? If so, can you please submit a reference implementation? |
I'm definitely interested in moving it forward but don't have time to put out a reference implementation at the moment. Perhaps I can revisit when I have some time. |
@ori-near can you please provide some links to other reference implementations e.g. a couple of closed PRs to nearcore that we can look at? I don't think this should be incredibly difficult. We know everyone would like to keep the API surface small for packages like near-sys, however getting access key information about the account your contract is currently executing on is fairly critical for some use cases. |
@mattlockyer here is the complete reference implementation for another host function: near/nearcore#7165 (some follow-up work was necessary but for a reference implementation this is more than needed) Regarding this feature here, I agree it is fairly basic and probably should be there. The main questions/concerns for me are:
But if you think this is a valuable addition to for smart contract devs and you can provide a reference implementation, then we can certainly move it forward. Me personally, I don't think I have the capacity to implement it in the near future. |
@mattlockyer @BenKurrek wanted to reping on this since progress on #452 moves forward |
Hello! Protocol NEP moderator here. it seems there hasn't been any activity on this NEP for quite some time. Is it still on going? Or shall we close NEP for now? @BenKurrek, @jakmeier |
Another ping on status of this NEP. As more than two months have passed since the last activity, if we don't hear back from the author by December 4th, we will mark the NEP as retracted for now. Thanks- |
I will assume this NEP is inactive and label as retracted for now. If anyone picks the NEP up again, please ping NEP moderators to make it active. |
This PR is based on the gov post found here that talks about introducing a way to query for function call access key information for the given contract at runtime.