diff --git a/neps/nep-0371.md b/neps/nep-0371.md new file mode 100644 index 000000000..a1e52ab3b --- /dev/null +++ b/neps/nep-0371.md @@ -0,0 +1,170 @@ +--- +NEP: 371 +Title: Runtime Function Call Access Key Info +Author: Ben Kurrek , Jakob Meier +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. + +## 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 ( + /// in: number of bytes of the public key to query OR u64::MAX + public_key_len: u64, + /// in: pointer OR register ID to the public key to query + public_key_ptr: u64, + /// out: pointer to where a u64 can be written by the host + nonce_ptr: u64 + /// 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, + /// out: register to write the method names as a comma separated list + method_names_register_id: u64, +) +-> u64; +``` + +Just like other host functions, such as `storage_read`, if `public_key_len` is set to `u64::MAX` it will treat the input as a register input instead of memory input. +That is, `public_key_ptr` will be used as a register id. + +### 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. +- `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) +- `MemoryAccessViolation`: If writing to the registers increases the total register memory beyond `registers_memory_limit`. +- `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 length + 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* + +If input public key is read from memory: +- `wasm_read_memory_base` +- `public_key_len` * `wasm_read_memory_byte` + +If input public key is read from register (`public_key_len` == `u64::MAX`): +- `wasm_read_register_base` +- (public key length) * `wasm_read_register_byte` + +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* +- 2 * `write_register_base` +- `wasm_write_register_byte` * `(length of returned account id)` +- `wasm_write_register_byte` * `(length of returned method names list)` + +### 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, +} +``` + +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/).