Skip to content

feat(SIP-53): add singleton ability modifier#53

Open
admin-aftermath wants to merge 1 commit intosui-foundation:mainfrom
AftermathFinance:sip/singleton-ability
Open

feat(SIP-53): add singleton ability modifier#53
admin-aftermath wants to merge 1 commit intosui-foundation:mainfrom
AftermathFinance:sip/singleton-ability

Conversation

@admin-aftermath
Copy link
Contributor

No description provided.

@admin-aftermath
Copy link
Contributor Author

I am currently running into another use case where I would want to use singleton and wanted to document it here.

I am working on an "ecosystem" of packages wherein I need to manage cross-package authorization. There is a core package and a series of packages that can be granted the ability to call permissioned functions from the core package.

I currently have a system where packages are granted authority by an AdminCap by whitelisting a witness type that is local to the external package. While whitelisted, the external package is allowed to call permissioned functions from the core package by passing in the witness type as proof of authority.

The issue with the above is that the external package just needs to pass in a witness type as proof. I have wrapped all permissioned functions so the witness type is created and passed in to the underlying core package function -- so, for the current contract version, there is deterministic security around (1) when the witness type is created and (2) how it is being used. The issue is that upon a package upgrade this determinism might not hold as you could just create a public constructor for the witness type.

Ultimately what you would want, from a transparency / security standpoint, is proof that the "witness" (using quotes here since it will likely be a struct with singleton + store and not a true witness) is a one of a kind. It's worth noting that although conceptually similar, a one-time witness cannot be used here as it only has drop.

This is a dumbed down version of what I have currently but gets the point across. Also, there are ways to get around this problem today, but singleton would be an simpler, more-intuitive approach.

@tnowacki
Copy link

Apologies for almost being a year late here (don't hesitate ping me on telegram if you need someone to look at something like this).

When this first went up, we thought about it and thought maybe to revisit it once we added this internal functionality. We ended up not making internal a language feature, but rather a standard library one with std::internal::Permit.

With such a creation, you could define a wrapper around Singleton<T> that hopefully feels pretty decent:

module my_package::singleton;

public struct Registry has key {
    id: UID,
}

public struct Marker<phantom T>() has copy, drop, store;

public struct Singleton<T>(T) has store;

fun init(ctx: &mut TxContext) {
    transfer::share_object(Registry { id: object::new(ctx) })
}

// You could make a specific Permit for this action
public fun singleton<T>(registry: &mut Registry, value: T, _: internal::Permit<T>): Singleton<T> {
    sui::dynamic_field::add(&mut registry.id, Marker<T>(), true);
    Singleton(value)
}

// You could imagine not requiring a Permit here, or making a specific Permit for this action
public fun borrow<T>(singleton: &Singleton<T>, _: internal::Permit<T>): &T {
    &singleton.0
}

// You could imagine not requiring a Permit here, or making a specific Permit for this action
public fun borrow_mut<T>(singleton: &mut Singleton<T>, _: internal::Permit<T>): &mut T {
    &mut singleton.0
}

We hesitate on adding something like this to the sui framework package because we aren't exactly sure what the best usage pattern would be:

  • Does Singleton need an object variant?
  • Should you be able to destroy a Singleton?
  • Should each one of these actions have a separate Permit? Or at least should the singleton module have its own permit? (probably at least a yes to that second one)

If we had a good common use case for something like this with these questions being easily answered, I think it could be valuable in sui::

@tnowacki
Copy link

A bit of a side note on abilities, something like singleton would not be a great ability I think...
It is hard to pin down exactly without getting too technical, but abilities should be thought of probably like the following:

  • They are used to gate access to bytecode instructions (or native functions).
  • They induce requirements on the fields (and in non-phantom cases the type arguments) of the type
    • copy, drop, and store require themselves
    • key requires store
  • They need to be "forgettable", e.g. if I have some type t: a it should be safe to use it to instantiate a generic that does not require a, e.g. fun foo<t>
    • I'm not sure this is the case for singleton for what its worth. Since "forgetting" singleton would let you copy one if the underlying type had copy.

@admin-aftermath
Copy link
Contributor Author

Here are my quick thoughts on these questions.

  • Does Singleton need an object variant?

I'm sure I have a very specific view of this SIP and the benefits of singleton but imo singleton benefits the most when applied to objects, so an object variant would be required.

I view singleton as a way to reduce the attack vectors across package boundaries: with singleton, you can assure that the state I expect to interact with is in fact the state I will interact with. For example,

  1. A secondary Config (with trusted fields), is not created and passed in to my package's context, or
  2. A secondary AdminCap is not maliciously created and transferred to an address outside the scope of the true package owners.

Both of these scenarios involve reducing the possible number of objects of a given type down to the expected size of 1. I'm sure there are also benefits for types T has store but imo the biggest impact is for shared or owned objects.

  • Should you be able to destroy a Singleton?

Given the examples I've used above and in the SIP I don't see a practical reason to need this but still imo it would make sense to support. Regardless, a destroy function would need to be defined within the package that defines the inner type T, so at the bare minimum you would be providing feature parity with what is possible w/o singleton.

  • Should each one of these actions have a separate Permit? Or at least should the singleton module have its own permit? (probably at least a yes to that second one)

Similar to the reasoning above, how you've defined the API in your first message is perfect. For creating and accessing by both reference / mutably you have feature parity as w/o the Singleton wrapper. The package maintainers of the inner type T can gate access to these calls as they would without Singleton.


All in all I would be in favor of a singleton.move module being added to the Sui Framework.

@admin-aftermath
Copy link
Contributor Author

One minor detail I dislike about the Singleton<T> approach is that, currently, it would not be possible to create Singleton for T during a package publish. You would need to create T during the package init (or a followup transaction) and then call singleton on T, since you would be unable to pass in the Registry object to the init function.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants