Skip to content

Investigate separation between internal and external account component procedures #2862

@bobbinth

Description

@bobbinth

Up until this point, we've assumed that all public procedures of an account component would be invoked via the call instruction, and all such procedures get included into the account code commitment. But, as we've started to build components that depend on other components, this may need to be reevaluated.

To give a specific example: in #2712 we've implemented role-based access control component (RBAC) which depends on the Ownable2Step component. Specifically, the assert_sender_is_owner_or_role_admin procedure needs to check whether the sender of the note that is being executed is the owner of the account. And for this, it needs to rely on the functionality of Ownable2Step. The way this is currently implemented is by invoking ownable2step::is_sender_owner_internal via the exec instruction.

A similar pattern is also present in the Pausable component (being implemented in #2793). There, 2 out of 3 procedures (pause and unpause) are meant to invoked "internally" - i.e., from within another component. And in fact, these procedures shouldn't be accessible externally (e.g., from a note script).

The main implication is that we may need to create two types of component procedures (this was first described in #2793 (comment)):

  1. "External" procedures - these are meant to be invoked via the call instruction and would be accessible to the note/tx scripts and via FPI calls.
  2. "Internal" procedures - these are meant to be invoked via the exec instruction from within other components deployed in the same account.

To support this, we would need to switch over to using attributes to identify "external" procedures. There are a couple of ways to do it:

Option 1: we can create some specialized attribute(s) for this. For example:

# This procedure forms a part of the public interface of the component and should be 
# invoked via the `call` instruction.
@external
pub proc foo
    ...
end

# This procedure is available to be invoked from other components installed in the account
# but is not accessible from external code. It should be invoked via the `exec` instruction.
pub proc bar
    ...
end

The exact naming of the attribute is TBD - i.e., we should probably use something less generic than @external.

Option 2: we can try to rely on the built-in calling convention attributes. It could look something like this:

# This procedure forms a part of the public interface of the component and should be 
# invoked via the `call` instruction.
@component_model
pub proc foo
    ...
end

# This procedure is available to be invoked from other components installed in the account
# but is not accessible from external code. It should be invoked via the `exec` instruction.
@fast
pub proc bar
    ...
end

This mostly works - but there are a couple of issues:

  1. The @fast calling convention semantics are not exactly the same as what we currently use. The main point of differences is that "internal" procedures can pass any number of arguments via the stack (not just 16 as is required by the @fast calling convention).
  2. @fast and @component_model names are not super meaningful in this specific context and may cause some confusion.

(cc @bitwalker and @greenhat in case I'm saying anything incorrect about calling conventions)

We could probably fix this by introducing new calling conventions or maybe changing the semantics of the existing ones. It may be nice to be able to say something like:

@component_external    # same as current `@component_model`
pub proc foo
    ...
end

@component_internal    # like `@fast` but with 16-stack element limitation for args
pub proc bar
    ...
end

But other approaches could work too.


Regardless of the approach we take, there is one annoying thing that would be good to resolve:

Some procedures may may need to be accessible in both contexts. For example, in the Pausable component, the is_paused procedure should be a part of the public interface (so that we could invoke it via the FPI calls) and ideally, it'll be also accessible internally as it would be used by internal components.

I can think of two ways to handle it:

  • Use different names for these procedures - e.g., is_paused and is_paused_internal.
  • Only have the external is_paused procedure and invoke it via the call instruction even internally.

Neither of these is ideal: the first approach is probably going to create confusion and the second approach would incur meaningful overhead (i.e., using call when exec would do). Maybe there is something else we can come up with?

This is also somewhat related to @greenhat's question from 0xMiden/compiler#697 (comment).

Metadata

Metadata

Assignees

No one assigned

    Labels

    standardsRelated to standard note scripts or account components

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions