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

NEP-480: Account Namespaces #480

Closed
wants to merge 11 commits into from
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Changes to the protocol specification and standards are called NEAR Enhancement
| [0399](https://github.com/near/NEPs/blob/master/neps/nep-0399.md) | Flat Storage | @Longarithm @mzhangmzz | Review |
| [0448](https://github.com/near/NEPs/blob/master/neps/nep-0448.md) | Zero-balance Accounts | @bowenwang1996 | Final |
| [0455](https://github.com/near/NEPs/blob/master/neps/nep-0455.md) | Parameter Compute Costs | @akashin @jakmeier | Final |
| [0480](https://github.com/near/NEPs/blob/master/neps/nep-0480.md) | Account Namespaces | @encody | Draft |

## Specification

Expand Down
143 changes: 143 additions & 0 deletions neps/nep-0480.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
---
NEP: 480
Title: Account namespaces
Authors: Jacob Lindahl <@encody>, Firat Sertgoz <@firatNEAR>
Status: Draft
DiscussionsTo: https://github.com/near/NEPs/pull/480
Type: Protocol
Version: 1.0.0
Created: 2023-05-12
LastUpdated: 2023-05-12
---

## Summary

This proposal introduces account namespaces, which enable deploying multiple WASM binaries to a single account. A WASM binary is deployed to a unique, deployer-selected namespace. Subsequent deployments to the same namespace overwrite the code currently deployed to that namespace. The empty namespace is considered the default namespace.

A WASM binary deployed to a non-default (non-empty) namespace can be invoked explicitly by specifying the namespace in the action. If no namespace is specified, the default namespace is used.
encody marked this conversation as resolved.
Show resolved Hide resolved

State is isolated between namespaces. That is to say, a contract deployed to `alice.near`'s default namespace cannot read or modify the storage of a contract deployed to `alice.near`'s `multisig` namespace. As described by this NEP, interactions between different namespaces can only be facilitated via reflexive function calls.

## Motivation

- Composing contracts is non-trivial. Currently, it is possible to deploy a single smart contract to a NEAR account using a `DEPLOY_CONTRACT` action. Subsequent `DEPLOY_CONTRACT` actions completely replace existing code with new code, while leaving all storage intact.

- It is impossible for a non-developer user to compose multiple contract standards together into a single contract without actually performing some development work. Account namespaces would enable a user workflow wherein a user only needs to choose which contracts to deploy, and not worry about how to compose them.

- If a developer wants their smart contract to be upgradable, they have to manually write the upgrade functionality into their smart contract, and smart contract upgrades are complex, multi-step operations with many security considerations and opportunities to "brick" the account. Namespaces would allow a developer to deploy a semi-standardized upgrade module to a namespace, and have that manage the upgrade process for the other (default, etc.) namespaces.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Namespaces would allow a developer to deploy a semi-standardized upgrade module to a namespace, and have that manage the upgrade process for the other (default, etc.) namespaces.

This does not explain how namespaces enable this feature. More specifically, what is the limitation of the current protocol that is eliminated with this proposal

Copy link
Author

@encody encody Jun 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Workflow example:

  1. Alice creates alice.near.
  2. Alice deploys main contract bytecode v1 to the default namespace.
  3. Alice deploys bytecode upgrade-manager to the upgrade-manager namespace. The upgrade-manager bytecode provides an upgrade method that performs a contract upgrade sequence (explained in later steps).
  4. Alice deletes all full-access keys from alice.near.
  5. Alice creates a new version of the main contract bytecode. She calls this version v2.
  6. Alice calls alice.near:upgrade-manager->upgrade(v2), passing in the v2 bytecode.
  7. The upgrade-manager code performs a contract upgrade sequence, deploying the v2 bytecode to the default namespace of alice.near.
  8. Turns out, the v2 bytecode is invalid/unusable.
  9. Alice creates another new version of the main contract bytecode, v3.
  10. Alice calls the still-functional upgrade-manager to perform the upgrade sequence with v3.
  11. The account has a valid contract deployed to the default namespace, and continues to work as intended.

If namespaces had not existed, the v2 deployment would have bricked the entire alice.near account.


As this is a part of the [Account Extensions upgrade](https://github.com/near/NEPs/issues/478), some of the benefits of account namespaces are realized in conjunction with other proposals from the upgrade:

- Certain contracts are frequently deployed on different accounts. "Codeless contracts" allow an account to deploy just the hash of a smart contract instead of the full binary. This, in conjunction with namespaces, would allow a user to pick and choose a set of account features to deploy with negligible gas and storage costs.
nagisa marked this conversation as resolved.
Show resolved Hide resolved

- Permissions: It could be possible to set permissions on a particular namespace. For example, restricting the namespace `dao_multisig` to interactions with `dao.near` only.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these the only capabilities that you propose to enable? What about arbitrary host functions, etc.? Having read through the proposal in more detail, I think it is important to have fine grained capabilities for namespaces. What you are proposing is to allow smart contract developers who are not proficient to deploy multiple wasm modules on their account. These wasm modules could be buggy or malicious. If this proposal is expected, I can imagine some users just having a single account with all the different contracts they want to try out / experiment with and without fine grained control over their capabilities, they could end up losing a lot of funds.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how all of the questions in this comment fit together.

Currently, I don't think that the typical non-tech-savvy user deploys any code to their accounts, but please correct me if I am wrong.

Specifically when speaking of non-tech-savvy users, the workflow we conceptualized is something like:

  1. Alice wants to allow Bob to access her funds if she is inactive for 180 days.
  2. Alice logs into her wallet.
  3. Alice clicks on "Account Extensions."
  4. Alice scrolls through the list of available extensions and selects "Dead Man Switch."
  5. Alice inputs parameters:
    • Beneficiary ID: bob.near
    • Timeframe: 180 days
  6. Alice clicks "Save."
  7. Alice approves the contract deployment + initialization function call action:
    • Namespace: dead-man-switch
    • Code: 0x...


- Synchronous execution. Namespaced contracts on the same account should be able to communicate with each other synchronously instead of asynchronously.

## Specification

### The `Namespace` type

We introduce a new primitive type: `Namespace`. A `Namespace` is a string that contains only lowercase letters, numbers, and underscores. The empty string is considered the default namespace.

### `DeployContract` action

The `DeployContract` action is modified to include a `namespace` field. If the `namespace` field is not specified in an RPC call, the default namespace (the empty string) is used. The attached code is then deployed to the specified namespace.

If code has already been deployed to the specified namespace, it is replaced, leaving code deployed to other namespaces untouched.

Once a namespace is created, this NEP does not provide a way to delete it. However, namespaces should not be considered permanent.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why we’d need a concept of namespace creation? Would this creation step introduce semantics meaningfully different from the empty namespace existing for accounts that haven't deployed a contract yet?

If not, I think it would be quite a bit more straightforward to work with this feature if all namespaces were presumed to “exist” with a code_hash equal to 11111111111111111111111111111111 or whatever we use for invalid contract.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Namespace creation is approximately equal to contract deployment in this NEP. If all namespaces are presumed to exist, how does an RPC account view call show namespaces?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I'm not sure! I imagine it would list the non-111...111 hashes, rather than the list of “namespaces”?

But counter-question to your question: what's the expected behaviour of calling a non-existent namespace? If my memory serves me right, in the namespace-less world of today, the behaviour is meaningfully different between calling a function in a non-existent account and calling a function in an existing account without a contract deployed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that case would be similar to calling a non-existent method on an account that does have a contract deployed. Possibly a case for a new, "namespace does not exist" error?


### `FunctionCall` action

The `FunctionCall` action is modified to include a `namespace` field. If the `namespace` field is not specified in an RPC call, the default namespace (the empty string) is used. The function call is then executed on the code deployed to the specified namespace.

### `FunctionCallPermission`

A `namespaces` field is added to `FunctionCallPermission`. This field contains a list of namespaces that the access key is allowed to call. If the `namespaces` field is not specified, the access key is allowed to call all namespaces.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason I’m not seeing why this deviates from the usual “no namespace ⇒ empty namespace” defaulting elsewhere in this NEP? I feel like it might be a notable implicit foot gun in the use of protocol. Consider for example:

  1. Following a tutorial I deploy a contract to the empty (default) namespace;
  2. Since the tutorial is namespace oblivious, they suggest adding a key without namespace;
  3. Later on I deploy a different contract that requires strict access permissions to a namespace.

Forgetting I’ve done 2, I have accidentally allowed the important contract to be called arbitrarily by the old key(s).

The alternative is to equate “empty namespaces ⇒ default empty namespace” here as well, in which case the failure mode is that I attempt to call the new contract with an old key, notice it doesn't work and expand the permissions for the key.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now, a function call access key that does not specify a list of methods is allowed to call any method. Therefore, a function call access key that does not specify a list of namespaces is allowed to call any namespace.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. I feel like the stakes are likely somewhat different here in that deploying a new contract to an account in absence of namespaces overwrites the old one, and thus the user is conscious about both the previous and the new contract (and thus possibly is thinking of the access controls in context of both.) In the world of namespaces they no longer need to care about any previous state anymore.

But fair, I see how the proposed behaviour is consistent.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we need a new syntax for function call access keys, wherein they can be permitted to call "any method on xyz namespace"?


### NEAR VM host functions

The following host functions are added to the NEAR host environment:

- `predecessor_namespace() -> Namespace`: Returns the namespace of the predecessor account, which may be the empty namespace. In the case that the predecessor is not a smart contract, this is the empty namespace.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... so we are using the empty namespace to represent two different values: empty namespace in the predecessor or no smart contract. Could this be a problem? Is there a good way to distinguish between the two?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we usually don't care too much whether a call comes from an EOA or contract, but if the distinction is pertinent, one may use env::signer_account_id().

- `current_namespace() -> Namespace`: Returns the namespace of the current account, which may be the empty namespace.
- `namespace_storage_usage(&Namespace) -> u64`: Returns the storage usage of the given namespace. This includes storage consumed by the contract and state, but not account data (e.g. balance, access keys). That is to say,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be the value of obtaining the storage usage of a different namespace than the one currently executing? If there isn’t one, might it make sense to require that the Namespace arugment is current_namepsace() otherwise a panic is raised for the time being?

Might be worthwhile to double check if an ability to do so does not preclude whatever other improvements we might want to make.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the suggested use-cases for namespaces is to support "hot-swapping" wherein a manager namespace deploys code to other namespaces and manages their state and execution at a high level, so some cross-namespace introspection functions like this would be useful.


$$\verb|storage_usage()| \ge \sum_{n} \verb|namespace_storage_usage| (n)$$

There is no need for a `signer_namespace` function, because a signer is never a smart contract, and can therefore never have a non-empty namespace.

Some existing host functions will need to be updated to include namespaces:
encody marked this conversation as resolved.
Show resolved Hide resolved

- `promise_batch_action_function_call`: Updated to include namespace.
- `promise_batch_action_deploy_contract`: Updated to include namespace.
- etc.

Existing host functions will generally still refer to the account as a whole:

- `storage_usage() -> u64`: Returns the storage usage of the current account, including all namespaces.
encody marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the current proposal, then there are two ways of getting storage used. storage_usage() and namespace_storage_usage(). I can see some developers getting confused when storage_usage() does not add up to what they are seeing the current namespace is actually using (they might have forgotten that other namespaces exist).

In the ideal world, it would be great to have a single hostfunction with a signature like: storage_usage(enum {EntireContract; Namespace(namespace)'}). Does something like this make sense?

If it does, I propose that we update the proposed namespace_storage_usage() to be something like the above and then mark storage_usage() as deprecated. We probably never will be able to remove it but hopefully with sufficient documentation, we will minimise the confusion.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I have enough information to have an informed opinion here. Making a decision one way or another has the potential to break contracts: if storage_usage() only references the current namespace, old contracts could incorrectly think the account is not using storage when it is, and if it references the whole account's usage (all namespaces), then old contracts could incorrectly think the current contract is using storage when it is not.

- `account_balance() -> u128`: Returns the entire balance of the current account. There is no namespace-specific accounting.

### RPC view calls

#### `view_account`

The `code_hash` field contains the hash of the code deployed to the default namespace. The `code_hashes` field contains a map of namespaces to code hashes, including the default namespace. (This means that the hash of the code deployed to the default namespace is returned twice. This is intentional, for backwards compatibility.)

#### `view_state`

The `view_state` call now accepts an optional `namespace` field. If it is not specified, the default namespace is used. The `view_state` call is then executed on the state associated with the specified namespace.

#### `view_code`

The `view_code` call now accepts an optional `namespace` field. If it is not specified, the default namespace is used. The `view_code` call then returns the code deployed on the specified namespace.

## Reference Implementation

- https://github.com/near/nearcore/pull/8890

## Security Implications

- Applications that detect smart contract updates by tracking the `code_hash` field from a `view_account` call will fail to account for updates to namespaces. (Note that the correct way to track code changes is by monitoring the blockchain for `DeployContract` actions targeting the account, not by tracking `code_hash`.)
- As described by this NEP, namespaces all have full permission to act on behalf of the account, just as any smart contract.
- If a namespaced contract interacts with a legacy contract (unaware of namespaces), it is possible that the legacy contract may save the account ID of the namespaced contract, but not the namespace. If the legacy contract subsequently attempts to interact with the namespaced contract, it will only be able to interact with the contract deployed to the default namespace instead. However, this is equivalent to the case in which an non-contract account signs the same set of actions to the legacy contract.

## Drawbacks (Optional)

## Unresolved Issues (Optional)
Comment on lines +106 to +108
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
## Drawbacks (Optional)
## Unresolved Issues (Optional)
## Drawbacks
## Unresolved Issues


- How to delete a namespace?
- How to enumerate namespaces from within a smart contract?
- Backwards compatibility issues could be resolved with a per-account routing table that maps incoming method names to a [namespace, method name] pair.

## Alternatives

- Only using a routing table. However, this increases complexity for end-users significantly.

## Future possibilities

- Sync execution between namespaces.
- Permissioned namespaces.
- Codeless contracts / contract subscriptions / global contracts.

See [the Account Extensions upgrade master issue](https://github.com/near/NEPs/issues/478).

## Changelog

### 1.0.0 - Initial Version

#### Benefits

- Easier contract composability, e.g. just deploy the NEP-141 and NEP-330 modules and you’re set. Good for developers (esp. w.r.t. contract factories) and non-developer users (one-click deploy composable modules).
- Safer and easier contract upgrades, e.g. have an upgrade_controller namespace that manages upgrading the rest of the contract’s components, so a bad upgrade doesn’t brick the account.

#### Concerns

## Copyright

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

## References

- https://gov.near.org/t/proposal-account-extensions-contract-namespaces/34227