Storing timestamps for course

Hello I am plannig to build a de-centralised app where people could buy courses, and get the NFT on completion. I want to store the timestamps and when that certain threshold hits lit actions would mint the NFT. The issue is bugging me is that, wouldn’t the student be able to maliciously change the timestamps? I came up with a solution which I would like to discuss the security aspect of the solution with the community.

The solution I have:

  1. I want the course owner’s PKP to perform the mutations in composeDB with the timestamps of student view time of each lecture so that student won’t be able to mark the course as completed.

How would I be able to perform mutations on behalf of course owner when the student is logged in?
Please help

Hello @ 0x74c7b157af4E5418F0!

Thanks so much for sharing a bit of context around the plans for your app and specific use-case. Just to make sure I understand, when you’re referring to the course owner, is this the same as the student who purchased the course, or are you referring to the author/creator of the course? Want to make sure I get a full view of how many parties might be involved within this model.

Additionally, do you have any draft models/composites to share related to #1 above at the moment?

Looking forward to helping you validate or create a working solution!

1 Like

Basically suppose user1 creates a course Blockchain Basics. Now what happens user2 buys the course. user2 watches the video through my app. Now the data generated is added to composeDB. What I actually want is that user2 can showcase the progess/completion wherever they like. Now making sure that user2 doesn’t tamper it.

How I wrote models. PS I am beginner in graphQL so try to go with variable names not, coz I don’t gave clear idea of @documentAccount & @accountReference.

type CourseDetails @createModel (accountRelation: LIST, description: "Course Details"){
    courseCreator: DID! @documentAccount
    courseName: String! @string(minLength: 3, maxLength: 50)
    courseCode: String! @string(minLength: 1, maxLength: 50)
    videoLecture: [CID] @list(maxLength: 200)
    version: CommitID! @documentVersion
}

type CourseDetails @loadModel(id: "kjzl6hvfrbw6c7w04b58g8aak64h8tu90ucwekp0a5oz52wu0hx0n25m1tqlvib"){
    id: ID!
}

type TimeStamps @createModel(accountRelation: LIST, description: " "){
    CourseDetailsID: StreamID! @documentReference(model: "CourseDetails")
    coursedetails: CourseDetails! @relationDocument(property: "CourseDetailsID")
    timestampCreator: DID @documentAccount
    timestampFor: DID @accountReference
    timestamp: [INT] @list (maxLength: 200)
}

I want to generate the timestamps for user2 on behalf of user1 or any user so that user2 doesn’t have edit access. Can LIT protocol help?

Can CommitID help if yes how?

Also is it possible to view the data after updation even after using options: {replace: true} now is it possible for anyone to see what was data before it?

1 Like

Thanks so much for sharing your initial models, and especially for the example user flow you provided. This helps a lot.

Regarding @documentAccount vs. @accountReference, @accountReference is a directive that makes an account field queryable in instances where the account field is referencing an account other than the data’s author. I can see that you therefore are using it correctly within the timestampFor field. @documentAccount on the other hand is used to allow for the data’s author field to be queryable (in your case, the timestampCreator).

Regarding your question about ensuring user2 doesn’t have edit access to timestamps, one option is for your application to be the controller of each instance of the TimeStamps model. If your application is serving as the platform where user2 watches the video, this might make more sense anyway, especially if you want to incorporate automation around timestamp writes.

For example:

type TimeStamps @createModel(accountRelation: LIST, description: " "){
    CourseDetailsID: StreamID! @documentReference(model: "CourseDetails")
    coursedetails: CourseDetails! @relationDocument(property: "CourseDetailsID")
    courseCreator: DID @accountReference
    timestampFor: DID @accountReference
    timestamp: [DateTime] @list (maxLength: 200)
}

You’ll also notice that I changed the scalar for the “timestamp” field above. If you want to see a list of supported scalars, here is the official reference.

As for your final question - the short answer is yes. Since each data model represents a stream and once deployed they are automatically indexed, you can always use the Commits API to access all of the commits of a given stream, regardless of whether you use CommitID or not. Since Ceramic is a public network, anyone would be able to view each version of the stream (unless you choose to encrypt). CommitID will make your models easy to query by identifying a specific version of the stream.

1 Like

@mzk Thank you for the response

The idea seems nice but how can I achieve this? I am using NextJS for the frontend bit. If you could elaborate on this a little more, would help me a lot. The part I am confused is if the user2 is signed in how to make app write the data

Thank you

Great question! We have a client-side library called did-session that’s designed to do exactly what you’re describing - authenticate users using sign in with ethereum, and author data on their behalf.

If you’d like to view an example of how to do this, I’d definitely recommend the Create Ceramic App starter repository (also uses NextJS). You’ll see the user flow within the index.tsx module.

Please let me know if there’s anything else I can help with!

1 Like

I get what you are trying to say, but issue is through this user2 generate did:key by signing into the metamask. Since user2 is signing the message wouldn’t he be allowed to change the message.

Thank you for the quick response.

Also by the way I am referencing the react app created by the community. Saved me from writing a lot of boilerplate code

Ah - I think I better understand the disconnect. When I suggested this as the new TimeStamps data model:

type TimeStamps @createModel(accountRelation: LIST, description: " "){
    CourseDetailsID: StreamID! @documentReference(model: "CourseDetails")
    coursedetails: CourseDetails! @relationDocument(property: "CourseDetailsID")
    courseCreator: DID @accountReference
    timestampFor: DID @accountReference
    timestamp: [DateTime] @list (maxLength: 200)
}

What I was suggesting is that your application would be the controller for each instance, therefore preventing anyone but you from writing data to them. For example, you might have automatic triggers set up in your codebase that automatically write to each instance as the relevant user is going through a course. This would still allow users to view and read data relevant to them. This issue then, however, then becomes the fact that querying and filtering by field is not yet supported, but is planned for release later this year (see the RFC).

One way around this is to create a unique hash using a combination of your DID and the DID of user2, thus generating a new DID based on both of your identities. In this case, you as the dev would still have control of each instance but could filter users by reversing the hashing I described above.

1 Like

Another thing to note is that if you want the application to be the controller of the data instead of the user, then you’ll need a backend to write the data. The client frontend can send a request to a backend web server when it wants data to be timestamped, and then the server can write the timestamp information to Ceramic using a did:key that the application controls.

Another approach that may be possible depending on the granularity needs of your timestamps is to rely on the timestamps from anchoring. Ceramic automatically “anchors” all writes by publishing a hash of the writes onto the Ethereum blockchain. This provides a upper bound timestamp based on the blockTimestamp of the associated ethereum transaction where you know the Ceramic write must have happened before the corresponding ethereum anchor transaction. The only downside of this approach is that anchors are infrequent. They are only guaranteed to happen once every 24 hours (though in practice they usually happen a bit more frequently than that). If you’re okay with that level of granularity in your timestamps though, then you can just check the anchoring timestamps and not have to do any of your own timestamping at all.

1 Like

Well @spencer @mzk Thanks a lot for your inputs.
I wrote a piece of code in API folder and was able to achieve this. I know there would be security issue of using admin_seed.txt but currently for my MVP this is a good addition to make demo :tada:

import { readFileSync } from "fs";
import { CeramicClient } from "@ceramicnetwork/http-client";
import { ComposeClient } from "@composedb/client";
import { DID } from "dids";
import { Ed25519Provider } from "key-did-provider-ed25519";
import { getResolver } from "key-did-resolver";
import { fromString } from "uint8arrays/from-string";
import { definition } from "../../../src/__generated__/definition.js";
import { RuntimeCompositeDefinition } from "@composedb/types";

const ceramic = new CeramicClient("http://localhost:7007");
const composeClient = new ComposeClient({
    ceramic: "http://localhost:7007",
    definition: definition as RuntimeCompositeDefinition,
});

export default function handler(req:any, res:any) {
    const authenticate = async () => {
        const seed = readFileSync("./admin_seed.txt");
        const key = fromString(seed, "base16");
        const did = new DID({
            resolver: getResolver(),
            provider: new Ed25519Provider(key),
        });
        await did.authenticate();
        ceramic.did = did;
        composeClient.setDID(did);
        const mutate = await composeClient.executeQuery(`
            mutation MyMutation {
                createCourseDetails(
                input: {content: {courseCode: "BCA", courseName: "Blockchain Advanced"}}
                ) {
                    document {
                        courseCode
                        courseName
                    }
                }
            }
            `
        );
        console.log(mutate)
    };

    if (req.method === 'GET') {
        async function hello (){
            await authenticate();
        }
        hello();
        res.status(200).json("YAY!")
    }

}

Awesome! Glad it helped you reach your MVP - can always improve security later on ur use environment variables