Skip to content

Conversation

@SidestreamStillJelly
Copy link

This PR creates a SIP draft, that intends to specify open standard for attesters to share signals about packages or modules, as per the original RFP "Record trust signals for packages onchain".

Copy link

@cos cos left a comment

Choose a reason for hiding this comment

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

Great to see attestations are happening :-)

High level, I would move in the direction of either curated attestation types or no type registry at all. The current design seems to add the complexity of a registry without much of the benefit of standardization. Actually, unless I'm missing something, the registry may not be necessary even with curated types.

We could just have some guidelines on the interface necessary for an attestation to be indexable. And shared event types for discoverability.

Also, imo, it would be really useful to have well-designed types, maybe part of this SIP, for what we already know. Like a really good audit type, including everything needed, auditor details, storage of artifacts on Walrus, etc. This way we won't end up with a bunch a different types created by different auditors, and indexes dealing with displaying diverse types in a pretty way.

@SidestreamStillJelly
Copy link
Author

High level, I would move in the direction of either curated attestation types or no type registry at all.

I think the intention to "curate" attestation types does not contradict the possibility to discover all existing types in a structured form. The curation implies that everyone is able to create attestation types, while we will have a mechanism to highlight only certain ones. The curation mechanism is already in the heart of the "per-sender list of trusted attesters", and I just added a similar structure for the attestation types via a "per-sender list of trusted attestation types" which can be maintained by a trusted entity.

To bring a bigger context, the proposal to create "attestation types" is highly inspired by the established on-chain attestation protocols like "EAS: Ethereum Attestation Service" (see their SchemaRegistry) and "EthSign: Sign Protocol" (see their Schema implementation).

In addition to "making attestation types discoverable on-chain", adding a separate function required for creating attestation types is also useful to:

  • Enforce Display type to be created for each attestation type
  • Enforce some standard base qualities, like is_revocable (which would otherwise always be non-revokable/revokable indifferent of the type) – learning from existing usage of attestations, different use-cases can have different requirements for the "revocability".

@SidestreamStillJelly
Copy link
Author

it would be really useful to have well-designed types, maybe part of this SIP

Yes, we're planning to propose the first attestation types and their implementation as part of the "reference implementation" section, but I think it shouldn't be the core of this SIP, as its primary focus is on establishing the registry itself.

Copy link
Contributor

@amnn amnn left a comment

Choose a reason for hiding this comment

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

Thanks for this @SidestreamStillJelly, I also had some questions about what "registry" really means in the context of this proposal, and some other thoughts about what needs to be stored on-chain as part of attestations vs not.

I think it's also worth getting some of the folks working on related standards, like Display to weigh in on their thinking -- I'll bring this up with them!

@cos
Copy link

cos commented Mar 26, 2025

Yes, we're planning to propose the first attestation types and their implementation as part of the "reference implementation" section, but I think it shouldn't be the core of this SIP, as its primary focus is on establishing the registry itself.

My take is that curated attestation types would be the hard and super useful part. For example, getting consensus among auditors on a common attestation type (and pretty Display) we can all share. Without, we will see a lot of fragmentation and clunky implementations.

Some form of registry might be the natural consequence of this discovery process, instead of being created first.

@amnn
Copy link
Contributor

amnn commented Apr 4, 2025

Thanks for sharing the POC, that really helps make things concrete. I've been discussing this SIP internally with folks, here's what came out of that discussion, let me know what you think:

Scope

There has been a good discussion on this thread about what should and shouldn't be in scope for this SIP (i.e. what is a minimally viable attestations platform). Here's where we landed after our internal discussions:

In Scope

  • The core mechanism of associating trust signals to particular package versions.
  • Common operations (e.g. revocation) that are applicable to potentially any attestation.
  • The authorization scheme for common operations.
  • One or more conventions agreed for common kinds of attestations (e.g. audit reports, verified source code, etc).
  • An end-to-end flow with good penetration among popular packages: An attestation, that has been attached to these packages, and shows up in an explorer.

Out of Scope

  • Having a way for users to express preference for attesters or specific attestation types, (on-chain or otherwise).
  • Similarly having an on-chain registry for attestation schemas. What is important is that we have good alignment over the few initial kinds of attestations we care about.
  • Supporting attestations over sets of packages (all versions of a package, packages between version bounds).
  • Similarly, subscriptions for package upgrades.

We can derive significant value from the in scope parts without these features, tackling them, when we have a better idea of how the system will be used. Others can also potentially contribute them, given the work is inherently decentralized.

Let us know if you feel strongly about pulling these or other requirements in scope though. These are just our thoughts, and it's open for discussion.

Common operations

Revocation

In the current design, whether or not an attestation is revocable is a property of the attestation type. We wonder whether this is necessary. Could we say that any attestation could be revoked if the attester later discovers a reason to?

There is perhaps scope for saying that when an Attestation<T> is revoked, it is replaced with a Revoked<T> so that history is not lost.

Pinning

There was discussion about a way for package publishers to hide certain attestations, and then a concern that this would allow bad actors to hide legitimate attestations that showed their package in a bad light.

When we discussed it internally, we came up with an alternative: Rather than package publishers being able to hide attestations they did not like, they could highlight (or "pin") attestations they do like.

This would allow legitimate publishers to highlight high value attestations (e.g. an audit they commissioned from a reputable audit firm) without giving bad actors the power to silence legitimate attestations.

Modifying Attestations

Although we want to avoid supporting arbitrary modifications to attestation contents, both revocation and pinning are a form of modification, and the initial idea of transferring the attestation to a package makes this difficult to achieve because packages are immutable.

The current solution of storing modifications in a lookaside table is workable, but it means clients need to consult two places to understand the current state of an attestation.

Internally we have been discussing a new feature that may help with this -- derived addresses -- which would allow packages to generate addresses from a parent address and a Move value (similarly to how dynamic field names work).

  • If we had this feature, we could represent the attestation registry as a shared object with ID R and an attestation A for package P would be represented by transferring A to address R || Package(P), where Package(_) is some wrapper type in Move.
  • Adding an attestation remains a parallel operation, because it is a simple transfer.
  • Revocation and Pinning can be performed by "receiving" the attestation, modifying it, and re-transferring it
  • Off-chain consumers only need to look in one place to fetch all of a package's attestations.

We do not have this feature yet, but it seems to yield a meaningful usability improvement, so if it seems appealing, we can prioritize developing it.

Authorization scheme

We discussed that using the attestation's original sender to authorize further actions (like revocation) is not ideal. This advice comes from our past experience where this mechanism has been too rigid -- it makes it difficult to rotate keys, or delegate control.

I'll nudge the relevant team to take a deeper look and give advice.

@SidestreamStillJelly
Copy link
Author

In Scope

  • One or more conventions agreed for common kinds of attestations (e.g. audit reports, verified source code, etc).
  • An end-to-end flow with good penetration among popular packages: An attestation, that has been attached to these packages, and shows up in an explorer.

I agree this is in the scope for our grant, but not sure how it can be applied to this document. I think the SIP can stay abstract and don’t mention specific attestation types as it will be irrelevant for the future readers thinking to implement their own types – let me know if you disagree. Also, we can keep this PR open until we have Audit attestation type implemented and used on-chain, but the discussion on specific attestation types can happen inside the PoC repository, in a relevant PR.

Out of Scope

Agree with all points, but schema. For the library/frontend, it would be useful to have some form of registry and also forbid updating Display type. So if you're not fully against the currently proposed structure, we would like to keep this one point in scope.

Revocation

Could we say that any attestation could be revoked if the attester later discovers a reason to?

Yes, using RevokeCap, we can also delegate “revocability” decision to the package implementing a specific attestation type. The solution is not great, since the frontend would not be able to automatically determine revokability of a type, but is acceptable. We've updated the PoC.

Pinning

Rather than package publishers being able to hide attestations they did not like, they could highlight (or "pin") attestations they do like.

Sounds good, that's actually what we assumed, when you first mentioned that package registry would integrate attestations. To be clear, the list of “pinned attestations per Publisher” will be stored and managed by the move registry?

Modifying Attestations

both revocation and pinning are a form of modification

I am not sure how pinning is supposed to modify the attestation. As you describe this functionality, we see it as an external set Table<Publisher.id, VecSet<Attestation.id>> that doesn’t need to modify the Attestation object.

The current solution of storing modifications in a lookaside table is workable, but it means clients need to consult two places to understand the current state of an attestation.

Consulting two places is not ideal to assess current attestation status, but a compromise – if we decide that full immutability of the Attestation is a more important quality. But if you think that convenience of having a single call is more important, then a new “derived address” can become a solution. But in order to agree and plan this in, we need to understand a few points about this proposed feature:

  • Would it be compatible with existing methods for object fetching and indexing (i.e. suix_getOwnedObjects)?
  • How would explorers be able to fetch and display those “in-between” objects (under both addresses or only under “derived” ones)? How will explorers be able to find all derived addresses?
  • How long would it take to specify, implement and release the “derived address” feature, until we can use it?

Just to outline two other solutions to revocability that doesn’t require new features:

  • Option A: (current approach) Revocation object with matching attestation: ID field is sent to the same address as the original Attestation. In other words, to get Attestation state, all Revocation objects have to be fetched and matched with the Attestation on the client.
  • Option B: Revocation flag is set in some table e.g.: Registry.is_revoked[attestation_id] = true. This also keeps the Attestation object intact, but allows a single targeted call to fetch Revocation.

Authorization scheme

Agree here, we’ve updated the SIP as well as the PoC to use the proposed *Cap pattern.

@amnn
Copy link
Contributor

amnn commented Apr 16, 2025

Thanks for the detailed reply @SidestreamStillJelly, and sorry for the delay in getting back to you.

In Scope

  • One or more conventions agreed for common kinds of attestations (e.g. audit reports, verified source code, etc).
  • An end-to-end flow with good penetration among popular packages: An attestation, that has been attached to these packages, and shows up in an explorer.

I agree this is in the scope for our grant, but not sure how it can be applied to this document.

I would be fine to leave the discussion about the conventions for common attestations out of this SIP. It may be worth raising another informational SIP for that though, because it is something that you may want to get feedback on from the broader community. Agreed that the end-to-end flow aspect is not relevant to this SIP.

Out of Scope

Agree with all points, but schema. For the library/frontend, it would be useful to have some form of registry and also forbid updating Display type. So if you're not fully against the currently proposed structure, we would like to keep this one point in scope.

In the previous design it looked like the type registry was used for two things:

  • It's part of the logic that enforces that Display cannot change. This seems like a good property to enforce so happy to keep this in scope, but you don't need a type registry for that.
  • It tracks whether an attestation is revocable or not -- but as per the discussion below, this is now a valid operation on any attestation, so we don't need to track this metadata.

So it seemed like you no longer needed a type registry to support the features you wanted to support in the initial implementation, but maybe I'm missing something -- can you share more details on how you imagine a type registry to be used by a library or front-end?

Revocation

Could we say that any attestation could be revoked if the attester later discovers a reason to?

Yes, using RevokeCap, we can also delegate “revocability” decision to the package implementing a specific attestation type. The solution is not great, since the frontend would not be able to automatically determine revokability of a type, but is acceptable. We've updated the PoC.

👍 thank you! The thinking on our part is that an "unrevocable" attestation seemed like an almost impossibly high bar, (accounting for human error, incomplete information, bugs, etc), so it seemed safest to say that you should be prepared for any attestation to be revocable.

Pinning

Rather than package publishers being able to hide attestations they did not like, they could highlight (or "pin") attestations they do like.

Sounds good, that's actually what we assumed, when you first mentioned that package registry would integrate attestations. To be clear, the list of “pinned attestations per Publisher” will be stored and managed by the move registry?

Pinning and Move Registry integrations are unrelated.

  • Pinning is an attestation-specific behaviour. A package publisher can highlight attestations that are important to them, and front-ends can use that as a signal to preference these attestations (show them first).
    • The Attestations package would need to decide how to identify a package publisher to authorise pinning, and
    • how to surface pinned attestations so that explorers and other clients can fetch a package's pinned attestations (without having to scan through all available attestations).
  • Integrations with Package Registries, like MVR (Move Registry) would come in two different forms:
    • Registries would often have front-ends, which are essentially domain-specific explorers, so they are likely to display attestations.
    • Registries also gather trust signals about packages (they may be able to attest to the source code of a package, after having verified it, or to the organisation that owns that package, or to test coverage, etc etc), and these could show up as attestations on the package.

Modifying Attestations

both revocation and pinning are a form of modification

I am not sure how pinning is supposed to modify the attestation. As you describe this functionality, we see it as an external set Table<Publisher.id, VecSet<Attestation.id>> that doesn’t need to modify the Attestation object.

Here's how we imagined pinning would work:

  • On the write side, the package publisher should be able to pin and unpin attestations for their package. When unpinning an attestation, we want to remember that it was pinned at one time (to prevent an attack where a bad actor pins a scam attestation and then later buries it to confuse people).
  • On the read side, it should be possible to fetch a package's pinned attestations without having to scan through all its attestations.

It's possible to implement this feature using a lookaside table (although it should probably be keyed by the package ID, not the publisher ID), but that brings us to the topic below, about minimising the number of places that need to be consulted to understand the latest state of an attestation:

The current solution of storing modifications in a lookaside table is workable, but it means clients need to consult two places to understand the current state of an attestation.

Consulting two places is not ideal to assess current attestation status, but a compromise – if we decide that full immutability of the Attestation is a more important quality. But if you think that convenience of having a single call is more important, then a new “derived address” can become a solution.

There's two properties that we're going for here:

  • For any package, it should be possible to fetch the attestations associated with it, using generally available RPCs. This property is designed to help adoption -- if it's easy to access, more places will be able to take advantage of this data.
  • For a given attestation, it should be possible to discover its most up-to-date state by consulting one place. It's more than a convenience -- we want to avoid giving a partial picture of an attestation.

We don't need to compromise on the immutability of attestation data to achieve this goal -- we just rely on the Attestation Move Package to enforce that property for us (that the content of an attestation cannot be modified), while still allowing us to make modifications for revocability and pinning.

... But in order to agree and plan this in, we need to understand a few points about this proposed feature:

  • Would it be compatible with existing methods for object fetching and indexing (i.e. suix_getOwnedObjects)?
  • How would explorers be able to fetch and display those “in-between” objects (under both addresses or only under “derived” ones)? How will explorers be able to find all derived addresses?
  • How long would it take to specify, implement and release the “derived address” feature, until we can use it?

These are great questions:

  • Yes, fetching objects owned by derived addresses would work just like fetching objects owned by any other address today.
  • Explorers who want to fetch a package's attestations would do so by computing the derived address for that package and then fetching its owned objects.
    • If we represent pinned attestations with a type wrapper (e.g. Pinned<Attestation<T>>), then it becomes possible to write a query to fetch just the pinned attestations for a type. You can do something similar for revocation, to make it possible to order them last.

Timeline is always a tricky one, for a protocol feature like this, it's measured in months, but it also depends on demand for that feature. We're also mindful that timeline has funding implications. If we're aligned that this feature is the right thing to build attestations on top of, we can go back to the foundation and discuss how to amend terms so that we have the flexibility to build the right thing, and not the fast thing.

On a technical level, it should be possible to prototype attestations using a centralised registry with dynamic fields, before we have derived address support, so depending on this feature wouldn't be a hard blocker until we're talking about the official launch.

Just to outline two other solutions to revocability that doesn’t require new features:

  • Option A: (current approach) Revocation object with matching attestation: ID field is sent to the same address as the original Attestation. In other words, to get Attestation state, all Revocation objects have to be fetched and matched with the Attestation on the client.
  • Option B: Revocation flag is set in some table e.g.: Registry.is_revoked[attestation_id] = true. This also keeps the Attestation object intact, but allows a single targeted call to fetch Revocation.

These alternative solutions for revocations didn't meet the second property: If revocations are stored in a lookaside table, then people can forget to look at that table. If they are handled by sending a new revocation attestation, clients will potentially need to fetch all attestations to know that they have the most up-to-date knowledge of any given attestation. Pinning is similar in this regard.

@SidestreamStillJelly
Copy link
Author

SidestreamStillJelly commented Jun 18, 2025

Modifying Attestations

Based on your comment @amnn, I understand that you're proposing the following (please correct me if I'm wrong):

  • Make pinning part of the SIP and PoC (so that it can be used across Registries) – ✅ agreed
  • When "derived addresses" feature is available, use it to re-implement pinning logic – ✅ added to "Steps after the feature is available"
    • Until then, temporary implement pining using not-so-ideal approach – ⚠️ agreed, but need to agree on the implementation, added to "Immediate next steps"
  • When "derived addresses" feature is available, use Attestation object to store isRevoked quality, so that there is just one place to consult – ✅ added to "Steps after the feature is available"
    • Until then, temporary implement revocation using not-so-ideal approach (for example, using Revocation object emitted to the same package or look-up table, etc) – ⚠️ agreed, but need to agree on the implementation, added to "Immediate next steps"

Next steps

Immediate next steps

  • Align on the need to register type
  • Align on the temporary implementation of the revocation
  • Align on the temporary implementation of the pinning
  • Update SIP and PoC on type registration, if necessary
  • Describe revocation in the SIP; update PoC, if necessary
  • Add pinning to the SIP, PoC

Steps after the feature is available

  • Update revocation logic in the PoC and the TS library (using new feature and modifying Attestation object), update SIP
  • Update pinning logic in the PoC and the TS library (using new feature and modifying Attestation object), update SIP

Align on the need to register type

In the previous design it looked like the type registry was used for two things:

  • It's part of the logic that enforces that Display cannot change. This seems like a good property to enforce so happy to keep this in scope, but you don't need a type registry for that.

I likely miss some important detail about Display standard or Move, as I could not figure out how can we enforce its immutability without the use of some sort of registry / table. Based on the docs, I can see that to edit a Display, a user need to provide mutable Display object, but nothing prevents someone with the Publisher object to simply create another Display object for the same type, multiple times (at least it doesn't throw when I tried it in the tests). How the subsequent Display objects for the same type are handled by the block scanners, I guess it up for their implementation, but likely they are using the latest one. Or do you, in general, had a different idea in mind for enforcing immutability?

but maybe I'm missing something -- can you share more details on how you imagine a type registry to be used by a library or front-end?

Another reason to register types (besides enforcing immutability of the Display) is to make types self-discoverable. Ideally, a frontend wouldn't have to be updated to support a new attestation type, but will fetch a list of AttestationTypes, fetch relevant underlying Type definition and then construct an input form, derived from that type. In case there is no concept of AttestationTypes, it have to either hardcode possible types in the frontend or off-chain index all attestations to quickly find ones that reference unique underlying type.

To be clear: the latest implementation in the PoC PR is using registry to enforce creation of a single Display and AttestationType, then use AttestationType to create attestation itself (without dependency on the registry). Please let me know if it's still something that you would rather remove.

Align on the temporary implementation of the revocation, pinning

Could you please outline a specific implementation that would be satisfactory here? By "centralised registry with dynamic fields", do you mean to use Registry object, add attestations field with a Table, where key is the package address, value is a Bag for the Attestation objects? Maybe I misunderstanding the proposal, but what I don't understand is, how would we be able to fetch all Attestation objects for a given address, when using dynamic fields?

@amnn
Copy link
Contributor

amnn commented Jun 20, 2025

Hi @SidestreamStillJelly, yes we're aligned on my proposal.

Align on the need to register type

I can see that to edit a Display, a user need to provide mutable Display object, but nothing prevents someone with the Publisher object to simply create another Display object for the same type, multiple times (at least it doesn't throw when I tried it in the tests).

This is a great point -- your understanding is right for how Display currently works. I suspect this is an over sight because we have not yet had a requirement to enforce Display immutability. I will check with the team!

There are some ways around this (centered around the fact that you want to create a Display<Attestation<T>> rather than a Display<T>), but they come with their own trade-offs and the team is also in the process of porting Display to using its own registry, rather than the current set-up where display information is carried through events, and in that world, you would not need to introduce another registry to enforce immutability.

Ideally, a frontend wouldn't have to be updated to support a new attestation type, but will fetch a list of AttestationTypes, fetch relevant underlying Type definition and then construct an input form, derived from that type.

If I understand correctly, this is to do with creating attestations, rather than about displaying them -- you want to be able to create a generic front-end for others to make attestations with. I can see how this would be useful, but I would treat this as an extension or a nice to have, for the following reasons:

  • Not all attestations will be created by humans (in fact, I expect the majority of attestations would not be), but rather by tools/services that are performing checks on packages. E.g. MVR might automatically attest to (verified) source code that was submitted to its UI -- it checks that the source matches the bytecode and then pushes an attestation -- and then it or other tools might use that source code to attest to test coverage, (lack of) lints and warnings, etc.
  • Those attestations that are created by humans are likely to be created by sophisticated actors (e.g. auditors) who are comfortable with building transactions on the CLI (and in general, we do not gain a lot by lowering the barrier to make an attestation -- we want this to be something that mainly sophisticated actors do).
  • Those sophisticated actors may have unconventional needs when it comes to signing which are difficult to mediate through a wallet. (Maybe they want to attest via a multi-sig, or using keys that are custodied in a TPM, or KMS, etc).

All that being said, my main concern with a traditional "registry" pattern is that it creates a parallelisation bottleneck, and I think it's possible to avoid that and still support all the above by creating an immutable object as part of registration, rather than adding a type to a central registry:

module attestation::type_def;

public struct AttestationType<T: key + store> has key {
    id: UID,
    /// Hypothetical cap for modifying `T`'s Display, in the registry model.
    display: DisplayCap<T>,
}

public fun register<T: key + store>(display: DisplayCap<T>, ctx: &mut TxContext) {
    transfer::freeze_object(AttestationType {
        id: object::new(ctx),
        display,
    })
}

By wrapping the DisplayCap in this frozen object, you can prove that it can't be used to further modify the type's display format, when you want to publish some Attestation<T> you will need to supply an &AttestationType<T> to prove that it is a valid type to be using as an Attestation, and you can also use filtering by object type to list valid attestation types.

Later on, if you wanted to create some subset of attestations that can be automatically created via a front-end, you could publish the schemas for those in the same way, so this approach is extensible and composable without sacrificing parallelism.

Align on the temporary implementation of the revocation, pinning

Could you please outline a specific implementation that would be satisfactory here? By "centralised registry with dynamic fields", do you mean to use Registry object, add attestations field with a Table, where key is the package address, value is a Bag for the Attestation objects? Maybe I misunderstanding the proposal, but what I don't understand is, how would we be able to fetch all Attestation objects for a given address, when using dynamic fields?

You can fetch all the dynamic fields owned by an object, just like you can fetch all objects owned by an address.

@SidestreamStillJelly
Copy link
Author

yes we're aligned on my proposal

Great 🖤

Align on the need to register type

All that being said, my main concern with a traditional "registry" pattern is that it creates a parallelisation bottleneck

Agree on the parallelization issue, but I have to say that current implementation of the register_type already did what you've described: it created and freezed AttestationType, then used its read reference in the attest function. As you think freezing DisplayCap is a better short-term approach, we have to freeze Publisher of the type. I just applied this suggestion via this commit.

Align on the temporary implementation of the revocation, pinning

You can fetch all the dynamic fields owned by an object, just like you can fetch all objects owned by an address.

I've now implemented both revocation and pinning in the PoC PR the way it was proposed above. Please review the PR: once approved, I will update SIP to match PoC.

@amnn
Copy link
Contributor

amnn commented Jun 25, 2025

but I have to say that current implementation of the register_type already did what you've described

Ah noted -- I have been focussing mainly on the SIP, apologies. I will take a detailed look at the PoC in the next couple of days.

@amnn
Copy link
Contributor

amnn commented Aug 14, 2025

I'm happy with the scope and wording of this SIP! I think we need to find you an Editor from Sui Foundation to get it officially stamped though.

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.

4 participants