Skip to content

Latest commit

 

History

History
239 lines (186 loc) · 9.52 KB

File metadata and controls

239 lines (186 loc) · 9.52 KB

Java GraphSDK Example

Setting up package dependencies

The GraphSDK JAR package is hosted in a GitHub Maven repository. The URL for the Maven repo is: https://maven.pkg.github.com/ProjectLibertyLabs/graph-sdk

Package imports:

import io.projectliberty.graphsdk.Configuration
import io.projectliberty.graphsdk.Graph
import io.projectliberty.graphsdk.models.*

Prerequisites

Since GraphSDK does not itself interact with the blockchain, for the following examples, we'll make use of a proposed frequencyClient, that provides an API to Frequency blockchain RPCs and Extrinsics. The structure/syntax for your own blockchain interface may be somewhat different.

Basic Social Graph Workflow

The following code examples illustrate how to implement various steps in the workflows represented here

Code Examples

Initializing graph state

Before the SDK can be used to ingest, manipulate, or export user graphs, it must be initialize with a chain configuration that specifies certain limits and chain-constant values. Note that the Graph object is the instance of the SDK itself and can hold may UserGraphs; thus it is not necessary to have more than one instance of Graph in your application.

Each type of graph is stored in its own schema on-chain, plus there is another schema to hold graph publickeys. The schema IDs are known and can be retrieved from the SDK environment config, as seen below.

Example:

    // ----- FETCH PUBLIC GRAPH FROM CHAIN ----- //
    val configuration = Configuration(Configuration.getMainnet())

    // This is how we get the schema IDs from the SDK's chain configuration
    val publicFollowSchemaId = configuration.getSchemaId(ConnectionType.FollowPublic)
    val privateFollowSchemaId = configuration.getSchemaId(ConnectionType.FollowPrivate)
    val privateFriendSchemaId = configuration.getSchemaId(ConnectionType.FriendshipPrivate)
    val graphPublicKeySchemaId = configuration.getEnvironment().getConfig().getGraphPublicKeySchemaId()


    // ----- SETUP GRAPH ----- //
    // initialize the Graph in memory
    val graph = Graph(configuration)

Working with graph encryption keys

For private graphs, it is necessary to have a user's graph encryption keys. When a new keypair is generated (new user, or key rotation), that keypair also needs to be announced to the chain. A keypair will typically be generated by a wallet at the user's request, and the public part of the keypair is then sent to be stored on-chain. Graph keys are x25519 (different from msa control keys which are sr25519 and should not get mixed).

When working with private graphs, it is also necessary to (1) have the graph owners complete graph encryption keypair, and (2) retrieve the graph owner's publickey(s) from the chain along with their chain indices so we can match them up with the corresponding privatekey.

Note that when we add a new graph key for a user, we should handle the complete key update to the chain and re-import keys from the chain before attempting to perform any other graph updates using the new keys.

  fun getUserDsnpKeyBundles(long msaId, int schemaId) {
    // Retrieve current public keys from the chain.
    val publishedKeys = frequencyClient.getItemizedStorage(msaId, publicKeySchemaId).join()

    // ----- DESERIALIZATION OF PUBLISHED ON-CHAIN KEYS ----- //
    // Deserialize Published Public Graph Keys to raw form
    val mappedKeys = publishedKeys.items.map { i ->
      KeyData.newBuilder()
        .setIndex(i.index)
        .setContent(
          // substring use is to skip the `0x` in front of the payload
          ByteString.fromHex(i.payload.substring(2)))
        .build()
    }
    val dsnpKeys = DsnpKeys.newBuilder()
      .setDsnpUserId(graphOwnerMsaId.toLong())
      .setKeysHash(publishedKeys.contentHash)
      .addAllKeys(mappedKeys)
      .build()

    return ImportBundles.newBuilder()
      .addBundles(
        ImportBundles.ImportBundle.newBuilder()
          .setDsnpUserId(msaId)
          .setSchemaId(schemaId)
          .setDsnpKeys(dsnpKeys)
          .build()
      ).build()
  }

  fun getUserGraphKeyPairBundle(
    long msaId,
    List<GraphKeyPair> currentKeyPairs {

    return ImportBundles.newBuilder()
      .addBundles(
        ImportBundles.ImportBundle.newBuilder()
          .setDsnpUserId(msaId)
          .setSchemaId(schemaId)
          .addAllKeypairs(currentKeyPairs)
          .build()
      ).build()
}

fun addNewUserGraphKeypair(long msaId, int schemaId, GraphKeyPair newKeypair)
{
    val bundles = getUserDsnpKeyBundles(msaId, schemaId));
    graph.importUserData(bundles);

    val graphActions = Actions.newBuilder().addActions(
      Actions.Action.newBuilder().setAddKeyAction(
        Actions.Action.AddGraphKey.newBuilder()
          .setOwnerDsnpUserId(msaId)
          .setNewPublicKey(newKeyPair.publicKey)
          .build()
      )
    ).build()

    // add new key to graph
    graph.applyActions(graphActions)

    // export graph updates which in this case is only the added new key
    val updates = graph.exportUpdates()

    // ----- PUBLISH TO BLOCKCHAIN ----- //
    // map exported key into chain request
    val itemizedActions: List<ItemAction> = updates.filter { u -> u.hasAddKey() }.map { update ->
      AddItemAction.from(update.addKey.payload.toByteArray())
    }

    // finally, add the new exported key to blockchain
    // the user MUST sign the payload of adding a new graph key with one of their msa control keys
    // The full data to sign is available: https://frequency-chain.github.io/frequency/pallet_stateful_storage/types/struct.ItemizedSignaturePayload.html
    val retVal =
      frequencyClient.createApplyItemActionsWithSignature(graphOwnerSigningPublicKey, signatureProof, publicKeySchemaId, keyContentHash.toBigInteger(), itemizedActions)
        .join()
  }

Fetching and preparing graph data

For all graphs (public & private), the graph data is fetched from the appropriate schema on-chain and formed into import bundles that are consumable by the SDK.

For private graphs, it is also necessary to get graph keys.

For the owner of a graph (the user whose graph we are updating), we must retrieve both the complete graph encryption keypair (typically from the wallet) and also the duplicate publickey portion from the chain (this is so that we can match the index of the publickey with the correct keypair).

For connections in a user's private friendship graph only, we must also fetch those users' graph publickeys from the chain. These are used to compute a PRId for each connection that can be used in friendship proofs without requiring both users's private graph keys.

fun getUserGraphImportBundles(
    long graphOwnerMsaId, int schemaId, Graph graph,
    /* for private graph, we'd have a variant of this function that also takes a GrapyKeypair as input */
    GraphKeypair userGraphKeypair) {
    val pages = frequencyClient.getPaginatedStorage(graphOwnerMsaId, schemaId).join()

    val rawPages = pages.map { page ->
      PageData.newBuilder()
        .setPageId(page.page_id)
        .setContentHash(page.content_hash.toInt())
        // substring use is to skip the `0x` in front of the payload
        .setContent(ByteString.fromHex(page.payload.substring(2)))
        .build()
    }

    // For Private graph only, we need the owning user's graph publickeys
    val ownerDsnpKeys = getUserDsnpKeyBundles(graphOwnerMsaId, schemaId);

    val bundles = ImportBundles.newBuilder()
      .addBundles(
        ImportBundles.ImportBundle.newBuilder()
          .setDsnpUserId(msaId)
          .setSchemaId(schemaId)
          .addAllPages(rawPages)
          // For private graph only
          .setDsnpKeys(ownerDsnpKeys)
          .build()
      ).build()

    return bundles;
}

fun updateUserGraph(
    long graphOwnerMsaId,
    int schemaId,
    List<long> connectionsToAdd
    // For Private graph only, need keys
    List<GraphKeypair> ownerKeys
    ) {
        val bundles = getUserGraphImportBundles(graphOwnerMsaId, schemaId, ownerKeys);
        graph.importUserData(bundles);

        val actionBuilder = Actions.newBuilder();
        connectionsToAdd.forEach { connectionMsaId -> {

            // Next 2 lines only needed for Private::Friendship graph
            val connectionKeyBundle = getUserDsnpKeyBundles(connectionMsaId, schemaId);
            graph.import(connectionKeyBundle);

            actionBuilder.addActions(
                Actions.Action.newBuilder().setConnectAction(
                    Actions.Action.ConnectAction.newBuilder()
                    .setOwnerDsnpUserId(graphOwnerMsaId)
                    .setConnection(
                        Connection.newBuilder().setDsnpUserId(connectionMsaId).setSchemaId(schemaId).build()
                        ).build()
                    )
            );
        }

        // add new connections to the graph
        val graphAction = actionBuilder.build();
        graph.applyActions(graphActions)

        // export graph updates which in this case is graph page update
        val updates = graph.exportUpdates()

        // ----- PUBLISH TO BLOCKCHAIN ----- //
        for (update in updates) {
        if (update.hasPersist()) {
           val persist = update.persist
            // finally, update the graph on chain with new connections
            val retVal =
                frequencyClient.createUpsertPage(
                    persist.ownerDsnpUserId,
                    persist.schemaId,
                    persist.pageId,
                    persist.prevHash.toBigInteger(),
                    persist.payload.toByteArray()
                ).join()
        }
    }