Creating nodes locally

Local nodes are used for testing and demo purposes only.

There are two ways you can create a node locally:

  • Manually: create a local directory, add the relevant node and CorDapp files, and configure them.
  • Automatically: use the Cordform or Dockerform gradle plug-ins, which automatically generate and configure a local set of nodes.

Create a local node manually

To create a local node manually, make a new directory and add the following files and sub-directories:

  • The Corda .jar artifact file, downloaded from https://r3.bintray.com/corda/net/corda/corda/ - under ../4.5/corda-4.5.jar.
  • A node configuration file with a name node.conf, configured as described in the Node configuration section.
  • A sub-directory with a name cordapps, containing any CorDapp .jar files you want the node to load.
  • An up-to-date version of the network-parameters file (see The network map), generated by the bootstrapper tool.

The remaining node files and directories will be generated at runtime. These are described in the Node folder structure section.

Use Cordform and Dockerform to create a set of local nodes automatically

Corda provides two gradle plug-ins called Cordform and Dockerform. They both allow you to run tasks that automatically generate and configure a local set of nodes for testing and demonstration purposes.

  • A Cordform task creates nodes in the build/nodes directory. The example Cordform task used in this document creates three nodes: Notary, PartyA, and PartyB, however you are free to spin up more nodes, specify what nodes you need on the network, change node names, and update node configurations.
  • Nodes deployed via Dockerform use Docker containers. A Dockerform task is similar to Cordform but it provides an extra file that enables you to easily spin up nodes using docker-compose. This creates a docker-compose file that enables you to run a single command to control the deployment of Corda nodes and databases (instead of deploying each node/database manually).

Specific requirements

  • Cordform tasks require you to deploy each Corda node and database separately.
  • Dockerform tasks require Docker to be installed on the local host.

Tasks using the Cordform plug-in

Run this example task to create the following three nodes in the build/nodes directory:

A Notary node, which:

  • Provides a validating Notary service.
  • Runs the corda-finance CorDapp.

PartyA and PartyB nodes, each of which:

  • Does not provide any services.
  • Runs the corda-finance CorDapp.
  • Has an RPC (Remote Procedure Call) user (user1), which enables you to log in the node via RPC.

All three nodes also include any CorDapps defined in the project’s source directories, even if these CorDapps are not listed in each node’s cordapps setting. As a result, if you run the deployNodes task from the template CorDapp, for example, it will automatically build and add the template CorDapp to each node.

The following example, as defined in the Kotlin CorDapp Template, shows a Cordform task called deployNodes that creates the three nodes described above: Notary, PartyA, and PartyB.

task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
    directory "./build/nodes"
    node {
        name "O=Notary,L=London,C=GB"
        // The Notary will offer a validating Notary service.
        notary = [validating : true]
        p2pPort  10002
        rpcSettings {
            port 10003
            adminPort 10023
        }
        h2Port   10004
        // Starts an internal SSH server providing a management shell on the node.
        sshdPort 2223
        // Includes the corda-finance CorDapp on our node.
        cordapps = ["$corda_release_distribution:corda-finance:$corda_release_version"]
        extraConfig = [
            // Setting the JMX reporter type.
            jmxReporterType: 'JOLOKIA',
            // Setting the H2 address.
            h2Settings: [ address: 'localhost:10030' ]
        ]
    }
    node {
        name "O=PartyA,L=London,C=GB"
        p2pPort  10005
        rpcSettings {
            port 10006
            adminPort 10026
        }
        h2Port   10008
        cordapps = ["$corda_release_distribution:corda-finance:$corda_release_version"]
        // Grants user1 all RPC permissions.
        rpcUsers = [[ user: "user1", "password": "test", "permissions": ["ALL"]]]
    }
    node {
        name "O=PartyB,L=New York,C=US"
        p2pPort  10009
        rpcSettings {
            port 10010
            adminPort 10030
        }
        h2Port   10012
        cordapps = ["$corda_release_distribution:corda-finance:$corda_release_version"]
        // Grants user1 the ability to start the MyFlow flow.
        rpcUsers = [[ user: "user1", "password": "test", "permissions": ["StartFlow.net.corda.flows.MyFlow"]]]
    }
}

The configuration values used in the example are described below.

Required configuration

  • name <string> - use this configuration option to specify the legal identity name of the Corda node. For more information, see myLegalName. For example:
name "O=PartyA,L=London,C=GB"
  • p2pAddress <string> - use this configuration option to specify the address/port the node uses for inbound communication from other nodes. For more information, see p2pAddress. Required if p2pPort is not specified. For example:
p2pAddress "example.com:10002"
  • p2pPort <integer> - use this configuration option to specify the port the node uses for inbound communication from other nodes. The assumed IP address is localhost. For more information, see p2pAddress. For example:
p2pPort 10006  // "localhost:10006"
  • rpcSettings <config> - use this configuration option to specify RPC settings for the node. For more information, see rpcSettings. For example:
rpcSettings {
  port 10006
  adminPort 10026
}

Optional configuration

  • notary <config> - use this configuration option to specify the node as a Notary node. Required> for Notary nodes. For more information, see Notary.

  • devMode <boolean> - use this configuration option to enable development mode when you set its value to true. For more information, see devMode. For example:

devMode true
  • rpcUsers <list> - use this configuration option to set the RPC users for the node. For more information, see rpcUsers. You can use arbitrary values in this configuration block - “incorrect” settings will not cause a DSL error. An example follows below:
rpcUsers = [[ user: "user1", "password": "test", "permissions": ["StartFlow.net.corda.flows.MyFlow"]]]
  • configFile <string> - use this configuration option to generate an extended node configuration. For more information, see extended node configuration. For example:
configFile = "samples/trader-demo/src/main/resources/node-b.conf"
  • sshdPort <integer> - use this configuration option to specify the port for sshd communication. For more information, ee sshd. For example:
sshd {
  port = 2222
}

You can extend the deployNodes task with more node {} blocks to generate as many nodes as necessary for your application.

To extend node configuration beyond the properties defined in the deployNodes task, use the configFile property with the file path (relative or absolute) set to an additional configuration file. This file should follow the standard Node configuration format of node.conf. The properties set there will be appended to the generated node configuration.

Alternatively, you can also add the path to the additional configuration file while running the gradle task via the -PconfigFile command-line option. However, this will result in the same configuration file being applied to all nodes.

Following on from the previous example, the PartyB node in the next example below has additional configuration options added from a file called none-b.conf:

task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
    [...]
    node {
        name "O=PartyB,L=New York,C=US"
        [...]
        // Grants user1 the ability to start the MyFlow flow.
        rpcUsers = [[ user: "user1", "password": "test", "permissions": ["StartFlow.net.corda.flows.MyFlow"]]]
        configFile = "samples/trader-demo/src/main/resources/node-b.conf"
    }
}

The drivers Cordform parameter in the node entry lists paths of the files to be copied to the drivers sub-directory of the node. To copy the same file to all nodes, define ext.drivers in the top level, and reuse it for each node by setting drivers=ext.drivers.

task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
    ext.drivers = ['lib/my_common_jar.jar']
    [...]
    node {
        name "O=PartyB,L=New York,C=US"
        [...]
        drivers = ext.drivers + ['lib/my_specific_jar.jar']
    }
}

Package namespace ownership

To configure package namespace ownership, use the optional networkParameterOverrides and packageOwnership blocks, in a similar way to how the configuration file is used by the Network Bootstrapper tool. For example:

task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
    [...]
    networkParameterOverrides {
        packageOwnership {
            "com.mypackagename" {
                keystore = "_teststore"
                keystorePassword = "MyStorePassword"
                keystoreAlias = "MyKeyAlias"
            }
        }
    }
    [...]
}

Sign CorDapp .jar files

The default Cordform behaviour is to deploy CorDapp .jar files “as built”.

  • Prior to Corda 4.0, all CorDapp .jar files were unsigned.
  • As of Corda 4.0, CorDapp .jar files created by the gradle cordapp plug-in are signed by a Corda development certificate by default.

You can use the Cordform signing entry to override and customise the signing of CorDapp .jar files. Signing a CorDapp enables its contract classes to use signature constraints instead of other types of constraints, such as Contract Constraints.

The signing task may use an external keystore, or create a new one. You can use the following parameters in the signing entry:

  • enabled - the control flag to enable the signing process. It is set to false by default. Set to true to enable signing.
  • all - if set to true (default), all CorDapps inside the cordapp sub-directory will be signed. If set to false, only the generated Cordapp will be signed.
  • options - any relevant parameters of SignJar ANT task and GenKey ANT task. By default the .jar file is signed by a Corda development key. You can specify the external keystore can be specified. The minimal list of required options is shown below. For other options, see SignJar task.
    • keystore - the path to the keystore file. The default setting is cordadevcakeys.jks. The keystore is shipped with the plug-in.
    • alias - the alias to sign under. The default value is cordaintermediateca.
    • storepass - the keystore password. The default value is cordacadevpass.
    • keypass - the private key password, if it is different from the keystore password. The default value is cordacadevkeypass.
    • storetype - the keystore type. The default value is JKS.
    • dname - the distinguished name for the entity. Only use this option when generateKeystore is set to true (see below).
    • keyalg - the method to use when generating a name-value pair. The default value is RSA because Corda does not support DSA. Only use this option when generateKeystore is set to true (see below).
  • generateKeystore - the flag to generate a keystore. The default value is false. If set to true, an “ad hoc” keystore is created and its key is used instead of the default Corda development key or any external key. The same options to specify an external keystore are used to define the newly created keystore. In addition, dname and keyalg are required. Other options are described in GenKey task. If the existing keystore is already present, the task will reuse it. However if the file is inside the build directory, then it will be deleted when the gradle clean task is run.

The example below shows the minimal set of options required to create a dummy keystore:

task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
     signing {
        enabled true
        generateKeystore true
        all false
        options {
            keystore "./build/nodes/jarSignKeystore.p12"
            alias "cordapp-signer"
            storepass "secret1!"
            storetype "PKCS12"
            dname "OU=Dummy Cordapp Distributor, O=Corda, L=London, C=GB"
            keyalg "RSA"
        }
    }
    //...

Contracts classes from signed CorDapp .jar files are checked by signature constraints by default. You can force them to be checked by zone constraints by adding contract class names to the includeWhitelist entry - the list will generate an include_whitelist.txt file used internally by the Network Bootstrapper tool. Before you add includeWhitelist to the deployNodes task, see Contract Constraints to understand the implications of using different constraint types. The snippet below configures contracts classes from the Finance CorDapp to be verified using zone constraints instead of signature constraints:

task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
    includeWhitelist = [ "net.corda.finance.contracts.asset.Cash", "net.corda.finance.contracts.asset.CommercialPaper" ]
    //...

Run the Cordform task

To create the nodes defined in the deployNodes task example above, run the following command in a command prompt or a terminal window, from the root of the project where the deployNodes task is defined:

  • Linux/macOS: ./gradlew deployNodes
  • Windows: gradlew.bat deployNodes

This command creates the nodes in the build/nodes directory. A node directory is generated for each node defined in the deployNodes task, plus a runnodes shell script (or a batch file on Windows) to run all the nodes at once for testing and development purposes. If you make any changes to your CorDapp source or deployNodes task, you will need to re-run the task to see the changes take effect.

Tasks using the Dockerform plug-in

You need both Docker and docker-compose installed and enabled to use this method. Docker CE (Community Edition) is sufficient. Please refer to Docker CE documentation and Docker Compose documentation for installation instructions for all major operating systems.

Dockerform supports the following configuration options for each node:

  • name
  • notary
  • cordapps
  • rpcUsers
  • useTestClock

You do not need to specify the node ports because every node has a separate container so no ports conflicts will occur. Every node will expose port 10003 for RPC connections. Docker will then map these to available ports on your host machine.

You should interact with each node via its shell over SSH - see the node configuration options for more information.

To enable the shell, yon need to set the sshdPort number for each node in the gradle task - this is explained in the section run the Dockerform task further below. For example:

node {
    name "O=PartyA,L=London,C=GB"
    p2pPort 10002
    rpcSettings {
        address("localhost:10003")
        adminAddress("localhost:10023")
    }
    rpcUsers = [[user: "user1", "password": "test", "permissions": ["ALL"]]]
    sshdPort 2223
}

The Docker image associated with each node can be configured in the Dockerform task. This will initialise every node in the Dockerform task with the specified Docker image. If you need nodes with different Docker images, you can edit the docker-compose.yml file with your preferred image.

Specify an external database

You can configure Dockerform to use a standalone database to test with non-H2 databases. For example, to use PostgresSQL, you need to make the following changes to your Cordapp project:

  1. Create a file called postgres.gradle in your Cordapp directory, and insert the following code block:
ext {
    postgresql_version     = '42.2.12'
    postgres_image_version = '11'
    dbUser                 = 'myuser'
    dbPassword             = 'mypassword'
    dbSchema               = 'myschema'
    dbName                 = 'mydb'
    dbPort                 = 5432
    dbHostName             = 'localhost'
    dbDockerfile           = 'Postgres_Dockerfile'
    dbInit                 = 'Postgres_init.sh'
    dbDataVolume           =  [
            hostPath      : 'data',
            containerPath : '/var/lib/postgresql/data:\${SUFFIX}',
            containerPathArgs   : [
                    SUFFIX : "rw"
            ]
    ]
    postgres = [
            dataSourceProperties: [
                    dataSourceClassName: 'org.postgresql.ds.PGSimpleDataSource',
                    dataSource: [
                            user    : dbUser,
                            password: dbPassword,
                            url     : "jdbc:postgresql://\${DBHOSTNAME}:\${DBPORT}/\${DBNAME}?currentSchema=\${DBSCHEMA}",
                            urlArgs : [
                                    DBHOSTNAME  : dbHostName,
                                    DBPORT      : dbPort,
                                    DBNAME      : dbName,
                                    DBSCHEMA    : dbSchema
                            ]
                    ]
            ],
            database: [
                    transactionIsolationLevel: 'READ_COMMITTED',
                    runMigration             : true,
                    schema                   : dbSchema
            ],
            dockerConfig: [
                    dbDockerfile    : dbDockerfile,
                    dbDockerfileArgs: [
                         DBNAME         : dbName,
                         DBSCHEMA       : dbSchema,
                         DBUSER         : dbUser,
                         DBPASSWORD     : dbPassword,
                         DBPORT         : dbPort
                    ],
                    dbUser          : dbUser,
                    dbPassword      : dbPassword,
                    dbSchema        : dbSchema,
                    dbName          : dbName,
                    dbPort          : dbPort,
                    dbHostName      : dbHostName,
                    dbDatabase      : dbName,
                    dbDataVolume    : dbDataVolume
            ]
    ]
}

apply plugin: 'net.corda.plugins.cordformation'

dependencies {
    cordaDriver "org.postgresql:postgresql:$postgresql_version"
}

def generateInitScripts = tasks.register('generateInitScripts') { Task task ->
    def initialDockerfile = file("$buildDir/$dbDockerfile")
    def initialScript = file( "$buildDir/$dbInit")
    task.inputs.properties(project['postgres'])
    task.outputs.files(initialDockerfile, initialScript)
    /*
     * Dockerfile to initialise the PostgreSQL database.
     */
    task.doLast {
        initialDockerfile.withPrintWriter('UTF-8') { writer ->
            writer << """\
# Derive from postgres image
FROM postgres:$postgres_image_version

ARG DBNAME=$dbName
ARG DBSCHEMA=$dbSchema
ARG DBUSER=$dbUser
ARG DBPASSWORD=$dbPassword
ARG DBPORT=$dbPort

ENV POSTGRES_DB=\$DBNAME
ENV POSTGRES_DB_SCHEMA=\$DBSCHEMA
ENV POSTGRES_USER=\$DBUSER
ENV POSTGRES_PASSWORD=\$DBPASSWORD
ENV PGPORT=\$DBPORT

# Copy all postgres init file to the docker entrypoint
COPY ./$dbInit /docker-entrypoint-initdb.d/$dbInit

# Allow postgres user to run init script
RUN chmod 0755 /docker-entrypoint-initdb.d/$dbInit
"""
        }

        /**
         * Append the persistence configuration if persistence is required (i.e., persistence=true)
         */
        if (project.hasProperty("dbDataVolume")) {

            initialDockerfile.withWriterAppend('UTF-8') { writer ->
                writer << """\

# Associate the volume with the host user
USER 1000:1000

# Initialise environment variable with database directory
ENV PGDATA=/var/lib/postgresql/data/pgdata
"""
            }
        }

        /*
         * A UNIX script to generate the init.sql file that
         * PostgreSQL needs. This must use UNIX line endings,
         * even when generated on Windows.
         */
        initialScript.withPrintWriter('UTF-8') { writer ->
            writer << """\
#!/usr/bin/env bash
# Postgres database initialisation script when using Docker images

dbUser=\${POSTGRES_USER:-"$dbUser"}
dbPassword=\${POSTGRES_PASSWORD:-"$dbPassword"}
dbSchema=\${POSTGRES_DB_SCHEMA:-"$dbSchema"}
dbName=\${POSTGRES_DB:-"$dbName"}

psql -v ON_ERROR_STOP=1 --username "\$dbUser"  --dbname "\$dbName" <<-EOSQL
        CREATE SCHEMA \$dbSchema;
        GRANT USAGE, CREATE ON SCHEMA \$dbSchema TO \$dbUser;
        GRANT SELECT, INSERT, UPDATE, DELETE, REFERENCES ON ALL tables IN SCHEMA \$dbSchema TO \$dbUser;
        ALTER DEFAULT privileges IN SCHEMA \$dbSchema GRANT SELECT, INSERT, UPDATE, DELETE, REFERENCES ON tables TO \$dbUser;
        GRANT USAGE, SELECT ON ALL sequences IN SCHEMA \$dbSchema TO \$dbUser;
        ALTER DEFAULT privileges IN SCHEMA \$dbSchema GRANT USAGE, SELECT ON sequences TO \$dbUser;
        ALTER ROLE \$dbUser SET search_path = \$dbSchema;
EOSQL
""".replaceAll("\r\n", "\n")
        }
        initialScript.executable = true
    }
}
  1. In the build.gradle file, add the gradle task generateInitScripts to the dependsOn list of the prepareDockerNodes task, add the dockerConfig element, and initialise it with the postgres block. An example is shown below:
task prepareDockerNodes(type: net.corda.plugins.Dockerform, dependsOn: ['jar',  'generateInitScripts']) {

    [...]

    node {
        [...]
    }

    // The postgres block from the postgres.gradle file
    dockerConfig = postgres
}

The postgres.gradle file includes the following:

  • A gradle task called generateInitScripts used to generate the Postgres Docker image files.
  • A set of variables used to initialise the Postgres Docker image.

To set up the external database, you must place the following two files in the build directory:

  • Postgres_Dockerfile - a wrapper for the base Postgres Docker image.
  • Postgres_init.sh - a shell script to initialise the database.

The Postgres_Dockerfile is referenced in the docker-compose.yml file and allows for a number of arguments for configuring the Docker image.

You can use the following configuration parameters in the postgres.gradle file:

ParameterDescription
postgresql_versionVersion of JDBC driver to connect to the database
postgres_image_versionVersion of Postgres Docker image
dbUserDatabase user
dbPasswordDatabase password
dbSchemaPostgres schema
dbNameDatabase name
dbPortDatabase port (default: 5432)
dbHostNameDatabase host (default: localhost)
dbInitInitialisation script for Postgres Docker image
dbDockerfileWrapper of base Postgres Docker image
dbDataVolumePath to database files for Postgres Docker image

To make the database files persistent across multiple docker-compose runs, you must set the dbDataVolume parameter. If this variable is commented out, the database files will be removed after every docker-compose run.

Run the Dockerform task

To run the Dockerform task, follow the steps below.

  1. Open the build.gradle file of your Cordapp project and add a new gradle task, as shown in the example below.
task prepareDockerNodes(type: net.corda.plugins.Dockerform, dependsOn: ['jar']) {
    nodeDefaults {
        cordapp project(":contracts-java")
    }
    node {
        name "O=Notary,L=London,C=GB"
        notary = [validating : false]
        p2pPort 10002
        rpcSettings {
            address("localhost:10003")
            adminAddress("localhost:10023")
        }
        projectCordapp {
            deploy = false
        }
        cordapps.clear()
        sshdPort 2222
    }
    node {
        name "O=PartyA,L=London,C=GB"
        p2pPort 10002
        rpcSettings {
            address("localhost:10003")
            adminAddress("localhost:10023")
        }
        rpcUsers = [[user: "user1", "password": "test", "permissions": ["ALL"]]]
        sshdPort 2223
    }
    node {
        name "O=PartyB,L=New York,C=US"
        p2pPort 10002
        rpcSettings {
            address("localhost:10003")
            adminAddress("localhost:10023")
        }
        rpcUsers = [[user: "user1", "password": "test", "permissions": ["ALL"]]]
        sshdPort 2224
    }

    // This property needs to be outside the node {...} elements
    dockerImage = "corda/corda-zulu-java1.8-4.4"
}
  1. To create the nodes defined in the prepareDockerNodes gradle task added in the first step, run the following command in a command prompt or a terminal window, from the root of the project where the prepareDockerNodes task is defined:
  • Linux/macOS: ./gradlew prepareDockerNodes
  • Windows: gradlew.bat prepareDockerNodes

This command creates the nodes in the build/nodes directory. A node directory is generated for each node defined in the prepareDockerNodes task. The task also creates a docker-compose.yml file in the build/nodes directory.