Expose GAS Price and Contract Access Key Info Within Runtime

Over the past week, I spent a substantial amount of development time on an upcoming contract to be launched at NEARCon. During this process, I ran into a slew of problems that led to a pretty bad developer experience.

The Problem

Currently, there is no way to query for the GAS price at runtime and there is also no way to query for data related to access keys belonging to your contract.

I understand that you cannot query for access key information for keys that belong to an arbitrary contract since that info might be stored in another shard. If the keys belong to your contract, I would imagine that this information could be queried at runtime (correct me if I’m wrong).

Specifically, I propose that the the following struct is queryable for keys belonging to the current contract account.

type KeyInformation = {
    public_key: string,
    access_key: {
        nonce: number,
        permission: FunctionCall | string // string being 'FullAccess' if the key is a full access key
    }
}

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

3 functions should be exposed (the naming is subject to change):

  • env::keys_for_current_account(from_index: Option<string>, limit: Option<number>) -> Vector<KeyInformation> which paginates through the keys belonging to the current account ID (contract).
  • env::key_supply_for_current_account() -> number which returns the total number of keys belonging to the current account ID.
  • env::key_info_by_public_key_for_current_account(public_key: PublicKey) -> Option<KeyInformation> which returns the key information for the specific public key passed in

In addition, as NEAR grows, the blocks might get congested and smart contracts might want to know the GAS price and act accordingly. I think adding a function to get the current GAS price at runtime is important as well.

  • env::gas_price which returns how much yoctoNEAR it costs to purchase 1 GAS.

Use Case

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.

5 Likes

Probably should add reciever_id to the FunctionCall struct that’s returned, so you can tell what accounts each access key can call methods on.

For the pagination args env::keys_for_current_account(from_index: Option<string>, limit: Option<number> I think it’s fine to assume u128 as from and limit types bc. the Rust SDK/any other lang should compile this correctly to wasm.

2 Likes

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.
On the 10th call, the key is deleted and any unused allowance from that key is returned to Alice.

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.

2 Likes

I think both additions should go through the NEP process. env::gas_price seems easy implementation wise and can be implemented quickly.

The key stuff contains two unfortunate cans of worms:

  • first, we’d have to spec borsh serialization into our VM. Today, all host functions return relatively simple objects (ints, strings, arrays). For access keys, we need to return complex objects.
  • second, we currently don’t really have ability to iterate access keys. We have a hack in place to do that for TrieViewer::view_access_keys, but all the “real” API only ever access a specific access key via public key. So, while env::key_info_by_public_key_for_current_account would be straightforward to add I think, iteration would need a somewhat significant change to our model. Not saying that it’s undoable, but it’s more design work. Would just key_info_by_public_key be enough?
3 Likes

For my specific use case, simply adding the key info by public key would work. I tried to keep it open in case anybody else needed to paginate through keys. We can always simplify in the first iteration and then later down the line, if anybody needs the iteration, we can brainstorm next steps.

1 Like

+1, on board with env::key_info_by_public_key_for_current_account and env::gas_price implementations.

5 Likes

I agree with the above proposal

1 Like

I’m unaware of the implementation costs but I’d be quite happy with env::key_info_by_public_key_for_current_account returning some kind of map, its iterability is something I could implement if I wished to do so.

very happy about env::gas_price too

@idea404 The problem of implementing it is mostly on the ABI layer. As @matklad mentioned, we currently only deal with numbers and pointers to raw bytes or strings on the cross-over point between the runtime and the contract code / SDK. The SDK gives you a wrapper to make it nicer but on the runtime level, host function signatures look like those defined here: near-sdk-rs/lib.rs at master · near/near-sdk-rs · GitHub

So, if we want to return a map, we need to define an encoding for it. There is currently no precedence for this in the NEAR contract runtime specification AFAIK. And considering that anything added to the runtime specification will probably stay there for eternity, this indeed opens a can of worms. Using borsh as the encoding is the obvious choice but it feels like that would still require its own NEP and thorough discussion.

The latest suggestion I discussed with @Benji was to simplify it even more, actually. Instead of key_info_by_public_key_for_current_account that returns a full access key or a function call access key, we could have just function_call_access_key_info that only works for function call access keys. That means you could not look up a full access key. But is there any use case for that? Only the existence of it and maybe the nonce can be learned from it, as full access keys don’t have allowance.

Unless someone has a real use case for more right now, the NEP (which will be ready soon) will contain only single function access key lookups, no full access keys and no iteration.

1 Like

@Benji Upon writing the runtime specification for this, I figured out that it makes very little sense to not include full access key information, from an implementation perspective. Sorry for the back-and-forth on this. :sweat_smile:

Querying for a function access key or a full access key is the exact same trie lookup. Therefore, when querying a function call access key, the host will know exactly whether it is a function call access key, a full access key, or if it does not exist at all. Not returning this information to the guest seems silly.

For the name on the runtime level, I suggest to use something short like access_key_info. The fact that it is for “current account” is somewhat implicit in the signature as it takes no account id to query inside.
If we ever add the ability to query other accounts, this will have to be a new action that goes through the promise API. Following the existing convention, this would be prefixed the name with “promise_”, so for example it could be promise_batch_action_read_access_key_info. So I don’t think we need to worry about name collisions or confusion here either.

I have no strong opinions how this should map to names of functions in the SDK. A longer, more explicit name might make sense there. But on that layer I am not familiar with conventions and usage patterns, so I really cannot tell.

One more nuance here.

When reading current_gas_price(), it will return the effective gas price of the block in which the function call is executing. The gas_price field on the receipt will be a different value, as that contains the pessimistic price that has been paid to purchase the gas originally.

Similarly, access_key_info will return the token allowance available at this very moment. After the function call finishes, this allowance increases by the refunded amount. (Assuming the queried key is signer_account_pk, otherwise the queried allowance is unrelated to the refund.)

If users care about this difference, it might also be necessary to add receipt_gas_price().

Hey Jakob, I think this is fine. For the case of keys, if you were to read the key during the execution, it would show it’s allowance deducted by the pessimistic amount and then if you were to delete the key, once the execution is finished, the contract’s balance would increase by the net (so the tokens wouldn’t be lost) but the allowance on the key wouldn’t increase since it’s deleted.

As for introducing receipt GAS price, as you had mentioned in slack, this would allow you to (almost exactly) calculate how much allowance you will have after the refund. If at the end of a function call, you calculate ( prepaid_gas() - gas_used() ) * ( receipt_gas_price() - current_gas_price() ) you will get almost exactly the amount that will be refunded, Assuming no more promises are produced at this point because receipt_gas_price will be the pessimistic price used.