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

Exposing Function Call Access Key Info At Runtime #371

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
149 changes: 149 additions & 0 deletions neps/nep-0371.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
---
NEP: 371
Title: Runtime Function Call Access Key Info
Author: Ben Kurrek <ben.kurrek@near.org>, Jakob Meier <jakob@near.org>
DiscussionsTo: https://gov.near.org/t/expose-gas-price-and-contract-access-key-info-within-runtime/24788
Status: Draft
Type: Standards Track
Category: Review
Created: 13-Jul-2022
---

## Summary

There is currently no way to query for information about access keys stored on a contract at runtime. Adding the host function `access_key_info` to the runtime specification will allow to query this for the current account an easy way. This fully covers the most important use cases that are currently known for querying access keys at runtime.

## Motivation

As contracts start to use access keys in more creative ways, developers will want to know information such as the access key's allowance, receiver and more. Since contracts are often built for scalability, Gas and storage costs are often delegated to the end user. If you charge the user for an access key and its allowance, when the key is deleted or used up, that allowance should be refunded to the purchaser. Currently, there is no way to query for the leftover allowance before a key is deleted. This information can tell you how much Gas was spent, and allows for a slew of different scenarios to arise.
akhi3030 marked this conversation as resolved.
Show resolved Hide resolved

## Rationale and alternatives

Without creating a promise, we can only query for information stored on the same shard. This is why in this proposal, the access key information at runtime will only come from keys stored on the current contract. We had also investigated the possibility of introducing pagination for all keys stored on the contract but this is infeasible at the moment due to the significant redesign requirement of the VM logic.

## Runtime specification

The contract runtime provides one host function called `access_key_info` which is callable in regular calls and views.

```rust
fn access_key_info (
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

public_key_len: u64, // in: number of bytes of the public key to query
public_key_ptr: u64, // in: pointer to the public key to query
nonce_ptr: u64 // out: pointer to where a u64 can be written by the host
allowance_ptr: u64, // out: pointer to where a u128 can be written by the host
account_id_register_id: u64, // out: register to write the access key holder account id
method_names_register_id: u64, // out: register to write the method names as a comma separated list
akhi3030 marked this conversation as resolved.
Show resolved Hide resolved
)
-> u64;
```

### Return values

- Returns 0 if the given public key is not registered as access key to the current account. In this case, the memory locations behind `nonce_ptr` and `allowance_ptr` are not changed by the host. The register content of `account_id_register_id` and `method_names_register_id` are not changed, either.
- Returns 1 if the given public key is a full access key to the current account. The memory location at `nonce_ptr` contains the current nonce value of the access key. The memory location at `allowance_ptr` is not changed by the host. The register content of `account_id_register_id` and `method_names_register_id` are not changed, either.
- Returns 2 if the given public key is a function call access key to the current account. The memory location at `nonce_ptr` contains the current nonce value of the access key. The memory location at `allowance_ptr` contains a `u128` value that is the remaining allowance in yoctoNEAR. The register `account_id_register_id` contains the account name of the access key holder. The register `method_names_register_id` contains a comma separated list of all method names that the access key is valid for.

### Panics

- `InvalidPublicKey`: Calling `access_key_info` with an invalid public key.
akhi3030 marked this conversation as resolved.
Show resolved Hide resolved
- `MemoryAccessViolation`: Any of the following ranges are not fully inside the guest memory.
- [public_key_ptr, public_key_ptr + public_key_len)
- [nonce_ptr, nonce_ptr + 8)
- [allowance_ptr, allowance_ptr + 16)
- `InvalidRegisterId`: `account_id_register_id` and `method_names_register_id` are equal

### Gas cost

No new gas cost parameter needs to be created. The workload put upon the runtime is almost exactly the same as `storage_read`, with slight changes to how the key is constructed and how values are returned

The gas cost for accessing is the base host-function cost, storage read costs including trie node costs, and writing to guest memory cost. The exact list is given here.

- `wasm_base` *-- host-function call base cost*
- `wasm_storage_read_base`
- `wasm_storage_read_key_byte` * `(2 + public_key_len + number of bytes in current account id)`
- `wasm_storage_read_value_byte` * `(number of bytes in method_names + number of bytes in returned account id)`
- `wasm_touching_trie_node` * `(number of accessed trie nodes that were no in chunk cache)`
- `wasm_read_cached_trie_node` * `(number of accessed trie nodes that were in chunk cache)`
- `wasm_write_memory_base` *-- for writing nonce to guest memory*
- 8 * `wasm_write_memory_byte` *-- for writing nonce to guest memory*

Additional costs that only occur if the result is a function access key:
- `wasm_write_memory_base` *-- for writing allowance*
- 16 * `wasm_write_memory_byte` *-- for writing allowance*

### Rationale behind runtime specification choices

The parameter values are mostly analog to `promise_batch_action_add_key_with_function_call` but only the public key is an input parameter. All other parameters are used to define output locations.

The output for nonce and allowance is of a small and fixed size, so a pointer to guest memory works well.

The Receiver ID can be between 2 and 64 bytes, based on the limits of account id length. It is also thinkable that this range changes in the future. Using a register to return the location is the most flexible solution to make the host-function signature agnostic to this dynamic size.

The method names list can be between 0 and 2000 bytes, based on `max_number_bytes_method_names` runtime parameter. Again, it is thinkable that this range changes in the future. The register is again the right choice here.

## User perspective

In the runtime, access key information will be queryable via a function by passing in the desired public key. The key information will be of the following form.

```js
enum KeyPermission {
FullAccess,
FunctionCall(FunctionCall),
}

type KeyInformation = {
public_key: string,
access_key: {
nonce: number,
permission: KeyPermission
}
}

type FunctionCall = {
receiver_id: string,
allowance: string,
method_names: Array<string>,
}
```

This information should be queryable via a new function called `access_key_info_for_current_contract` which takes a public key as a parameter.

```ts
function access_key_info_for_current_contract(
public_key: &PublicKey,
): KeyInformation | null
```

This function should be exposed in the environment and callable using `env::access_key_info_for_current_contract();`. An example of this can be seen below.

```rs
pub fn check_key_exists(&self, pk: PublicKey) {
let key_info: FunctionCallKeyInfo = env::access_key_info_for_current_contract(&pk);

if let Some(info) = key_info {
near_sdk::log!("key info found");
} else {
near_sdk::log!("No public key found");
}
}
```

An example of returning the allowance of the key can be seen below.

```rs
pub fn check_key_allowance(&self, pk: PublicKey) {
let key_info: FunctionCallKeyInfo = env::access_key_info_for_current_contract(&pk).unwrap();

let allowance = fc.allowance;
near_sdk::log!("key allowance: {}", allowance);
}
```

## Future possibilities

Some of the future possibilities for the standard could be to return a vector of KeyInformation by paginating through the keys on the contract. In addition, we could add a `key_total_supply` function that returns the number of access keys stored on the contract. One could also expand and introduce the ability to query for access key info from other contracts by using a promise.

## Copyright
[copyright]: #copyright

Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).