Verax On-Chain Attestations + Ceramic Dynamic Storage (Tutorial Draft)

Feedback Request - Verax + Ceramic Integration Example

Hello! My name is Mark and I’m a partner eng on the 3Box Labs team (creators of the Ceramic Network). One of the areas our efforts have been focused on over the past few months has been supporting and extending verifiable data. One obvious route has been to use Ceramic as a storage layer for “off-chain” verifiable data (for example, eas off-chain attestations and verifiable credentials) - many teams in our community gravitate toward this design pattern given Ceramic’s inherent provenance and user-data-control functionality.

We’ve also been thinking about how Ceramic can extend on-chain data artifacts - for example, acting as a mutable data layer that an on-chain Verax attestation points to.

I’m hoping to engage the Verax community on a fairly simple first take that I’ve placed into a tutorial format meant to extend the existing from schema to attestation Verax docs page. I would love any and all feedback, as well as notes on how this might be applicable to projects you’re currently building. I’ve copy-pasted for your convenience below, but here is the repository it references:

Ceramic Verax Tutorial.


Verax On-Chain Attestations + Ceramic Dynamic Storage

This repository shows how Ceramic can be used together with Verax on-chain attestations as an efficient storage mechanism for off-chain metadata. This brief codebase is intended to be an extension to the Verax tutorial entitled From Schema to Attestation.

But First - What’s the Reasoning?

Ceramic is a decentralized data network that offers strong provenance and censorship-resistant qualities one would expect from a blockchain, with the cost efficiency, scalability, and querying capabilities one would compare to a traditional database. Users of applications built on Ceramic authenticate themselves using a familiar “Sign in With Ethereum” flow, and data those users create are forever solely controlled by the user accounts that created them. As a protocol, Ceramic can be characterized as a multi-directional replication layer that enables multi-master, eventually consistent databases to be built on top (such as ComposeDB).

What the Relation to On-Chain Attestations?

In an on-chain attestation context, it may be the case that an individual or entity would want to attest to data that is inherently dynamic and subject to change. For example, within the realm of identity, an individual is likely to take on different roles, responsibilities, personas, and other self-defining characteristics over time. While that person could attest to each change in their identity individually in the form of separate on-chain attestations, this would not only incur cost inefficiencies from transaction fees over time, but would also make it incredibly difficult and inefficient for data consumers (applications, for example) to query and leverage in a meaningful way.

Why Would Ceramic Make Sense?

In Ceramic, data is organized into append-only event logs, with each new data event representing a change in that data stream’s state. When building with ComposeDB on Ceramic, developers can define schemas (usable by any participant node in the network) that define the specific requirements of any stream’s scalars that wish to implement that schema, including restrictions on how many schema model instances a given user can have, what subfields must be included, and so on. ComposeDB also uses GraphQL to offer developers a familiar, easy-to-use querying layer.

Finally, after the genesis commit (or initiating data event), a stream’s ID will never change, no matter how many times it’s updated by the controlling account. This provides a suitable identifier for developers who wish to point on-chain events to static off-chain identifiers.

For a more thorough dive on Ceramic’s qualities, read Data Provenance in ComposeDB.

Getting Started

This tutorial uses a simple example of a Developer Profile. Therefore, we will swap out several sections of this Verax tutorial to meet our custom use case.

Follow the instructions in the tutorial linked above (using Linea Testnet) until you reach the section labeled “3. Create a Schema” (you can do so in a separate repository, or by creating a dummy file in this repository to follow the steps).

The Verax schema we will be using for this tutorial will be intended to point to a Ceramic Stream ID. Therefore, to keep things simple, we will define our Verax schema as follows:

const SCHEMA = '(string ceramicProfileStream)';

Let’s customize the metadata associated with our Verax schema as well:

const txHash = await veraxSdk.schema.create(
    "Ceramic Tutorial Schema",
    "This Schema is used for the Ceramic tutorial and uses a ceramicProfileStream string to point to the Ceramic stream representing the users profile.",
    "Ceramic",
    SCHEMA,
);

You can continue following the instructions as the Verax tutorial entails until you reach “4. Create a Portal”. We can go ahead and customize this portion as well:

const txHash = await veraxSdk.portal.deployDefaultPortal(
    [],
    "Ceramic Tutorial Portal",
    "This Portal is used for the Verax with Ceramic tutorial",
    true,
    "Verax with Ceramic Tutorial",
);

Navigate to line 156 of the profile component and replace the string you see with the transaction hash you obtained from creating your portal. This will ensure that your portal ID is pulled each time you create an attestation.

You are finally ready to start creating attestations!

Installing and Generating Ceramic Node Credentials

First, install your dependencies:

npm install

Once your dependencies are installed we will need to create a Ceramic node configuration, which will also require a node admin DID. For this application, we’re going to be running a local node configuration (for more on this, visit Running Locally in the Ceramic docs).

While we won’t go into detail around all the options available to you in this tutorial, the main takeaway is that our server configuration dictates setting such as which SQL database our indexing protocol will use, whether we want our IPFS service to run separately or bundled, and other details like that.

To keep things simple, we’ve created a script for you to use in order to create your configuration and admin DID. Simply run the following in your terminal:

npm run generate

You should now see an admin seed and settings appear in both your admin_seed.txt and composedb.config.json files.

Finally, let’s make sure we’re running the correct version of node:

nvm use 20

Observe Our Data Models

As mentioned above, we will be using the paradigm of a developer profile for this tutorial. As such, we’ve defined a very simple ComposeDB schema to meet this requirement:

enum Proficiency {
  Beginner
  Intermediate
  Advanced
  Expert
}

type Language {
  JavaScript: Proficiency
  Python: Proficiency
  Rust: Proficiency
  Java: Proficiency
  Swift: Proficiency
  Go: Proficiency
  Cpp: Proficiency
  Scala: Proficiency
  WebAssembly: Proficiency
  Solidity: Proficiency
  Other: Proficiency
}

type DeveloperProfile
  @createModel(accountRelation: SINGLE, description: "A developer profile") {
  developer: DID! @documentAccount
  name: String! @string(maxLength: 100) 
  languages: Language!
  description: String! @string(maxLength: 100000)
  projects: [String] @string(maxLength: 100) @list(maxLength: 10000)
}

Important things to note here:

  • The accountRelation: SINGLE ensures that any given authenticated user can only ever create 1 instance of this data type (whereas accountRelation: LIST would allow users to create as many as they wanted)
  • The ! next to the scalar definition defines those fields as required. Therefore, a user could create a DeveloperProfile instance without any projects, if they wanted to

While this is not a particularly complex schema, we’ll include several additional tutorials at the end of this document you can explore that introduce additional complexity and cool features.

Finally, we’ve create a script for you that deploys this definition onto your local node for you during the start-up sequence, and writes the composite runtimes to our generated folder that we’ll use later to access our schema definitions. When running in production, your application would instead be using a live node endpoint that has already endured this model deployment sequence, and therefore, the only artifact needed by your production app would be the endpoint and the canonical .js composite runtime definition.

Observe Our Authentication Methodology

Users write data to Ceramic in the form of user sessions derived from a “Sign in With Ethereum” flow. This action allows applications temporary access (default is a 24-hour period) to edit Ceramic data on the authenticated user’s behalf, and also defines limited scope solely for the models used for that application. Our application will authenticate the user on our local node, and grant us access to create and edit the DeveloperProfile data on the user’s behalf.

If you look into the fragments index file, you can see this in action. More specifically, on line 47, you can see how we’re creating a session with the limited scope over compose.resources (taken from our imported runtime definition).

Start Up Your Application

We’re finally ready to start up the application! In your terminal (running node v20), run the following:

npm run dev

During the authentication sequence, you will see a special message that looks similar to this:

After authenticating yourself (you will be prompted to move to Linea Testnet, by the way), you should see the following in your browser:

Fill in some profile information including your name, programming languages + corresponding expertise, a description of yourself, and a list of projects you’re associated with.

Once ready, you can go ahead and submit your profile. You can also follow along in the profile component, starting with the invocation of createCeramicDoc on line 64. You’ll notice that were using our newly defined state variables in our ComposeDB mutation query. If our mutation is successful, we obtain the Ceramic Stream ID from the document we just created/mutated, and move on to createVeraxAttestation on line 141.

Since you’ve already edited line 156 to align with your portal’s creation hash, your portalAddress on line 161 will be defined as your portal’s unique contract address. More importantly, notice how we’re using the id (Ceramic Stream ID) when defining our attestationData on line 171, thus creating a link between our immutable on-chain attestation and our static Ceramic stream identifier.

Back in your UI, you should now be able to access two new buttons - one will show you your stream’s raw data read directly from your local node endpoint, and the other will take you to Linea Testnet Scan where you can observe the on-chain transaction result (this link may take a few moments to work).

Next Steps

Now that you’ve learned how to create an on-chain attestation to data on Ceramic that you can continue to mutate without needing to create a new attestation, try altering the code in the profile component to remove the call to create a new on-chain attestation (you can simply comment out lines 135-137). Now, when you make changes to your profile, your data lineage will be preserved and accessible by the static stream ID you recorded when creating your on-chain attestation.

2 Likes

Hey, great to hear from Ceramic, Mark.
I have a question. One of the screenshots shows permission being asked from the user to share data with an application. Is the data stored encrypted such that only the owner can decrypt (and share)?

However i was not able to acertain the private nature of the storage using this post and the ceramic.network website.

Could you point in the right direction?

Hey,
Rubyscore.io team is online :slight_smile:

What are the main advantages of using this distributed storage technology over the widely adopted IPFS? With IPFS, it is also possible to ensure data immutability through data hashes and establish a mechanism for updates, making the entire update history transparent through blockchain transaction records.
Considering that data updates may occur frequently, in your view, how much more efficient and beneficial would this approach be for end-users compared to other alternatives?

Additionally, we are aware that Gitcoin Passport supports the Ceramic network and has encountered some performance issues during their attestations. We need to ensure that these issues are not related to the infrastructure itself. Do you have any ideas on this?

Hi, @madhavanmalolan - thanks so much for your question!

The question of how data privacy works in Ceramic is one our team receives often. Data in Ceramic is public by default (and, at the time of writing this, there is no native encryption layer built into the protocol), so many developers layer on encryption services that allow users to perform selective disclosures, set access control conditions, etc. Lit Protocol’s access control module seems to resonate with many teams building on Ceramic.

On that note - here are two different examples of how to enable encryption on ComposeDB (both of which I wrote):

Encrypted Data on ComposeDB
Lit with ComposeDB

Hello @Dmihas ! Also great questions.

At the time of writing this, Ceramic’s core underlying data structure (self-certifying event log) relies on IPLD for hash-linked data and cryptographic proofs to define an immutable append-only log. Each Ceramic node deploys with a go-ipfs node, so Ceramic leans into several IPFS services for the data lineage and transparency benefits you mentioned. IPFS in its raw form is also notoriously difficult to work with, so Ceramic layers on organizational structure, networking capabilities, and blockchain timestamping to reduce this friction (and open the possibility for database implementations like ComposeDB to be built on top).

Gitcoin is actually in the process of moving over to ComposeDB at the moment and has played an important role in our ecosystem by helping define interoperable schema definitions for VCs and attestations. My understanding of the more recent issues they were experiencing was broadly due to anchor requests failing related to an error the uint8arrays lib introduced - happy to connect you with others on the team who can share further details, but this was not related to performance.

Since we’re on this topic, might be worth sharing a network spec currently being integrated called recon that will alter how stream events in Ceramic are synchronized.

1 Like

We’ve been closely partnering with the Ceramic team to help improve the network given our high volume. As @zkras mentioned we just upgraded to ComposeDB and we ran into problems with the new unit8arrays lib in logging in.

I’ve been happy with the partnership and we like:

  • the ability to provide all data offchain as well for long term storage in a gas free way for users
  • the user control of their data (e.g they give us permission to write the data on their behalf, but we can’t update/change it without their permission)

It’s a good compliment for onchain through Verax or other services

2 Likes

Thanks for the outside view, it’s good to see that the problem is solved and the system works as it should.
We will take a closer look at Ceramic’s implementation. But even at first glance, we think it can bring more value to the whole community.

2 Likes