Closed evgenykuzyakov closed 5 years ago
Before we implement this, let's think if there is a simpler solution. For example, adding a field to TX "app" signed with key of which can have different logic (charge the app, on account allow "app" to do stuff, etc).
@evgenykuzyakov I suggest two options:
enum AccessKey {
FullAccess(PublicKey), // this key has full access
MultiSig(Vec<PublicKey>, u32), // only if x out of total keys have signed
DeployKey(PublicKey), // only can deploy
AppKey(PublicKey, AccountId), // only can use app with `AccountId`
// other options.
}
struct Account {
amount: u256,
access_keys: Vec
2. Go directly to contract-based accounts, where we just have a function `check` which has enough gas for max 5 signature verifications + a bit of logic. This function is getting called for each transaction, when we accept it. It also can work as permissioning for what this transaction can do (deploy vs spend money, etc)
What do you think?
Option 2 sounds good if the account runs shared code. I'd still prefer if we charge gas for the validation and we need to figure out how to charge gas for large code size preparation. Currently we assume that instance spawn is free and don't cost any gas.
We'd also have to move public keys out of Account structure, since deserialization of all public keys would be expensive. WASM code should know which public key was used and we can do this by passing it as an argument.
We'd also have to either expose some more data to WASM through read api or serialize the TX info into some format. It would be really hard to replace that standard format in the future. Which brings a question about JSON vs protobuf again.
Problems (for option 2):
Maybe we can have merged approach:
You have 2 types of keys:
account_id:public_key
. They should be stored on the same shard as the owner's account. Owner can modify all access_keys on their accountsstruct AccessKey {
/// Owner's account_id
owner_id: AccountId,
/// signature required for this key
public_key: PublicKey,
/// amount of funding on this key to pay for gas
amount: u256,
/// owner of the funding on this key, e.g. if app wants to pay for the user. So user can't steal funds
funding_owner: AccountId,
/// account_id of the contract towards which this access key can be used.
/// It's also can be owner's account_id and be used for rate limiting and proxying.
contract_id: AccountId,
}
We can limit the number of public keys an account can have. Also @ilblackdragon said that we should charge gas for verification but have a fixed limit of how much gas you can use there to discourage people from writing their own functions.
We can limit the number of public keys an account can have.
You don't want to limit app specific keys since there are can be 100s of apps enabled at once, like on mobile
Let's go through problems:
App may spam with valid transactions and drain account from gas (since it doesn't require a user to be online)
With access_key app can only send as many transaction as funding allows. Unless the developer pays for this, then it's fine.
Should we charge for verification to prevent invalid txs spam attack? If so, should we charge account or the specific key? (We already have AccountingInfo that can do proper accounting)
Yep, we're charging access_key for gas. Public key would be verified similar to standard account verification, no overhead. If the access key points to the owners account, then owners account would just proxy the call (similar to option 2), but we don't limit the runtime with specific API on how to approve transactions. There are also no additional hops needed (as in my original proposal). But we need to expose public key of the signature to WASM. It's much easier to do, just one more data_type within read. AccessKeys could be auto-refunded daily to support some type of protection and rate limiting.
Too many keys on a single account would slow down account deserialization. Needs to refactor public keys out of the account.
No need to refactor public keys out, but need to support access key TX verification. In theory we can move everything to access keys if we want to.
Some standard on the function api to pass info for verification. It would be hard to change this in the future, since both runtime, wallet and wasm code has to be aware of the version.
No need to have a standard for this, but need to expose public key to have proper proxy support when you call your own account using access_key.
I don't really have much too add except: 1) I prefer hardcoded to contract-based, cause it's likely easier to make it happen and optimize it to work fast. 2) As this gonna end up changing the way keys are stored – let's make sure we aren't limited to ed25519 signatures. I think we should at least support Ethereum-style signatures + whatever Apple supports in their hardware. That would greatly simplify wallet integrations. Basically Ethereum-style signature support means we can integrate with whatever Web3 provider. Fortmatic, Portis, Metamask, whatever.
AccessKeys
can be stored under account_id,public_key
to make lookup cheap.
Right now we actually need to iterate over all public keys on the account to validate signature will all of them. Alternatively tx has public_key
that is used for signature and then it's just one lookup.
One side note, is that if application needs to refresh keys, it will need to update all of the AccessKeys
for all applications. @evgenykuzyakov here points that app should not store it on the server side, but this works not for all use cases.
High level, access keys are allowing to prepay for specific key to do more custom validation. @evgenykuzyakov suggests it can execute custom code from user account for additional validation.
E.g. if we want to have vesting on the account, we can implement it:
"illia.near": {amount: 100, public_keys: [], code: vesting contract}
"illia.near,primary_key": {amount: 10, function: "has_vested", funding_owner: "", contract_id: ""}
Additional question is that we need to top of the fees on the access keys.
One question I still have, that given we are currently checking signatures on the account, how much slower would that be if the same thing was done in WASM?
To previous note - don't need to store sender's public key in transaction, because there is a way to recover public key from signature + hash (may be only for Eth sigs like SECP256K1 though)
I don't think we need to restrict function
that can be called, even though it's an interesting idea and we probably should do this.
I was thinking you'd create a proxy function on your account that either creates a async call or dies. Here is the pseudo code:
let accessParams = collections.map<string, AccessParams>("ap");
function proxyTx(tx: Transaction): void {
// Create ContractPromise based on the tx and returns it.
}
function assertAccess(tx: Transaction, ap: AccessParams): void {
// Verifies parameters of transaction and ap
}
export function proxy(tx: Transaction): void {
// Called by our account (through access key probably)
assert(context.sender == context.contract_id);
// Identify access key
const pk = context.public_key;
// Check PK access params
let ap = accessParams.get(pk);
assert(ap != null);
assertAccess(tx, ap);
proxyTx(tx);
}
You'd also have a function to modify access params on your account that would be called from the wallet to issue access params and control apps.
@evgenykuzyakov So the last part with the proxy on the account via access key already works or not yet?
For proxy call, we need to expose public_key to context.
@evgenykuzyakov is this done now?
To support mobile apps on NEAR, we need to implement app-specific keys in some form or another. Mobile apps don't have a good way to seamlessly communicating with the wallet to sign transactions without exposing them to the app (especially on iOS). You either have to use server-side communication, or you need to bring the wallet to the foreground.
One way of doing it is to create an app-specific account and then give keys to the app. Here is how it works: 0) Your account is
account_id
. 1) You create a new account for the app. Let's saynew_acc_id
. 2) You lend/lock tokens on thenew_acc_id
to use your accountaccount_id
. Nownew_acc_id
can only send transactions to your original account. The amount defines the limit on how many transactions it can send. 2) You authorize your account's contract to proxy transactions fromnew_acc_id
with restrictions, e.g., non-monetary and only to specific contracts. You can also add additional GAS/MANA to the transaction. In this case, you can minimize the amount of tokens you need to lendnew_acc_id
. (This change requires a new API to WASM and work on accounting, we need to discuss as part of economy model). 3) You pass keys for thenew_acc_id
to the app (maybe encrypting them first with the app's public key to avoid leaking on the way). 4) App uses the keys and proxies transactions through your real account.