Building, deploying, and running a Corda 5 CorDapp

June 28, 2022

In this blog post we will show how to build and deploy a simple example of a Corda Distributed Application (CorDapp) for Corda 5 using a snapshot of Corda 5 from when it was first open sourced around mid May 2022.

This CorDapp will run a workflow, AKA a “flow”, that calculates the sum of two numbers and returns the result. We will show how to build and deploy the example CorDapp and then execute its calculation flow. Typically, a CorDapp would allow transactions between different parties, maintain a ledger and check transactions with smart contracts. However at the time of writing Corda 5 support of these operations is not yet complete. While most of what is covered here will continue to be broadly applicable, please expect changes to the APIs used and in some of the details of how to build, deploy, and run CorDapps. Our example CorDapp is written in Kotlin but we also support CorDapp development in Java. Please see our overview of the Corda 5 architecture in the wiki for the corda/corda-runtime-os git repository.

Example CorDapp Prerequisites

Hardware

A system meeting the following specification is suitable for developing our CorDapp, and for building and deploying a Corda cluster required to run our CorDapp:

  • Intel/AMD CPU with 8 or virtual cores / threads
  • 32GiB RAM
  • At least 30GiB disk.

These are not minimum specifications. This is what we have found to work for simple CorDapp development with snapshots of Corda 5 from around mid May 2022.

Operating Systems

The CorDapp can be built and run on Windows 10, MacOS Catalina, and Linux (Ubuntu 20.04.04), but it is anticipated that CorDapps can be developed on other versions of Linux, MacOS and Windows.

Third party development tooling

We recommend development using:

To deploy our Corda cluster in this example Kubernetes is used, you will need to have installed:

Kubernetes Installation

For instructions on how to install a local Kubernetes cluster for the example CorDapp see ”Create a kubernetes cluster” and “Install Helm” from the Local development with Kubernetes page in the corda/corda-runtime-os Wiki up to and including the Install Helm section.

Getting Corda 5

Before we can build and run our CorDapp we must have:

  • a Corda 5 cluster to run our CorDapp
  • the Corda API to supply the annotations, classes and interfaces used to develop a CorDapp
  • the “corda-cli” utility to provide tooling to package up our built CorDapp in a form that can be deployed onto the Corda 5 Cluster

The source code to build what we need is in the following GitHub repositories:

Note: The source in corda/corda-runtime-os has dependencies in the corda/corda-api and corda/corda-cli-plugin-host repositories. To allow corda/corda-runtime-os to build R3 have implemented a “composite build” in its build scripts. In order for that to work the corda/corda-runtime-os, corda/corda-api, and corda/corda-cli-plugin-host repositories must all be immediate children of the same parent directory.

As the Corda 5 code base is undergoing a lot of change a set of commits from the repositories that will work together has been given the tag BlogPost-corda-5-os-cordapp. We will need to checkout that tag for each repository before we can build anything.

Preparation Steps For The Dependant Repositories

To clone the four repositories:

  1. Change to a directory where we will clone the four repositories to.
  2. Run git clone for:
    1. corda/corda-runtime-os
    2. corda/corda-api
    3. corda/corda-cli-plugin-host
    4. corda/corda-dev-helm
  3. Then for each of the cloned repository:
    1. Change direction to the repository (and project) root directory.
    2. Run git checkout BlogPost-corda-5-os-cordapp

Note:

Within each repository corda/corda-runtime-os, corda/corda-api, corda/corda-cli-plugin-host there is a Gradle project called corda-runtime-os, corda-api and corda-cli-plugin-host respectively. The terms project and repository will be used interchangeably.

Building Corda

Before attempting to run a build in any Corda 5 repository:

  • Make sure that all the corda/corda-runtime-os, corda/corda-api and corda/corda-cli-plugin-host repositories are cloned such they are all the immediate children of the same parent directory.
  • For each repository checkout out the tag BlogPost-corda-5-os-cordapp.

Build The Corda 5 Cluster Components

A Corda Cluster is composed of a set of services. For this example we will use the services built as a set of Docker images. We will run the publishOSGiImage task to build the images, setting compositeBuild to true in order to build the dependencies in the other repositories as required.

To Build The Cluster Services:
  1. Change directory to the root directory of the corda-runtime-os repository.
  2. If you are using minikube with docker, as you will if you followed our instructions for installing Kubernetes on Linux, minikube must be started and the shell must be configured to use the correct Docker daemon

        a. Start Minikube:

minikube start

       b. Configure the shell you use to run the build commands to use the minikube Docker daemon:

eval $(minikube docker-env)

3. Ensure that the docker daemon is running. The method to do this will depend on how Docker was installled and on what OS it was installed on. For a typical installation the daemon is started automatically on booting.

4. Run the build command, for Linux or MacOS:

./gradlew clean publishOSGiImage -PcompositeBuild=true

or on Windows in a PowerShell session:

.\gradlew.bat clean publishOSGiImage -PcompositeBuild=true

Build Corda-CLI

The “corda-cli” utility implements the tooling needed to create a CPI file for our CorDapp which can be deployed on a Corda cluster.

To Build The Corda-Cli Utility:
  1. Change directory to the root directory of the corda-cli-plugin-host project.
  2. Run the build command. For Linux or MacOS:
./gradlew clean build

or for Windows in a PowerShell session:

.\gradlew.bat clean build

Build Corda API

We also need to buld and publish the Corda API objects to the maven local repository. These supply the dependencies needed by the CorDapp. This is a repository that resides on the build machine.

To Publish The Corda API Objects:
  1. Change directory to the root of the corda-api project.
  2. Run the build command. For Linux or MacOS:
./gradlew clean publishToMavenLocal

For Windows in a PowerShell session:

.\gradlew.bat clean publishToMavenLocal

Our Example CorDapp

This blog post will show CorDapp development using the IntelliJ IDEA IDE. The CorDapp will be developed in Kotlin targetting Java 11. Corda API requires us to use Java 11. We will use the Gradle build automation tool with build scripts written in Groovy.

What follows are fairly general instructions of how to configure the project in Intellij with screenshots taken of IntelliJ IDEA 2021.3.3 (Community Edition).

Create A New Project

First we need to create a new project:

  1. From the menu select: File->New->Project.
    This will open the “New Project” project dialog window.
  2. Select Gradle project on the left hand side panel (see pink 1 in the screenshot below).
  3. Select Java 11 JDK from Project JDK drop down on the right hand side of the dialog window (see pink 2 in the screenshot below).
  4. Tick the Kotlin/JVM tick box under “Additional Libraries And Frameworks”. No other tick boxes should be ticked (see pink 3 in the screenshot below).

Set Project Name

  1. Set project name to NewCorDapp (see pink 1 in the screenshot below).
  2. Set directory for the project (see pink 2 in the screenshot below).

Wait for the gradle build process which was triggered after creating the new project to complete. If you find that a src/main/kotlin directory was not created under the project root directory, you can create it by right clicking on the project name, NewCorDapp, in the “Project Tool Window” on the left hand side (see pink 1 in the screenshot below) and then select New followed by Directory (see pink 2 in the screenshot below):

Then select src/main/kotlin in the dialog box that appears and then press enter:

Add A Package To The Project For Our CorDapp Code

Next, we need to create a package for our CorDapp called net.cordapp.example.calculator . A Kotlin package is equivalent to a Java package. In the project tool window navigate to the Kotlin directory and right click on it (see pink 1 in the screenshot below) and select New followed by Package (see pink 2 in the screenshot below):

In the dialog box that appears, enter a name for your package, in this case, net.cordapp.example.calculator, and press enter:

Add “Skeleton” Source Files To The CorDapp Project

Our CorDapp code is composed of four Kotlin classes:

  • CalculatorFlow
  • InputMessage
  • OutputFormattingFlow
  • OutputMessage

We will implement these classes in four source files:

  • CalculatorFlow.kt
  • InputMessage.kt
  • OutputFormattingFlow.kt
  • OutputMessage.kt

Before any code is written we will use IntelliJ to add empty source files for us to add the class implementations in a later step. The files will be added to the net.cordapp.example.calculator package in our CorDapp project. To create the CalculatorFlow.kt file:

  1. Navigate to net.cordapp.example.calculator in the project tool window on the left hand side (see pink 1 in the screenshot below).
  2. Right-click on the package, click New and select Kotlin Class/File (see pink 2 in the screenshot below).

In the dialog box that appears select File (see pink 1 in the screenshot below) and enter CalculatorFlow.kt (see pink 2 in the screenshot below):

This will create a skeleton CalculatorFlow.kt file that we will add code to.

  • Repeat these steps to create the remaining files:
    • InputMessage.kt
    • OutputFormattingFlow.kt
    • OutputMessage.kt

By following the above instructions we should now have:

  • A new IntelliJ project
    • Configured to use Gradle with Groovy for our build scripting
    • Autogenerated build.gradle, settings.gradle and gradle.properties files
  • A new source directory src/main/kotlin/net/cordapp/example/calculator containing the following four nearly empty files:
    • CalculatorFlow.kt
    • InputMessage.kt
    • OutputFormattingFlow.kt
    • OutputMessage.kt

Populate Our Source Files With Code

By this stage will should have four empty files except for the net.cordapp.example.calcultor package declaration.

Next, we need to edit the four files so each contains the code in the following sections:

CalculatorFlow.kt

package net.cordapp.example.calculator

import net.corda.v5.application.flows.CordaInject
import net.corda.v5.application.flows.Flow
import net.corda.v5.application.flows.FlowEngine
import net.corda.v5.application.flows.StartableByRPC
import net.corda.v5.application.serialization.JsonMarshallingService
import net.corda.v5.application.serialization.parseJson
import net.corda.v5.base.annotations.Suspendable
import net.corda.v5.base.util.contextLogger

@StartableByRPC
class CalculatorFlow(private val jsonArg: String) : Flow<String> {

    private companion object {
        val log = contextLogger()
    }

    @CordaInject
    lateinit var flowEngine: FlowEngine

    @CordaInject
    lateinit var jsonMarshallingService: JsonMarshallingService

    @Suspendable
    override fun call(): String {
        log.info("Calculator starting...")
        var resultMessage = ""
        try {
            val inputs = jsonMarshallingService.parseJson<InputMessage>(jsonArg)
            val result = (inputs.a ?: 0) + (inputs.b ?: 0)
            log.info("Calculated result ${inputs.a} + ${inputs.b} = $result, formatting for response...")
            val outputFormatter = OutputFormattingFlow(result)
            resultMessage = flowEngine.subFlow(outputFormatter)
            log.error("Calculated response:  $resultMessage")
        } catch (e: Exception) {
            log.error(":( could not complete calculation of '$jsonArg' because:'${e.message}'")
        }
        log.info("Calculation completed.")

        return resultMessage
    }
}

OutputFormattingFlow.kt

package net.cordapp.example.calculator

import net.corda.v5.application.flows.CordaInject
import net.corda.v5.application.flows.Flow
import net.corda.v5.application.serialization.JsonMarshallingService
import net.corda.v5.base.util.contextLogger

class OutputFormattingFlow(private val result: Int) : Flow<String> {

    private companion object {
        val log = contextLogger()
    }

    @CordaInject
    lateinit var jsonMarshallingService: JsonMarshallingService

    override fun call(): String {
        try {
            return jsonMarshallingService.formatJson(OutputMessage(result))
        } catch (e: Exception) {
            log.warn("could not serialise result '$result'")
            throw e
        }
    }
}

InputMessage.kt

package net.cordapp.example.calculator

data class InputMessage(
    var a: Int? = null,
    var b: Int? = null
)

OutputMessage.kt

package net.cordapp.example.calculator

data class OutputMessage(
    val result: Int
)

The above code implements two flows and two data classes to hold input and output data. The main flow, CalculatorFlow, (implemented in CalculatorFlow.kt) does the following:

  • Parses the input data and extracts two integers.
  • Sums the integers together.
  • Runs a subflow, OutputFormattingFlow (implemented in OutputFormattingFlow.kt), that turns the integer result into a JSON string.
  • Returns the JSON string.

OutputFormattingFlow is not strictly necessary, but demonstrates that flows can run other flows. We call flows executed by another flow, subflows. CorDapp flows implement the Flow<T> interface. The call() member function is what is executed when we “run a flow”. Input parameters are passed in as the object constructor parameters and the call() function returns the result of the flow.

Our CorDapp also uses the following annotations:

  • @CordaInject — injects the implementations for the FlowEngine and JosnMarshallingService interfaces.
  • @Suspendable — allows the CalculatorFlow to be stopped and resumed.
  • @StartableByRPC — allows the flow to be started via an HTTP API end point.

All of these annotations, as well as the Flow<T> interface, are provided by the Corda API.

Create The Gradle Build Scripts For The CorDapp

To build a CorDapp that we can deploy, we must build each of its components as a separate CorDapp Package (CPK file). These are bundled together as one CorDapp Package Bundle (CPB file) for the CorDapp. CPK and CPB file formats are built around OSGi bundles. This ensures the isolatation of separate CorDapps when installed and executed on the same Corda 5 cluster.

Complex CorDapps would be split into components composed of related classes implementing different areas of functionaility. The simple example CorDapp shown here is composed of just one component that holds all the classes.

R3 has produced a set of Corda plugins to extend the Gradle DSL and the “build” task so that:

  • The dependencies from the Corda API are declared and processed correctly.
  • Additional CorDapp metadata can be supplied.
  • CPK files are built for each CorDapp component and bundled together to produce a CPB file with a regular gradle build task.

The Corda plugins are available from the Gradle Plugin Portal.

Next, we need to update the build.gradle, settings.gradle and gradle.properties files to contain the code in the following sections:

build.gradle

import static org.gradle.api.JavaVersion.VERSION_11

plugins {
   id 'org.jetbrains.kotlin.jvm'
 
   // (1)
   id 'net.corda.plugins.cordapp-cpb'
}

group 'org.example'
version '1.0-SNAPSHOT'

def javaVersion = VERSION_11
 
// (2)
cordapp {
    targetPlatformVersion platformVersion as Integer
    minimumPlatformVersion platformVersion as Integer
    workflow 
    {
            name "Calculator"
            versionId 1
            vendor "R3"
    }
}

// (3)
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
    kotlinOptions
    {
            allWarningsAsErrors = true
            languageVersion = '1.6'
            apiVersion = '1.6'
            jvmTarget = javaVersion
            javaParameters = true
            freeCompilerArgs += [
                "-java-parameters",
                "-Xjvm-default=all"
            ]
    }
}

// (4)
repositories {
    mavenLocal()
    mavenCentral()

    // Needed for kotlin osgi bundles for corda
    maven {
            url = "$artifactoryContextUrl/corda-dependencies"    
    }
}

// (5)
dependencies {
    // We need to use kotlin stdlib built as an OSGi Bundle
    cordaProvided 'net.corda.kotlin:kotlin-stdlib-jdk8-osgi'
    cordaProvided platform("net.corda:corda-api:$cordaApiVersion")
    cordaProvided 'net.corda:corda-base'
    cordaProvided 'net.corda:corda-application'
    cordaProvided 'org.slf4j:slf4j-api'
}

test {
    useJUnitPlatform()
}

build.gradle Notes

Some brief explanations on key parts of the above build.gradle file.

// ...

plugins {
   id 'org.jetbrains.kotlin.jvm'
 
   // (1)
   id 'net.corda.plugins.cordapp-cpb'
}

// ...

(1) Acorda-cpb plugin declaration. This will automatically bring in all the corda plugins required to build a Corda Package Bundle (CPB) for the CorDapp and supply the cordapp DSL to specify metadata for our CorDapp.

// ...

// (2)
cordapp {
    targetPlatformVersion platformVersion as Integer
    minimumPlatformVersion platformVersion as Integer
    workflow 
    {
            name "Calculator"
            versionId 1
            vendor "R3"
    }
}

// ...

(2) The cordapp section is part of the DSL provided by the corda plugins to define metadata for our CorDapp. Each component of the CorDapp would get its own cordapp section in the build.gradle file for the component’s subproject. The cordapp section contains either a workflow or contract subsection depending on the type of component. A cordapp section is required by the corda plugins to build the CorDapp. Our example CorDapp has just one component so we can implement everything in the main project and use the top level build.gradle file. The CorDapp only implements flows so its one cordapp section gets a workflow subsection. In there we set name to “Calculator” and set a vender string and a version number. Note that for the time being, targetPlatformVersion and minimumPlatformVersion are both set to platformVersion. This is set to a dummy value in the properties.gradle file for now.

import static org.gradle.api.JavaVersion.VERSION_11

// ...

def javaVersion = VERSION_11

// ... 

// (3)
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
    kotlinOptions
    {
            allWarningsAsErrors = true
            languageVersion = '1.6'
            apiVersion = '1.6'
            jvmTarget = javaVersion
            javaParameters = true
            freeCompilerArgs += [
                "-java-parameters",
                "-Xjvm-default=all"
            ]
    }
}

// ...

(3) A set of Kotlin compiler options we need to build a CorDapp.

// ...

// (4)
repositories {
    mavenLocal()
    mavenCentral()

    // Needed for kotlin osgi bundles for corda
    maven {
            url = "$artifactoryContextUrl/corda-dependencies"    
    }
}

// ...

(4) Declarations of the Maven central and Maven Local repositories. The latter holds the Corda API packages that we built locally. We must also declare the R3 Artifactory repository that has the Kotlin stdlib built as an OSGi bundle, required to build the CorDapp as a CPB package.

// ...

// (5)
dependencies {
    // We need to use kotlin stdlib built as an OSGi Bundle
    cordaProvided 'net.corda.kotlin:kotlin-stdlib-jdk8-osgi'
    cordaProvided platform("net.corda:corda-api:$cordaApiVersion")
    cordaProvided 'net.corda:corda-base'
    cordaProvided 'net.corda:corda-application'
    cordaProvided 'org.slf4j:slf4j-api'
}

// ...

(5) A cordaProvided declaration is required for anything that we use from the Corda API. We also need to declare a platform so that we pick up the correct set of dependency versions for the version of the Corda API specified. We provide our own kotlin-stdlib-jdk8-osgi bundle which is Kotlin’s kotlin-stdlib-jdk8 jar which R3 have built into a/an OSGi bundle. Kotlin’s kotlin-osgi-bundle, that R3 might have used otherwise, only includes kotlin-stdlib. This does not contain all the dependencies we need but kotlin-stdlib-jdk8 does. Note that kotlin-stdlib-jdk11 does not exist. However, even though we are targetting Java 11 a Java 11 specific kotlin-stdlib is not required.

settings.gradle

For our example CorDapp, we need to replace the autogenerated settings.gradle with the following settings.gradle file:

pluginManagement {
     // (1)
     repositories
     {
         mavenLocal()
         gradlePluginPortal()
     }
     
     // (2)
     plugins {
        id 'net.corda.plugins.cordapp-cpk' version cordaPluginsVersion
        id 'net.corda.plugins.cordapp-cpb' version cordaPluginsVersion
        id 'net.corda.cordapp.cordapp-configuration' version cordaApiVersion
        id 'org.jetbrains.kotlin.jvm' version kotlinVersion
        id 'org.jetbrains.kotlin.plugin.jpa' version kotlinVersion
        id 'org.jetbrains.kotlin.plugin.allopen' version kotlinVersion
    }
}

rootProject.name = 'NewCorDapp'

For our example CorDapp, we need to specify the following in the settings.gradle file:

(1) The repositories where the Corda plugins are found.

(2) The plugin dependencies with versions of the plugins congruent with the specified Corda plugin version, Corda API version, and Kotlin version.

gradle.properties

For our example CorDapp, we need to replace the autogenerated gradle.properties file with the following gradle.properties file:

kotlin.code.style=official

# (1)
artifactoryContextUrl=https://software.r3.com/artifactory

# (2)
cordaApiVersion=5.0.0.95-SNAPSHOT
cordaPluginsVersion=6.0.0-DevPreview

# (3)
platformVersion = 999

# (4)
kotlinVersion = 1.6.21

# (5)
kotlin.stdlib.default.dependency=false

For our example CorDapp, we need to specify the following in the gradle.properties file:

(1) The URL of R3’s artifactory repository that holds Kotlin stdlib built as an OSGi bundle.

(2) The versions of the Corda API and the Corda plugins we need to build.

(3) A temporary dummy value for platformVersion.

(4) The version of Kotlin required.

(5) Specify that we do not use default dependencies.

Build The CorDapp

Next we build the CordDapp. This means building the CPB file. Details on the CPB format and background on why we use it are in the Corda 5 Architecture Guide.

To build the CorDapp run the following command from the root directory of the CordDapp project, on Linux or MacOS:

./gradlew clean build

Or on Windows:

.\gradlew.bat clean build

These commands will build the CPB file that we need under build/libs directory in the project root directory that is build/libs/NewCorDapp-1.0-SNAPSHOT-package.cpb .

Make the CorDapp Deployable

In order to use a CorDapp CPB, bundle we need to create a Corda Package Installer (CPI). This a signed CPB with a Group Policy that provides information to Corda on the virtual network that the CorDapp(s) are to be used in. The Corda-cli.sh (for Linux and MacOS) and corda-cli.cmd (for Windows) scripts from the corda-cli-plugin-host project can create a Group Policy and CPI files. The scripts should be generated in the build/generatedScripts directory relative to the corda-cli-plugin-host project root directory. See the project documentation for more details.

To create our CPI:

If working on Linux or MacOS make sure that corda-cli.sh is executable:

  • Change directory to the project root directory of corda-cli-plugin-host
  • Run this command:
chmod +x build/generatedScripts/corda-cli.sh 
  • Create a directory called cordapp-deployfrom the parent directory of the corda-runtime-os, corda-api and corda-cli-plugin-host projects.
  • Change directory to cordapp-deploy
  • Create a keystore to hold the certificates we will need to create the CPI file from our CorDapp CPB file. Create this with the keytool utility from the JDK. On Linux or MacOS run:
keytool -genkey \
    -alias "my signing key" \
    -keystore signingkeys.pfx \
    -storepass "keystore password" \
    -dname "cn=CPI Example - My Signing Key, o=CorpOrgCorp, c=GB" \
    -keyalg RSA \
    -storetype pkcs12 \
    -validity 4000

Or on Windows PowerShell 7 run:

keytool -genkey `
    -alias "my signing key" `
    -keystore signingkeys.pfx `
    -storepass "keystore password" `
    -dname "cn=CPI Example - My Signing Key, o=CorpOrgCorp, c=GB" `
    -keyalg RSA `
    -storetype pkcs12 `
    -validity 4000

  • We need a time stamping authority to sign the CPI. For example Free Time Stamp Authority. Using a web browser download the Free Time Stamp Authority certificate cacert.pem and move the file to the cordapp-deploy directory.
  • The time stamping authority’s certificate needs to be added to our truststore so that we can use it to sign our CPI. We need to run a the keytool command. When we do it will ask whether to trust cacert.pem certificate. To continue type yes and press return. Then on Linux or MacOS run:
keytool -import \
        -alias "freetsa" \
        -keystore signingkeys.pfx \
        -storepass "keystore password" \
        -file cacert.pem

Or on Windows PowerShell 7:

keytool -import `
        -alias "freetsa" `
        -keystore signingkeys.pfx `
        -storepass "keystore password" `
        -file cacert.pem

Generate a “Group Policy” file needed for the CPI we will create. On Linux or MacOS :

../corda-cli-plugin-host/build/generatedScripts/corda-cli.sh mgm groupPolicy > MyGroupPolicy.json

Or on Windows PowerShell 7:

..\corda-cli-plugin-host\build\generatedScripts\corda-cli.cmd mgm groupPolicy > MyGroupPolicy.json

  • Create a CPI called NewCorDapp.cpi using the CPB file for our CorDapp. Where <cordapp-project-root-dir> is the path to the root directory of our CorDapp project, on Linux or MacOS run:
../corda-cli-plugin-host/build/generatedScripts/corda-cli.sh package create \
    --cpb <cordapp-project-root-dir>/build/libs/NewCorDapp-1.0-SNAPSHOT-package.cpb \
    --group-policy MyGroupPolicy.json \
    --keystore signingkeys.pfx \
    --storepass "keystore password" \
    --key "my signing key" \
    --tsa https://freetsa.org/tsr \
    --file NewCorDapp.cpi

Or on Windows PowerShell 7 run:

..\corda-cli-plugin-host\build\generatedScripts\corda-cli.cmd package create `
    --cpb <cordapp-project-root-dir>\build\libs\NewCorDapp-1.0-SNAPSHOT-package.cpb `
    --group-policy MyGroupPolicy.json `
    --keystore signingkeys.pfx `
    --storepass "keystore password" `
    --key "my signing key" `
    --tsa https://freetsa.org/tsr `
    --file NewCorDapp.cpi

This will create the CPI file incordapp-deploy called NewCorDapp.cpi that we can deploy on our Corda cluster.

Deploy a Corda cluster for the CorDapp

For the example CorDapp we will deploy our Corda cluster on a local Kubernetes cluster. If you do not already have Docker, kubernetes and helm installed on your system follow the instructions from the top of the Local development with Kubernetes page down to and including the Install Helm section. Once we have deployed our Corda Cluster we will then be able upload our CorDapp to it and run its flows.

The commands given below will deploy a cluster for a version of Corda known to with the example CorDapp. For the most up-to-date instructions on bringing up a Corda cluster on Kubernetes see the corda/corda-runtime-os wiki.

To Deploy A Cluster:

  • If you are using Windows, you must download and install PowerShell 7.
  • If you are using minikube:
    • You must have started minikube, you do not need to restart minikube if it is already started
minikube start
    • You must configure the shell you use to run the helm commands to use the Docker daemon running in minikube
eval $(minikube docker-env)
    • You must have run the build command for corda-runtime-os as descibed above in a shell configured to use the minikube Docker daemon.
  • Ensure that the docker daemon is running.
  • Create a new kubernetes name space for our Corda cluster to run in called corda. Run:
kubectl create namespace corda

Deploy what we call the corda prerequisites on our Kubernetes cluster. These include a postgres RDBMS and a small kafka cluster that the Corda cluster needs.

  • Change directory to root directory of the corda-dev-helm repository.
  • Deploy the helm charts for the prerequisites. On Linux or MacOS run:
helm repo add bitnami https://charts.bitnami.com/bitnami
helm dependency build charts/corda-dev
helm upgrade --install prereqs -n corda \
  charts/corda-dev \
  --set kafka.replicaCount=1,kafka.zookeeper.replicaCount=1 \
  --render-subchart-notes \
  --timeout 10m \
  --wait

Or on Windows PowerShell 7 run:

helm repo add bitnami https://charts.bitnami.com/bitnami
helm dependency build charts/corda-dev
helm upgrade --install prereqs -n corda `
  charts/corda-dev `
  --set kafka.replicaCount=1,kafka.zookeeper.replicaCount=1 `
  --render-subchart-notes `
  --timeout 10m `
  --wait

  • You will have to wait several minutes for the last command to finish.
  • Change directory to the root directory of the corda-runtime-os project. This should be ../corda-runtime-os if you carried out the steps above.
  • Create a values.yaml file with the contents:
imagePullPolicy: IfNotPresent
image:
  registry: corda-os-docker-dev.software.r3.com
  tag: latest-local

kafka:
  bootstrapServers: prereqs-kafka:9092

db:
  cluster:
    host: prereqs-postgresql.corda
    existingSecret: prereqs-postgresql

  • Now start the Corda 5 specific services. Note that this command can timeout sometimes. In that case rerunning should be enough to bring everything up. On Linux or MacOS run:
helm upgrade --install corda -n corda \
  charts/corda \
  --values values.yaml \
  --wait

  • Or on Windows PowerShell 7 run:
helm upgrade --install corda -n corda `
  charts/corda `
  --values values.yaml `
  --wait

Deploy and run the CorDapp (CPI)

Now we have our CPI we can deploy it to the Corda cluster and run its flows. In broad terms to do this we will need to:

  1. Upload the CPI to the cluster.
  2. Create a virtual node in the cluster
  3. Run the CPI flow.

The details for each step above is given in more detail below. For each step we will need to send commands to the Corda cluster. These are submitted via an HTTP API provided by one of the Corda workers running in the cluster. In the examples given we will issue commands with the command line utility, curl.

Note:

  • You must allow some time for the CPI to upload to complete in cluster after the upload command given below is complete.
  • Only the first CPI uploaded is accepted for the same CPI. Rebuilding the CPI or creating a CPI with a different GroupPolicy.json is not enough. Subsequent uploads seem to ‘succeed’ but do not.

Enable HTTP API Access To Cluster

  • Before we can proceed we need to forward a port to the pod providing the HTTP API. This can be done with the following command run on Linux, MacOS or Windows PowerShell 7. You might want to run this command in a separate terminal on Linux or MacOS as it generates output when running on those systems:
kubectl port-forward -n corda deploy/corda-rpc-worker 8888 &

Upload the CorDapp CPI to the cluster

  • In order to use a CPI we must first upload to it to the cluster. We set the location of the CPI file to an environment variable for convenience. On Linux or MacOS:
CPI=<path-to-CPI-file-created-earlier>/NewCorDapp.cpi

Or on Windows PowerShell 7:

$CPI="<path-to-CPI-file-created-earlier>\NewCorDapp.cpi"

  • We can upload the CPI file with a curl command. On Linux, MacOS and Windows PowerShell 7 run:
curl --insecure -u admin:admin  -s -F upload=@$CPI https://localhost:8888/api/v1/cpi/

This should return JSON that looks a bit like this:

{"id":"13bf657f-bf1a-4957-960c-6f99c944e972"}

Keep the request ID, the value for id from the JSON. On Linux or MacOS:

REQUEST_ID="13bf657f-bf1a-4957-960c-6f99c944e972"

On Window PowerShell 7

$REQUEST_ID="13bf657f-bf1a-4957-960c-6f99c944e972"

If you want to upload the CPI for a second time you will need to delete the kubernetes cluster (kubernetes delete namespace corda) and recreate following the steps from the “Deploy a Corda cluster for the CorDapp” section again.

  • Now wait for around 1 minute for the upload to finish. It is important to wait as at the time of writing there is a bug where attempting subsequent commands will never succeed if attempted too soon. The Corda 5 cluster would have to be deleted and recreated in that circumstance.
  • We need to get the CPI hash that we will use to refer to the CPI in future commands. The following command also indicates the status of the upload process (on Linux, MacOS and Windows PowerShell 7):
curl --insecure -u admin:admin  https://localhost:8888/api/v1/cpi/status/$REQUEST_ID

We should get something like this:

{"status":"OK","checksum":"C8447000638E"}

The CPI hash is returned as the value for checksum and allows us to uniquely identify the CPI we uploaded in future commands.

  • We will need the value of the checksum for following commands. Store the checksum from the JSON in an environment variable. On Linux or MacOS:
CPI_HASH="C8447000638E"

Or Windows PowerShell 7:

$CPI_HASH="C8447000638E"

Create a virtual node in the cluster

A virtual node is required to run a CorDapp. Creating a virtual node links the CPI with an identity that will run the CorDapp.

  • For this example we will use an arbitrary X.500 identity. Store an X.500 identity in an environment variable for use in later commands. On Linux or MacOS:
X500="CN=IRunCorDapps, OU=Application, O=R3, L=London, C=GB"

On Windows PowerShell 7:

$X500="CN=IRunCorDapps, OU=Application, O=R3, L=London, C=GB"
  • Now we create the virtual node. On Linux and MacOS:
curl --insecure -u admin:admin -s -d '{ "request": { "cpiFileChecksum": "'"$CPI_HASH"'", "x500Name": "'"$X500"'"  } }' https://localhost:8888/api/v1/virtualnode

Or on Windows PowerShell 7:

$PAYLOAD=@{ request = @{ cpiFileChecksum = "$CPI_HASH"; x500Name = "$X500" } }
$PAYLOAD | ConvertTo-Json | curl --insecure -u admin:admin -s -d '@-' https://localhost:8888/api/v1/virtualnode

This returns JSON that looks a bit like this (after I pretty-printed it):

{
  "x500Name":"CN=IRunCorDapps, OU=Application, O=R3, L=London, C=GB",
  "cpiId":
    {
      "cpiName":"NewCorDapp",
      "cpiVersion":"1.0-SNAPSHOT",
      "signerSummaryHash":null
    },
  "cpiFileChecksum":"C8447000638ECFE24EC3B1DDA4576B186D28014C37B268D12FE3FA8746DF69ED",
  "mgmGroupId":"ABC123",
  "holdingIdHash":"C5659EC601CE",
  "vaultDdlConnectionId":"e2c8568b-40f4-47b5-9daa-a0c9b024b648",
  "vaultDmlConnectionId":"265e6a71-c488-4454-93a6-7694fbc8bb42",
  "cryptoDdlConnectionId":"29f6e82d-7e00-4b0a-b9a7-f71f1dc968de",
  "cryptoDmlConnectionId":"f124a344-646b-41c7-adc2-3d764a9a66b0"
}

We need the value of the holding ID hash that holdingIdHash has been set to. It will identify the virtual node in following commands. Note that the holdingIdHash will almost certainly be different for each virtual node. On Linux or MacOS:

HOLDING_ID="C5659EC601CE"

Or Windows PowerShell 7:

$HOLDING_ID="C5659EC601CE"

Run the CPI Flow

We can now run a flow on the virtual node we created.

  • The following command calculates the sum of the two integers 32 and 10 using CalculatorFlow. On Linux or MacOS:
curl --insecure -u admin:admin -X PUT -d '{ "requestBody": "{ \"a\":32, \"b\":10 }" }' https://localhost:8888/api/v1/flow/$HOLDING_ID/r1/net.cordapp.example.calculator.CalculatorFlow

Or on Windows PowerShell 7:

$PAYLOAD=@{ requestBody= "`{ `"a`":32, `"b`":10 `}" }
$PAYLOAD | ConvertTo-Json | curl --insecure -u admin:admin -s -X PUT -d '@-' https://localhost:8888/api/v1/flow/$HOLDING_ID/r1/net.cordapp.example.calculator.CalculatorFlow

The command returns JSON indicating the flow status (again pretty-printed):

{
  "isExistingFlow":false,
  "flowStatus":
     {
       "holdingShortId":"C5659EC601CE",
       "clientRequestId":"r1",
       "flowId":null,
       "flowStatus":"START_REQUESTED",
       "flowResult":null,
       "flowError":null,
       "timestamp":"2022-06-17T12:32:20.366724Z"
    }
}

  • Now wait to give the cluster some time to execute the flow.
  • Poll the cluster to get the status of the flow (Linux, MacOS or Windows PowerShell 7):
curl --insecure -u admin:admin https://localhost:8888/api/v1/flow/$HOLDING_ID/r1

You might back get something like this if the flow has not completed:

{
  "holdingShortId":"C5659EC601CE",
  "clientRequestId":"r1",
  "flowId":null,
  "flowStatus":"START_REQUESTED",
  "flowResult":null,
  "flowError":null,
  "timestamp":"2022-06-24T00:22:34.216987Z"
  }

Eventually the command should return JSON where flowStatus is set to COMPLETED as shown below:

{
  "holdingShortId":"C5659EC601CE",
  "clientRequestId":"r1",
  "flowId":"c4b4702d-e1ef-4445-a465-05b09bcb8738",
  "flowStatus":"COMPLETED",
  "flowResult":"{\"result\":42}",
  "flowError":null,
  "timestamp":"2022-06-17T12:35:39.174119Z"
}

Our flow returns its result as the value for flowResult. In the JSON above this is “result :42”. The code that generates the output is in OutputFormatterFlow.kt line 17 and CalculatorFlow.kt line 26. Thankfully, the answer returned for the sum of 32 and 10 is 42.

Rerunning The Flow

Each time we start a flow we need to use a unique request number. Looking at the 2 URLs from Run the CPI flow:

  • https://localhost:8888/api/v1/flow/$HOLDING_ID/r1/net.cordapp.example.calculator.CalculatorFlow
  • https://localhost:8888/api/v1/flow/$HOLDING_ID/r1

Both have “r1” as part of the path component. To rerun the flow again you need to use r2, r3, r4 and so on. The reason why is because we can run flows concurrently and when we run a flow we make separate HTTP API calls to start flows and poll for results. So when we poll for results we need to tell the corda cluster which flow start request we want the results for.

For example to rerun the flow, on Linux or MacOS: or Windows PowerShell 7):

curl --insecure -u admin:admin -X PUT -d '{ "requestBody": "{ \"a\":15, \"b\":53 }" }' https://localhost:8888/api/v1/flow/$HOLDING_ID/r2/net.cordapp.example.calculator.CalculatorFlow

Or on Windows PowerShell 7

$PAYLOAD=@{ requestBody= "`{ `"a`":15, `"b`":53 `}" }
$PAYLOAD | ConvertTo-Json | curl --insecure -u admin:admin -s -X PUT -d '@-' https://localhost:8888/api/v1/flow/$HOLDING_ID/r2/net.cordapp.example.calculator.CalculatorFlow
  • Wait
  • Then collect results, run (on Linux, MacOS or Windows PowerShell 7):
curl --insecure -u admin:admin https://localhost:8888/api/v1/flow/$HOLDING_ID/r2

Cluster Clean Up

Deleting The Kubernetes Cluster

You can remove the corda cluster from kubernetes by deleting the corda namespace we created earlier.

Warning: this will delete everything in the Kubernetes corda namespace. On Linux, MacOS or Windows:

kubectl delete namespace corda

Removing Corda Images

To remove the Corda 5 images once used for the cluster use the docker image rm command to delete images with names prefixed with “corda-os-docker-dev.software.r3.com/corda-os-“.

Conclusion

Following the instructions above should give you a working Corda cluster, built and deployed from source, and a simple CorDapp, built and deployed to the cluster with running flows. There are lots of changes coming to Corda 5 but we hope that this article shows what we have so far and how it works. This is just the beginning.

Share: