├── .gitignore ├── DockerfileNMS ├── DockerfileNotary ├── LICENSE ├── README.md ├── build.gradle ├── build.sh ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── nodes └── COPY-YOUR-NODES-HERE ├── settings.gradle └── src └── main ├── kotlin └── net │ └── corda │ └── network │ └── map │ ├── NetworkMapApi.kt │ ├── NetworkMapApplication.kt │ ├── SerializationEngine.kt │ ├── bootstrapping │ └── UbuntuBootstapper.kt │ ├── certificates │ └── CertificateUtils.kt │ ├── notaries │ ├── NotaryInfoLoader.kt │ └── filesystem │ │ └── FilesystemNotaryInfoLoader.kt │ ├── repository │ ├── MapNodeInfoRepository.kt │ ├── NodeInfoRepository.kt │ └── sqllite │ │ └── SqlLiteRepository.kt │ └── whitelist │ └── JarLoader.kt ├── resources ├── nodes │ └── .keep └── notaryconfigs │ ├── node.conf │ ├── node1.conf │ └── node2.conf └── shell ├── run-notary-and-nms.sh └── start.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Build output 2 | target/ 3 | out/ 4 | .idea/ 5 | .gradle/ 6 | *.class 7 | 8 | # Log file 9 | *.log 10 | 11 | # BlueJ files 12 | *.ctxt 13 | 14 | # Mobile Tools for Java (J2ME) 15 | .mtj.tmp/ 16 | 17 | # Package Files # 18 | *.jar 19 | *.war 20 | *.ear 21 | *.zip 22 | *.tar.gz 23 | *.rar 24 | 25 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 26 | hs_err_pid* 27 | 28 | # IDE 29 | .idea/ 30 | *.iml 31 | 32 | # macOS 33 | .DS_Store 34 | 35 | local.settings.json 36 | -------------------------------------------------------------------------------- /DockerfileNMS: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk-alpine 2 | VOLUME /tmp 3 | RUN apk --no-cache --update add bash 4 | RUN apk add curl 5 | 6 | RUN mkdir /opt/notaries 7 | RUN mkdir /opt/node-storage 8 | 9 | COPY --from=notary_one /opt/notaries/nodeInfo* /opt/notaries 10 | COPY --from=notary_two /opt/notaries/nodeInfo* /opt/notaries 11 | COPY --from=notary_three /opt/notaries/nodeInfo* /opt/notaries 12 | COPY --from=notary_four /opt/notaries/nodeInfo* /opt/notaries 13 | COPY --from=notary_five /opt/notaries/nodeInfo* /opt/notaries 14 | 15 | COPY start.sh start.sh 16 | RUN chmod +x start.sh 17 | EXPOSE 8080 18 | COPY app.jar app.jar 19 | RUN mkdir -p /jars 20 | ENTRYPOINT ["/start.sh"] 21 | CMD ["--minimumPlatformVersion=4"] -------------------------------------------------------------------------------- /DockerfileNotary: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk-alpine 2 | ARG NOTARY_NAME 3 | VOLUME /tmp 4 | RUN apk --no-cache --update add bash 5 | RUN apk add curl 6 | RUN mkdir -p /opt/corda 7 | COPY corda.jar /opt/notaries/corda.jar 8 | COPY node.conf /opt/notaries/node.conf 9 | RUN sed -i "s/NOTARY_NAME_PLACEHOLDER/$NOTARY_NAME/g" /opt/notaries/node.conf 10 | 11 | WORKDIR /opt/notaries 12 | RUN export NETWORK_SERVICES_URL=http://localhost PUBLIC_ADDRESS=localhost && java -jar /opt/notaries/corda.jar run-migration-scripts --core-schemas --app-schemas && java -jar /opt/notaries/corda.jar --just-generate-node-info 13 | 14 | CMD ["java", "-jar", "corda.jar", "--log-to-console"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 (the "License"); 2 | you may not use this file except in compliance with the License. 3 | You may obtain a copy of the License at 4 | 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The network map 2 | Corda uses a http protocol for sharing address/identity mappings 3 | This container aims to be a simple way of getting started with corda networks. 4 | To do this, it provides a simple non-validating notary and a network map within a single container 5 | 6 | 7 | # The doorman 8 | Corda is a _permissioned_ system, and control of which identities are allowed onto a network is provided by the doorman 9 | protocol as described [here](https://docs.corda.net/permissioning.html). 10 | 11 | This container implements provides an "Auto" accept doorman, with a trust root of the corda development certificate authority. This trust root is available at the endpoint `http:///trustroot` 12 | with a password of `trustpass`. 13 | 14 | 15 | ## Building the project 16 | 17 | * To build the network map jar use: ```./gradlew clean build``` 18 | * To build the network map jar and the docker image use: ```./gradlew clean build && ./gradlew docker``` (This must be run as two commands due to an issue with the docker plugin used //TODO - change plugin) 19 | 20 | ## Notary Config 21 | The notary must be configured to know what address to advertise itself on. 22 | This is done via a PUBLIC_ADDRESS environment variable passed to the container 23 | 24 | ## WhiteListing of contracts 25 | If your cordapp makes use of the zone whitelist, you must provide a set of jars for the container to whitelist. 26 | This is done via mounting a folder into the /jars directory. 27 | 28 | 29 | ## Platform Version 30 | The network-map is configured by default to use the platform version of the corda runtime bundled as the minimumPlatformVersion of the network. It is possible to modify this by passing ``--minimumPlatformVersion=`` to the container 31 | 32 | ### Example Run 33 | 34 | ```$xslt 35 | docker run -it -v corda/samples/bank-of-corda-demo/build/nodes/BankOfCorda/cordapps:/jars -p 8080:8080 -p 10200:10200 -e PUBLIC_ADDRESS=stefano-corda.azure.io roastario/notary-and-network-map:4.0 --minimumPlatformVersion=4 36 | ``` 37 | 38 | * the network map will be exposed on port 8080, 39 | * the notary will be exposed on port 10200 and advertised as `stefano-corda.azure.io` 40 | * the cordapps located in ``corda/samples/bank-of-corda-demo/build/nodes/BankOfCorda/cordapps`` will be used to generate a whitelist 41 | * the minimum version of the network will be set to 4 42 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.2.50' 3 | ext.typesafe_config_version = '1.3.1' 4 | 5 | 6 | repositories { 7 | mavenLocal() 8 | mavenCentral() 9 | jcenter() 10 | maven { 11 | url 'https://dl.bintray.com/kotlin/kotlin-eap/' 12 | } 13 | maven { 14 | url "https://ci-artifactory.corda.r3cev.com/artifactory/corda-releases" 15 | } 16 | maven { url 'https://jitpack.io' } 17 | } 18 | 19 | dependencies { 20 | classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version" 21 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 22 | } 23 | } 24 | 25 | plugins { 26 | id 'org.springframework.boot' version '1.5.10.RELEASE' 27 | id 'com.palantir.docker' version '0.19.2' 28 | } 29 | 30 | repositories { 31 | mavenLocal() 32 | mavenCentral() 33 | jcenter() 34 | maven { 35 | url 'https://dl.bintray.com/kotlin/kotlin-eap/' 36 | } 37 | maven { 38 | url "http://ci-artifactory.corda.r3cev.com/artifactory/corda-releases" 39 | } 40 | 41 | maven { 42 | url "http://ci-artifactory.corda.r3cev.com/artifactory/corda-dependencies" 43 | } 44 | 45 | maven { url 'https://jitpack.io' } 46 | } 47 | 48 | group = "com.roastario" 49 | version = "1.0-SNAPSHOT" 50 | 51 | apply plugin: 'kotlin' 52 | apply plugin: 'idea' 53 | apply plugin: 'java' 54 | apply plugin: 'kotlin-noarg' 55 | apply plugin: 'com.palantir.docker' 56 | 57 | ext { 58 | spring_version = '1.5.10.RELEASE' 59 | corda_release_version = '4.5.1' 60 | corda_group = 'net.corda' 61 | bouncycastle_version = '1.57' 62 | } 63 | 64 | configurations { 65 | notaryRuntime 66 | notaryRuntime.transitive = false 67 | } 68 | 69 | dependencies { 70 | // Spring boot dependencies 71 | compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: spring_version 72 | compile group: corda_group, name: 'corda-node-api', version: corda_release_version 73 | compile group: 'org.xerial', name: 'sqlite-jdbc', version: '3.34.0' 74 | compile group: 'commons-io', name: 'commons-io', version: '2.6' 75 | compile group: "com.typesafe", name: "config", version: typesafe_config_version 76 | compile group: 'io.github.classgraph', name: 'classgraph', version: '4.4.12' 77 | 78 | compile "org.bouncycastle:bcprov-jdk15on:${bouncycastle_version}" 79 | compile "org.bouncycastle:bcpkix-jdk15on:${bouncycastle_version}" 80 | 81 | testCompile "org.jetbrains.kotlin:kotlin-test" 82 | testCompile "org.jetbrains.kotlin:kotlin-test-junit" 83 | compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 84 | 85 | notaryRuntime group: corda_group, name: 'corda', version: "4.8.1" 86 | } 87 | 88 | compileKotlin { 89 | kotlinOptions { 90 | jvmTarget = "1.8" 91 | apiVersion = "1.2" 92 | languageVersion = "1.2" 93 | } 94 | } 95 | compileTestKotlin { 96 | kotlinOptions { 97 | jvmTarget = "1.8" 98 | } 99 | } 100 | 101 | compileJava { 102 | sourceCompatibility = 1.8 103 | targetCompatibility = 1.8 104 | options.encoding = 'UTF-8' 105 | } 106 | 107 | task copyCordaJar() { 108 | def cordaJarName = file(project.configurations.notaryRuntime.first()).name 109 | copy { 110 | from project.configurations.notaryRuntime.first() 111 | into "${buildDir}/docker" 112 | } 113 | file("${buildDir}/docker/${cordaJarName}").renameTo("${buildDir}/docker/corda.jar") 114 | } 115 | 116 | 117 | task copyNetworkMapJar() { 118 | def networkMapJarName = file(jar.archivePath).name 119 | copy { 120 | from jar.archivePath 121 | into "${buildDir}/docker" 122 | } 123 | file("${buildDir}/docker/${networkMapJarName}").renameTo("${buildDir}/docker/app.jar") 124 | } 125 | 126 | task copyNotaryConfigs() { 127 | copy{ 128 | from(new FileNameFinder().getFileNames(project.projectDir.absolutePath, "**/notaryconfigs/*.conf")) 129 | into "${buildDir}/docker" 130 | } 131 | } 132 | 133 | task copyShell(){ 134 | copy{ 135 | from(new FileNameFinder().getFileNames(project.projectDir.absolutePath, "**/shell/start.sh")) 136 | into "${buildDir}/docker" 137 | } 138 | } 139 | 140 | tasks.dockerPrepare.dependsOn(build, jar, copyCordaJar, copyNetworkMapJar) -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ./gradlew clean build || exit 3 | ./gradlew copyCordaJar copyNetworkMapJar copyNotaryConfigs copyShell || exit 4 | 5 | cd ./build/docker || exit 6 | docker build --build-arg NOTARY_NAME="O=BISNotary, L=Basel, C=CH" . -f ../../DockerfileNotary -t notary_one --no-cache || exit 7 | docker build --build-arg NOTARY_NAME="O=RBANotary, L=Sydney, C=AU" . -f ../../DockerfileNotary -t notary_two --no-cache || exit 8 | docker build --build-arg NOTARY_NAME="O=BNMNotary, L=KualaLumpur, C=MY" . -f ../../DockerfileNotary -t notary_three --no-cache || exit 9 | docker build --build-arg NOTARY_NAME="O=MASNotary, L=Singapore, C=SG" . -f ../../DockerfileNotary -t notary_four --no-cache || exit 10 | docker build --build-arg NOTARY_NAME="O=SARBNotary, L=Pretoria, C=ZA" . -f ../../DockerfileNotary -t notary_five --no-cache || exit 11 | docker build . -f ../../DockerfileNMS -t network_services --no-cache || exit 12 | # 13 | docker tag notary_one roastario/notary-one-fixed:7 14 | docker tag notary_two roastario/notary-two-fixed:7 15 | docker tag notary_three roastario/notary-three-fixed:7 16 | docker tag notary_four roastario/notary-four-fixed:7 17 | docker tag notary_five roastario/notary-five-fixed:7 18 | docker tag network_services roastario/network-services-fixed:7 19 | # 20 | docker push roastario/notary-one-fixed:7 21 | docker push roastario/notary-two-fixed:7 22 | docker push roastario/notary-three-fixed:7 23 | docker push roastario/notary-four-fixed:7 24 | docker push roastario/notary-five-fixed:7 25 | 26 | docker push roastario/network-services-fixed:7 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roastario/spring-boot-network-map/d0608c28163a13cb34c5de57947f304ff6cf0616/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Apr 03 13:26:14 BST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /nodes/COPY-YOUR-NODES-HERE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roastario/spring-boot-network-map/d0608c28163a13cb34c5de57947f304ff6cf0616/nodes/COPY-YOUR-NODES-HERE -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/main/kotlin/net/corda/network/map/NetworkMapApi.kt: -------------------------------------------------------------------------------- 1 | /* 2 | */ 3 | package net.corda.network.map 4 | 5 | import net.corda.core.crypto.SecureHash 6 | import net.corda.core.crypto.sha256 7 | import net.corda.core.internal.SignedDataWithCert 8 | import net.corda.core.internal.signWithCert 9 | import net.corda.core.node.NetworkParameters 10 | import net.corda.core.serialization.SerializedBytes 11 | import net.corda.core.serialization.deserialize 12 | import net.corda.core.serialization.serialize 13 | import net.corda.core.utilities.loggerFor 14 | import net.corda.network.map.bootstrapping.UbuntuBootstapper 15 | import net.corda.network.map.certificates.CertificateUtils 16 | import net.corda.network.map.notaries.NotaryInfoLoader 17 | import net.corda.network.map.repository.MapBacked 18 | import net.corda.network.map.repository.NodeInfoRepository 19 | import net.corda.network.map.repository.SqlLiteBacked 20 | import net.corda.network.map.whitelist.JarLoader 21 | import net.corda.nodeapi.internal.DEV_ROOT_CA 22 | import net.corda.nodeapi.internal.SignedNodeInfo 23 | import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair 24 | import net.corda.nodeapi.internal.network.NetworkMap 25 | import org.bouncycastle.jce.ECNamedCurveTable 26 | import org.bouncycastle.jce.spec.ECParameterSpec 27 | import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest 28 | import org.slf4j.Logger 29 | import org.springframework.beans.factory.annotation.Autowired 30 | import org.springframework.beans.factory.annotation.Value 31 | import org.springframework.http.HttpStatus 32 | import org.springframework.http.MediaType 33 | import org.springframework.http.ResponseEntity 34 | import org.springframework.web.bind.annotation.* 35 | import org.springframework.web.context.request.async.DeferredResult 36 | import java.io.ByteArrayOutputStream 37 | import java.math.BigInteger 38 | import java.security.KeyPair 39 | import java.security.KeyPairGenerator 40 | import java.security.SecureRandom 41 | import java.security.cert.X509Certificate 42 | import java.time.Instant 43 | import java.util.* 44 | import java.util.concurrent.ConcurrentHashMap 45 | import java.util.concurrent.Executors 46 | import java.util.concurrent.ThreadLocalRandom 47 | import java.util.concurrent.atomic.AtomicReference 48 | import java.util.zip.ZipEntry 49 | import java.util.zip.ZipOutputStream 50 | import kotlin.collections.HashMap 51 | import kotlin.collections.set 52 | import kotlin.streams.toList 53 | 54 | 55 | val rootCa = DEV_ROOT_CA 56 | 57 | /** 58 | * API for serving the Network Map over HTTP to clients. 59 | * 60 | * Note that serializationEngine must remain as an Autowired parameter, even though its not explicitly used 61 | * by this class. Its needed to initialize the serialization engines in Corda. 62 | */ 63 | @RestController 64 | class NetworkMapApi( 65 | @Autowired @SqlLiteBacked private val nodeInfoRepository: NodeInfoRepository, 66 | @Autowired private val notaryInfoLoader: NotaryInfoLoader, 67 | @Autowired private val jarLoader: JarLoader, 68 | @Autowired private val ubuntuBootstapper: UbuntuBootstapper, 69 | @Value("\${minimumPlatformVersion:1}") private val minPlatformVersion: String, 70 | @Value("\${doormanCN:BasicDoorman}") private val doormanCommonName: String, 71 | @Value("\${networkMapCN:BasicNetworkMap}") private val networkMapCommonName: String, 72 | @Suppress("unused") @Autowired private val serializationEngine: SerializationEngine 73 | ) { 74 | 75 | private val doormanCa = CertificateUtils.createDevDoormanCa(rootCa, doormanCommonName) 76 | private val networkMapCa = CertificateUtils.createDevNetworkMapCa(rootCa, networkMapCommonName) 77 | 78 | private val networkMapCert: X509Certificate = networkMapCa.certificate 79 | private val networkMapKeyPair = networkMapCa.keyPair 80 | 81 | private val doormanCert: X509Certificate = doormanCa.certificate 82 | private val doormanKeyPair = doormanCa.keyPair 83 | 84 | private val executorService = Executors.newSingleThreadExecutor() 85 | private val networkMap: AtomicReference>> = AtomicReference() 86 | private val signedNetworkParams: AtomicReference> = AtomicReference() 87 | 88 | private val trustRoot = CertificateUtils.generateTrustStore() 89 | 90 | 91 | companion object { 92 | val logger: Logger = loggerFor() 93 | 94 | fun generateKeyPair(): KeyPair { 95 | val ecSpec: ECParameterSpec = ECNamedCurveTable.getParameterSpec("prime256v1"); 96 | val generator: KeyPairGenerator = KeyPairGenerator.getInstance("ECDSA", "BC"); 97 | generator.initialize(ecSpec, SecureRandom()) 98 | return generator.generateKeyPair(); 99 | } 100 | } 101 | 102 | private val csrHolder: MutableMap = Collections.synchronizedMap(HashMap()) 103 | private val signedCSRHolder: ConcurrentHashMap = ConcurrentHashMap() 104 | 105 | init { 106 | val whiteList = jarLoader.generateWhitelist() 107 | whiteList.forEach { entry -> 108 | entry.value.forEach { 109 | logger.info("found hash: " + it + " for contractClass: " + entry.key) 110 | } 111 | } 112 | 113 | logger.info( 114 | "Starting NMS with Parameters = NetworkParameters(\n" + 115 | " minimumPlatformVersion = ${minPlatformVersion.toInt()},\n" + 116 | " notaries = ${notaryInfoLoader.load()},\n" + 117 | " maxMessageSize = 10485760 * 10,\n" + 118 | " maxTransactionSize = 10485760 * 5,\n" + 119 | " modifiedTime = Instant.MIN,\n" + 120 | " epoch = 10,\n" + 121 | " whitelistedContractImplementations = $whiteList\n" + 122 | " )" 123 | ) 124 | 125 | logger.info("using: ${networkMapCert.subjectX500Principal.name} as network map certName issued by ${networkMapCert.issuerX500Principal.name}") 126 | logger.info("using: ${doormanCert.subjectX500Principal.name} as doorman certName issued by ${networkMapCert.issuerX500Principal.name}") 127 | 128 | val networkParams = NetworkParameters( 129 | minimumPlatformVersion = minPlatformVersion.toInt(), 130 | notaries = notaryInfoLoader.load().sortedBy { it.identity.name.toString() }, 131 | maxMessageSize = 10485760 * 10, 132 | maxTransactionSize = 10485760 * 5, 133 | modifiedTime = Instant.MIN, 134 | epoch = 10, 135 | whitelistedContractImplementations = whiteList 136 | ) 137 | signedNetworkParams.set(networkParams.signWithCert(networkMapKeyPair.private, networkMapCert)) 138 | networkMap.set(buildNetworkMap()) 139 | } 140 | 141 | @RequestMapping(path = ["/ping"], method = [RequestMethod.GET]) 142 | fun ping(): ByteArray { 143 | return "OK".toByteArray() 144 | } 145 | 146 | @RequestMapping(path = ["network-map/bumpMPV"], method = [RequestMethod.GET]) 147 | fun bumpMPVInNetParams() { 148 | val currentSignedParams = this.signedNetworkParams.get() 149 | val currentParams = currentSignedParams.verified() 150 | val newParams = currentParams.copy( 151 | minimumPlatformVersion = currentParams.minimumPlatformVersion + 1, 152 | epoch = currentParams.epoch + 1, 153 | notaries = notaryInfoLoader.load().sortedBy { it.identity.name.toString() }) 154 | signedNetworkParams.set(newParams.signWithCert(networkMapKeyPair.private, networkMapCert)) 155 | networkMap.set(buildNetworkMap()) 156 | } 157 | 158 | @RequestMapping(path = ["network-map/bumpEpoch"], method = [RequestMethod.GET]) 159 | fun bumpEpoch() { 160 | val currentSignedParams = this.signedNetworkParams.get() 161 | val currentParams = currentSignedParams.verified() 162 | val newParams = currentParams.copy(epoch = currentParams.epoch + 1, notaries = notaryInfoLoader.load().sortedBy { it.identity.name.toString() }) 163 | signedNetworkParams.set(newParams.signWithCert(networkMapKeyPair.private, networkMapCert)) 164 | networkMap.set(buildNetworkMap()) 165 | } 166 | 167 | @RequestMapping(path = ["network-map/publish"], method = [RequestMethod.POST]) 168 | fun postNodeInfo(@RequestBody input: ByteArray): DeferredResult> { 169 | logger.debug("Processing network-map/publish") 170 | val deserializedSignedNodeInfo = input.deserialize() 171 | logger.info("Processing network-map/publish for " + deserializedSignedNodeInfo.verified().legalIdentities) 172 | deserializedSignedNodeInfo.verified() 173 | nodeInfoRepository.persistSignedNodeInfo(deserializedSignedNodeInfo) 174 | val result = DeferredResult>() 175 | executorService.submit { 176 | networkMap.set(buildNetworkMap()) 177 | result.setResult(ResponseEntity.ok().body("OK")) 178 | } 179 | return result 180 | } 181 | 182 | @RequestMapping(path = ["truststore", "trustStore"], method = [RequestMethod.GET]) 183 | fun getTrustStore(): ResponseEntity { 184 | return ResponseEntity.ok() 185 | .header("Content-Type", "application/octet-stream") 186 | .header("Content-Disposition", "attachment; filename=\"network-root-truststore.jks\"") 187 | .body(trustRoot) 188 | } 189 | 190 | @RequestMapping(path = ["certificate"], method = [RequestMethod.POST]) 191 | fun submitCSR(@RequestBody input: ByteArray): DeferredResult> { 192 | val csr = JcaPKCS10CertificationRequest(input) 193 | logger.info("Received A CSR: ${csr.publicKey}") 194 | val id = csr.subject.toString().toByteArray().sha256().toString() 195 | val result = DeferredResult>() 196 | if (csrHolder[id] != null) { 197 | if (csr != (csrHolder[id])){ 198 | throw IllegalStateException("Duplicated CSR for ${csr.subject}") 199 | } 200 | } 201 | executorService.submit { 202 | csrHolder[id] = csr 203 | result.setResult(ResponseEntity.ok().body(id)) 204 | } 205 | return result 206 | } 207 | 208 | @RequestMapping(path = ["/certificate/{id}"], method = [RequestMethod.GET]) 209 | fun getSignedCSR(@PathVariable("id") id: String): ResponseEntity { 210 | val csr = csrHolder[id] 211 | 212 | val issuerCert = doormanCert 213 | val issuerKeyPair = doormanKeyPair 214 | 215 | val zipBytes = csr?.let { 216 | val nodeCaCertificate = CertificateUtils.createAndSignNodeCACerts(CertificateAndKeyPair(issuerCert, issuerKeyPair), csr) 217 | val backingStream = ByteArrayOutputStream() 218 | backingStream.use { 219 | val zipOutputStream = ZipOutputStream(backingStream); 220 | zipOutputStream.use { 221 | listOf(nodeCaCertificate, doormanCert, rootCa.certificate).forEach { certificate -> 222 | zipOutputStream.putNextEntry(ZipEntry(certificate.subjectX500Principal.name)) 223 | zipOutputStream.write(certificate.encoded) 224 | zipOutputStream.closeEntry() 225 | } 226 | } 227 | } 228 | val zipBytes = backingStream.toByteArray() 229 | zipBytes 230 | } 231 | 232 | return zipBytes?.let { 233 | ResponseEntity.ok() 234 | .header("Content-Type", "application/octet-stream") 235 | .body(zipBytes) 236 | } ?: ResponseEntity.notFound().build() 237 | 238 | } 239 | 240 | @RequestMapping(path = ["network-map/node-info/{hash}"], method = [RequestMethod.GET]) 241 | fun getNodeInfo(@PathVariable("hash") input: String): ResponseEntity? { 242 | logger.info("Processing retrieval of nodeInfo for {$input}.") 243 | val foundNodeInfo = nodeInfoRepository.getSignedNodeInfo(input) 244 | return if (foundNodeInfo == null) { 245 | ResponseEntity.notFound().build() 246 | } else { 247 | return ResponseEntity.ok() 248 | .contentLength(foundNodeInfo.second.size.toLong()) 249 | .contentType(MediaType.APPLICATION_OCTET_STREAM) 250 | .body(foundNodeInfo.second) 251 | } 252 | 253 | } 254 | 255 | @RequestMapping(path = ["network-map"], method = [RequestMethod.GET]) 256 | fun getNetworkMap(): ResponseEntity { 257 | logger.debug("Processing method to obtain network map.") 258 | return if (networkMap.get() != null) { 259 | val networkMapBytes = networkMap.get().bytes 260 | ResponseEntity.ok() 261 | .contentLength(networkMapBytes.size.toLong()) 262 | .contentType(MediaType.APPLICATION_OCTET_STREAM) 263 | .header("Cache-Control", "max-age=${ThreadLocalRandom.current().nextInt(10, 30)}") 264 | .body(networkMapBytes) 265 | } else { 266 | ResponseEntity(HttpStatus.NOT_FOUND) 267 | } 268 | } 269 | 270 | @RequestMapping( 271 | method = [RequestMethod.GET], 272 | path = ["network-map/network-parameters/{hash}"], 273 | produces = [MediaType.APPLICATION_OCTET_STREAM_VALUE] 274 | ) 275 | fun getNetworkParams(@PathVariable("hash") h: String): ResponseEntity { 276 | logger.info("Processing retrieval of network params for {$h}.") 277 | return if (SecureHash.parse(h) == signedNetworkParams.get().raw.hash) { 278 | ResponseEntity.ok().header("Cache-Control", "max-age=${ThreadLocalRandom.current().nextInt(10, 30)}") 279 | .body(signedNetworkParams.get().serialize().bytes) 280 | } else { 281 | ResponseEntity.notFound().build() 282 | } 283 | } 284 | 285 | 286 | @ResponseBody 287 | @RequestMapping( 288 | method = [RequestMethod.GET], 289 | value = ["network-map/reset-persisted-nodes"], 290 | produces = [MediaType.APPLICATION_JSON_VALUE] 291 | ) 292 | fun resetPersistedNodes(): ResponseEntity { 293 | val result = nodeInfoRepository.purgeAllPersistedSignedNodeInfos() 294 | val resultMsg = "Deleted : {$result} rows." 295 | logger.info(resultMsg) 296 | return ResponseEntity(resultMsg, HttpStatus.ACCEPTED) 297 | } 298 | 299 | @ResponseBody 300 | @RequestMapping( 301 | method = [RequestMethod.GET], 302 | value = ["network-map/map-stats"], 303 | produces = [MediaType.APPLICATION_JSON_VALUE] 304 | ) 305 | fun fetchMapState(): SimpleMapState { 306 | val stats = SimpleMapState() 307 | signedNetworkParams.get().verified().notaries.forEach { 308 | stats.notaryNames.add("organisationUnit=" + it.identity.name.organisationUnit + " organisation=" + it.identity.name.organisation + " locality=" + it.identity.name.locality + " country=" + it.identity.name.country) 309 | } 310 | val allNodes = nodeInfoRepository.getAllHashes() 311 | allNodes.forEach { 312 | val pair: Pair? = nodeInfoRepository.getSignedNodeInfo(it.toString()) 313 | pair?.let { 314 | val orgName = pair.first.verified().legalIdentities[0].name.organisation 315 | stats.nodeNames.add(orgName) 316 | } 317 | } 318 | return stats 319 | } 320 | 321 | @GetMapping("install.sh") 322 | fun installScript(@RequestHeader(value = "Host") host: String): ResponseEntity = 323 | ResponseEntity(ubuntuBootstapper.installScript(host), HttpStatus.OK) 324 | 325 | class SimpleMapState { 326 | val nodeNames: MutableList = emptyList().toMutableList() 327 | val notaryNames: MutableList = emptyList().toMutableList() 328 | val notaryCount get() = notaryNames.size 329 | val nodeCount get() = nodeNames.size 330 | } 331 | 332 | private fun buildNetworkMap(): SerializedBytes> { 333 | val allNodes = nodeInfoRepository.getAllHashes() 334 | logger.info("Processing retrieval of allNodes from the db and found {${allNodes.size}}.") 335 | return NetworkMap(allNodes.toList(), signedNetworkParams.get().raw.hash, null).signWithCert( 336 | networkMapKeyPair.private, 337 | networkMapCert 338 | ).serialize() 339 | } 340 | 341 | 342 | } 343 | -------------------------------------------------------------------------------- /src/main/kotlin/net/corda/network/map/NetworkMapApplication.kt: -------------------------------------------------------------------------------- 1 | /* 2 | */ 3 | package net.corda.network.map 4 | 5 | import net.corda.network.map.certificates.CertificateUtils 6 | import org.springframework.boot.SpringApplication 7 | import org.springframework.boot.autoconfigure.SpringBootApplication 8 | import java.security.Security 9 | 10 | /** 11 | * Starts the Network Map Service. 12 | * 13 | * @author Stephen Houston (steve) on 04/04/2018. 14 | */ 15 | @SpringBootApplication(scanBasePackages = ["net.corda.network.map"]) 16 | open class NetworkMapApplication 17 | 18 | /** 19 | * Starts the Network Map Service. 20 | * 21 | * @param args Any args passed from the command line. 22 | */ 23 | fun main(args: Array) { 24 | System.setProperty("logging.level.org.springframework.web", "DEBUG") 25 | Security.insertProviderAt(CertificateUtils.provider, 1); 26 | val app = SpringApplication(NetworkMapApplication::class.java) 27 | app.isWebEnvironment = true 28 | app.run(*args) 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/main/kotlin/net/corda/network/map/SerializationEngine.kt: -------------------------------------------------------------------------------- 1 | /* 2 | */ 3 | package net.corda.network.map 4 | 5 | import net.corda.core.serialization.SerializationContext 6 | import net.corda.core.serialization.internal.SerializationEnvironment 7 | import net.corda.core.serialization.internal.nodeSerializationEnv 8 | import net.corda.serialization.internal.AMQP_P2P_CONTEXT 9 | import net.corda.serialization.internal.CordaSerializationMagic 10 | import net.corda.serialization.internal.SerializationFactoryImpl 11 | import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme 12 | import net.corda.serialization.internal.amqp.SerializerFactory 13 | import net.corda.serialization.internal.amqp.amqpMagic 14 | import org.springframework.stereotype.Component 15 | 16 | @Component 17 | class SerializationEngine { 18 | init { 19 | if (nodeSerializationEnv == null) { 20 | val classloader = this.javaClass.classLoader 21 | nodeSerializationEnv = SerializationEnvironment.with( 22 | SerializationFactoryImpl().apply { 23 | registerScheme(object : AbstractAMQPSerializationScheme(emptyList()) { 24 | override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean { 25 | return (magic == amqpMagic && target == SerializationContext.UseCase.P2P) 26 | } 27 | 28 | override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory { 29 | throw UnsupportedOperationException() 30 | } 31 | 32 | override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory { 33 | throw UnsupportedOperationException() 34 | } 35 | }) 36 | }, AMQP_P2P_CONTEXT.withClassLoader(classloader) 37 | ) 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/corda/network/map/bootstrapping/UbuntuBootstapper.kt: -------------------------------------------------------------------------------- 1 | package net.corda.network.map.bootstrapping 2 | 3 | import org.springframework.stereotype.Component 4 | 5 | @Component 6 | class UbuntuBootstapper { 7 | 8 | fun installScript(host: String): String { 9 | return """ 10 | #!/bin/sh 11 | 12 | apt-get update && apt-get install -y openjdk-8-jre sudo 13 | 14 | PUBLIC_IP=${'$'}(curl ifconfig.me) 15 | ORG=${'$'}( /opt/corda/node.conf 64 | cd /opt/corda; java -jar corda.jar initial-registration -p trustpass" 65 | 66 | echo "${'$'}CORDA_SERVICE" > /etc/systemd/system/corda.service 67 | systemctl daemon-reload 68 | sudo systemctl enable --now corda 69 | """.trimIndent() 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/corda/network/map/certificates/CertificateUtils.kt: -------------------------------------------------------------------------------- 1 | package net.corda.network.map.certificates 2 | 3 | import net.corda.core.identity.CordaX500Name 4 | import net.corda.core.internal.toX500Name 5 | import net.corda.network.map.NetworkMapApi 6 | import net.corda.nodeapi.internal.DEV_CA_TRUST_STORE_PASS 7 | import net.corda.nodeapi.internal.DEV_ROOT_CA 8 | import net.corda.nodeapi.internal.crypto.* 9 | import org.bouncycastle.asn1.x509.GeneralName 10 | import org.bouncycastle.asn1.x509.GeneralSubtree 11 | import org.bouncycastle.asn1.x509.NameConstraints 12 | import org.bouncycastle.jce.provider.BouncyCastleProvider 13 | import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder 14 | import org.bouncycastle.pkcs.PKCS10CertificationRequest 15 | import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest 16 | import java.io.ByteArrayOutputStream 17 | import java.security.cert.X509Certificate 18 | import java.time.Instant 19 | import java.time.temporal.ChronoUnit 20 | import java.util.* 21 | import javax.security.auth.x500.X500Principal 22 | 23 | class CertificateUtils { 24 | 25 | companion object { 26 | 27 | val provider = BouncyCastleProvider() 28 | 29 | fun createDevNetworkMapCa(rootCa: CertificateAndKeyPair, commonName: String = "BasicNetworkMap"): CertificateAndKeyPair { 30 | val keyPair = NetworkMapApi.generateKeyPair() 31 | val cert = X509Utilities.createCertificate( 32 | CertificateType.NETWORK_MAP, 33 | rootCa.certificate, 34 | rootCa.keyPair, 35 | X500Principal("CN=$commonName,O=R3 Ltd,L=London,C=GB"), 36 | keyPair.public 37 | ) 38 | return CertificateAndKeyPair(cert, keyPair) 39 | } 40 | 41 | fun createDevDoormanCa(rootCa: CertificateAndKeyPair, commonName: String = "BasicDoorman"): CertificateAndKeyPair { 42 | val keyPair = NetworkMapApi.generateKeyPair() 43 | val cert = X509Utilities.createCertificate( 44 | CertificateType.INTERMEDIATE_CA, 45 | rootCa.certificate, 46 | rootCa.keyPair, 47 | X500Principal("CN=$commonName,O=R3 Ltd,L=London,C=GB"), 48 | keyPair.public 49 | ) 50 | return CertificateAndKeyPair(cert, keyPair) 51 | } 52 | 53 | fun generateTrustStore(): ByteArray = ByteArrayOutputStream().apply { 54 | X509KeyStore(DEV_CA_TRUST_STORE_PASS).apply { 55 | setCertificate("cordarootca", DEV_ROOT_CA.certificate) 56 | }.internal.store(this, DEV_CA_TRUST_STORE_PASS.toCharArray()) 57 | }.toByteArray() 58 | 59 | 60 | fun createAndSignNodeCACerts(caCertAndKey: CertificateAndKeyPair, 61 | request: PKCS10CertificationRequest): X509Certificate { 62 | val jcaRequest = JcaPKCS10CertificationRequest(request) 63 | val type = CertificateType.NODE_CA 64 | val nameConstraints = NameConstraints( 65 | arrayOf(GeneralSubtree(GeneralName( 66 | GeneralName.directoryName, 67 | CordaX500Name.parse(jcaRequest.subject.toString()).copy(commonName = null).toX500Name()))), 68 | arrayOf() 69 | ) 70 | val issuerCertificate = caCertAndKey.certificate 71 | val issuerKeyPair = caCertAndKey.keyPair 72 | val validityWindow = Date.from(Instant.now()) to Date.from(Instant.now().plus(500, ChronoUnit.DAYS)) 73 | val subject = X500Principal(CordaX500Name.parse(jcaRequest.subject.toString()).toX500Name().encoded) 74 | val builder = X509Utilities.createPartialCertificate(type, issuerCertificate.subjectX500Principal, issuerKeyPair.public, subject, jcaRequest.publicKey, validityWindow, nameConstraints) 75 | val signer = JcaContentSignerBuilder("SHA256withECDSA").setProvider(provider).build(issuerKeyPair.private) 76 | val certificate = builder.build(signer).toJca() 77 | certificate.checkValidity(Date()) 78 | certificate.verify(issuerKeyPair.public) 79 | return certificate 80 | } 81 | 82 | } 83 | 84 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/corda/network/map/notaries/NotaryInfoLoader.kt: -------------------------------------------------------------------------------- 1 | /* 2 | */ 3 | package net.corda.network.map.notaries 4 | 5 | import net.corda.core.node.NotaryInfo 6 | 7 | interface NotaryInfoLoader { 8 | fun load(): List 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/corda/network/map/notaries/filesystem/FilesystemNotaryInfoLoader.kt: -------------------------------------------------------------------------------- 1 | /* 2 | */ 3 | package net.corda.network.map.notaries.filesystem 4 | 5 | import net.corda.core.identity.Party 6 | import net.corda.core.internal.readObject 7 | import net.corda.core.node.NodeInfo 8 | import net.corda.core.node.NotaryInfo 9 | import net.corda.network.map.SerializationEngine 10 | import net.corda.network.map.notaries.NotaryInfoLoader 11 | import net.corda.nodeapi.internal.SignedNodeInfo 12 | import org.apache.commons.io.FileUtils 13 | import org.apache.commons.io.filefilter.DirectoryFileFilter 14 | import org.apache.commons.io.filefilter.RegexFileFilter 15 | import org.slf4j.LoggerFactory 16 | import org.springframework.beans.factory.annotation.Autowired 17 | import org.springframework.beans.factory.annotation.Value 18 | import org.springframework.context.ApplicationContext 19 | import org.springframework.stereotype.Component 20 | 21 | 22 | @Component 23 | class FilesystemNotaryInfoLoader( 24 | @Autowired val context: ApplicationContext, 25 | @SuppressWarnings("unused") serializationEngine: SerializationEngine, 26 | @Value("\${nodesDirectoryUrl:classpath:nodes}") private val nodesDirectoryUrl: String 27 | ) : NotaryInfoLoader { 28 | 29 | override fun load(): List { 30 | val directoryToLoadFrom = context.getResource(nodesDirectoryUrl).file 31 | log.info("Started scanning nodes directory ${directoryToLoadFrom.absolutePath} for notaries nodeInfo files") 32 | val nodeInfoFiles = FileUtils.listFiles( 33 | directoryToLoadFrom, 34 | RegexFileFilter("nodeInfo-.*"), 35 | DirectoryFileFilter.DIRECTORY 36 | ) 37 | log.info("Found ${nodeInfoFiles.size} nodeInfo files") 38 | val notaryInfos = nodeInfoFiles.map { 39 | val nodeInfo = it.toPath().readObject() 40 | log.info("Found nodeInfo for notary: ${nodeInfo.verified().legalIdentities.first().name}") 41 | NotaryInfo(nodeInfo.verified().notaryIdentity(), false) 42 | } 43 | log.info("Found ${notaryInfos.size} notaries in ${directoryToLoadFrom.absolutePath}") 44 | return notaryInfos.toSet().toList() 45 | } 46 | 47 | companion object { 48 | private val log = LoggerFactory.getLogger(FilesystemNotaryInfoLoader::class.java) 49 | } 50 | 51 | } 52 | 53 | 54 | private fun NodeInfo.notaryIdentity(): Party { 55 | return when (legalIdentities.size) { 56 | 1 -> legalIdentities[0] 57 | else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenario: $this") 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /src/main/kotlin/net/corda/network/map/repository/MapNodeInfoRepository.kt: -------------------------------------------------------------------------------- 1 | package net.corda.network.map.repository 2 | 3 | import net.corda.core.crypto.SecureHash 4 | import net.corda.core.identity.CordaX500Name 5 | import net.corda.core.serialization.serialize 6 | import net.corda.nodeapi.internal.SignedNodeInfo 7 | import org.springframework.stereotype.Component 8 | import java.util.concurrent.ConcurrentHashMap 9 | import java.util.concurrent.ConcurrentMap 10 | import java.util.concurrent.locks.ReentrantReadWriteLock 11 | import kotlin.concurrent.read 12 | import kotlin.concurrent.write 13 | 14 | @Component 15 | @MapBacked 16 | class MapNodeInfoRepository : NodeInfoRepository { 17 | 18 | private val lock = ReentrantReadWriteLock() 19 | 20 | private val mapByName: ConcurrentMap = ConcurrentHashMap() 21 | private val mapByHash: ConcurrentHashMap = ConcurrentHashMap() 22 | 23 | override fun persistSignedNodeInfo(signedNodeInfo: SignedNodeInfo) { 24 | lock.write { 25 | signedNodeInfo.verified().legalIdentities.forEach { 26 | val name = it.name 27 | val nodeInfoHolder = NodeInfoHolder(signedNodeInfo.raw.hash, signedNodeInfo, signedNodeInfo.serialize().bytes) 28 | mapByName[name] = nodeInfoHolder 29 | mapByHash[nodeInfoHolder.hash] = nodeInfoHolder 30 | } 31 | } 32 | } 33 | 34 | override fun getSignedNodeInfo(hash: String): Pair? { 35 | val parsedHash = SecureHash.parse(hash) 36 | return lock.read { mapByHash[parsedHash]?.let { it.signedNodeInfo to it.byteRepresentation } } 37 | } 38 | 39 | override fun getAllHashes(): Collection { 40 | return lock.read { mapByName.values.map { it.hash } } 41 | } 42 | 43 | override fun purgeAllPersistedSignedNodeInfos(): Int { 44 | return lock.write { 45 | mapByHash.clear() 46 | mapByName.size.also { mapByName.clear() } 47 | } 48 | } 49 | } 50 | 51 | data class NodeInfoHolder(val hash: SecureHash, val signedNodeInfo: SignedNodeInfo, val byteRepresentation: ByteArray) { 52 | override fun equals(other: Any?): Boolean { 53 | if (this === other) return true 54 | if (javaClass != other?.javaClass) return false 55 | 56 | other as NodeInfoHolder 57 | 58 | if (hash != other.hash) return false 59 | if (signedNodeInfo != other.signedNodeInfo) return false 60 | if (!byteRepresentation.contentEquals(other.byteRepresentation)) return false 61 | 62 | return true 63 | } 64 | 65 | override fun hashCode(): Int { 66 | var result = hash.hashCode() 67 | result = 31 * result + signedNodeInfo.hashCode() 68 | result = 31 * result + byteRepresentation.contentHashCode() 69 | return result 70 | } 71 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/corda/network/map/repository/NodeInfoRepository.kt: -------------------------------------------------------------------------------- 1 | package net.corda.network.map.repository 2 | 3 | import net.corda.core.crypto.SecureHash 4 | import net.corda.nodeapi.internal.SignedNodeInfo 5 | import org.springframework.beans.factory.annotation.Qualifier 6 | 7 | /** 8 | * Represents a Repository for storing Node Info. 9 | */ 10 | interface NodeInfoRepository { 11 | fun persistSignedNodeInfo(signedNodeInfo: SignedNodeInfo) 12 | fun getSignedNodeInfo(hash: String): Pair? 13 | fun getAllHashes(): Collection 14 | fun purgeAllPersistedSignedNodeInfos(): Int 15 | } 16 | 17 | @Target(AnnotationTarget.CLASS, AnnotationTarget.VALUE_PARAMETER) 18 | @Qualifier 19 | annotation class MapBacked 20 | 21 | @Target(AnnotationTarget.CLASS, AnnotationTarget.VALUE_PARAMETER) 22 | @Qualifier 23 | annotation class SqlLiteBacked -------------------------------------------------------------------------------- /src/main/kotlin/net/corda/network/map/repository/sqllite/SqlLiteRepository.kt: -------------------------------------------------------------------------------- 1 | package net.corda.network.map.repository.sqllite 2 | 3 | import net.corda.core.crypto.SecureHash 4 | import net.corda.core.serialization.deserialize 5 | import net.corda.core.serialization.serialize 6 | import net.corda.network.map.repository.NodeInfoRepository 7 | import net.corda.network.map.repository.SqlLiteBacked 8 | import net.corda.nodeapi.internal.SignedNodeInfo 9 | import org.springframework.stereotype.Component 10 | import org.sqlite.SQLiteDataSource 11 | import java.sql.Connection 12 | import java.util.concurrent.locks.ReentrantReadWriteLock 13 | import kotlin.concurrent.read 14 | import kotlin.concurrent.write 15 | 16 | @Component 17 | @SqlLiteBacked 18 | class SqlLiteRepository : NodeInfoRepository { 19 | 20 | 21 | companion object { 22 | internal val globalLock: ReentrantReadWriteLock = ReentrantReadWriteLock() 23 | val jdbcUrl = "jdbc:sqlite:/opt/node-storage/nm.db:" 24 | val dataSource: SQLiteDataSource = SQLiteDataSource().also { it.url = jdbcUrl } 25 | } 26 | 27 | init { 28 | dataSource.write { 29 | it.createStatement().use { statement -> 30 | statement.execute( 31 | """ 32 | CREATE TABLE IF NOT EXISTS NODEINFOS_BY_HASH 33 | ( 34 | hash text NOT NULL PRIMARY KEY, 35 | value BLOB NOT NULL 36 | ) 37 | """.trimIndent() 38 | ) 39 | } 40 | 41 | it.createStatement().use { statement -> 42 | statement.execute( 43 | """ 44 | CREATE TABLE IF NOT EXISTS HASH_BY_X500 45 | ( 46 | x500 text NOT NULL PRIMARY KEY, 47 | hash text NOT NULL 48 | ) 49 | """.trimIndent() 50 | ) 51 | 52 | } 53 | } 54 | } 55 | 56 | 57 | fun SQLiteDataSource.read(block: (ds: Connection) -> T): T { 58 | globalLock.read { 59 | return this.connection.use(block) 60 | } 61 | } 62 | 63 | fun SQLiteDataSource.write(block: (ds: Connection) -> T): T { 64 | globalLock.write { 65 | return this.connection.use(block) 66 | } 67 | } 68 | 69 | override fun persistSignedNodeInfo(signedNodeInfo: SignedNodeInfo) { 70 | dataSource.write { 71 | it.autoCommit = false 72 | it.prepareStatement( 73 | """ 74 | INSERT INTO NODEINFOS_BY_HASH (HASH, VALUE) 75 | VALUES (?,?) 76 | ON CONFLICT(HASH) 77 | DO UPDATE SET VALUE=excluded.VALUE; 78 | """.trimIndent() 79 | ).use { statement -> 80 | statement.setString(1, signedNodeInfo.raw.hash.toString()) 81 | statement.setBytes(2, signedNodeInfo.serialize().bytes) 82 | statement.execute() 83 | } 84 | it.prepareStatement( 85 | """ 86 | INSERT INTO HASH_BY_X500 (X500, HASH) 87 | VALUES (?,?) 88 | ON CONFLICT(X500) 89 | DO UPDATE SET HASH=excluded.HASH; 90 | """.trimIndent() 91 | ).use {statement -> 92 | statement.setString(1, signedNodeInfo.verified().legalIdentities.first().name.toString()) 93 | statement.setString(2, signedNodeInfo.raw.hash.toString()) 94 | statement.execute() 95 | } 96 | it.commit() 97 | } 98 | } 99 | 100 | override fun getSignedNodeInfo(hash: String): Pair? { 101 | return dataSource.read { 102 | it.prepareStatement("SELECT hash, value FROM NODEINFOS_BY_HASH where hash = ? ").use { statement -> 103 | statement.setString(1, hash) 104 | val rs = statement.executeQuery() 105 | if (rs.next()) { 106 | val bytes = rs.getBytes(2) 107 | val signedNodeInfo = bytes.deserialize() 108 | signedNodeInfo to bytes 109 | } else { 110 | null 111 | } 112 | } 113 | } 114 | } 115 | 116 | override fun getAllHashes(): Collection { 117 | return dataSource.read { 118 | it.prepareStatement("SELECT hash FROM HASH_BY_X500").use { statement -> 119 | val resultSet = statement.executeQuery() 120 | resultSet.use { 121 | generateSequence { 122 | if (resultSet.next()) SecureHash.parse(resultSet.getString(1)) else null 123 | }.toList() 124 | } 125 | } 126 | } 127 | } 128 | 129 | override fun purgeAllPersistedSignedNodeInfos(): Int { 130 | return dataSource.write { 131 | it.createStatement().use { statement -> 132 | statement.executeUpdate("TRUNCATE TABLE NODEINFOS_BY_HASH") 133 | statement.executeUpdate("TRUNCATE TABLE HASH_BY_X500") 134 | } 135 | } 136 | } 137 | 138 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/corda/network/map/whitelist/JarLoader.kt: -------------------------------------------------------------------------------- 1 | package net.corda.network.map.whitelist 2 | 3 | import io.github.classgraph.ClassGraph 4 | import net.corda.core.contracts.Contract 5 | import net.corda.core.contracts.ContractClassName 6 | import net.corda.core.crypto.SecureHash 7 | import net.corda.core.internal.* 8 | import net.corda.core.node.services.AttachmentId 9 | import net.corda.nodeapi.internal.ContractsJar 10 | import net.corda.nodeapi.internal.coreContractClasses 11 | import org.springframework.beans.factory.annotation.Value 12 | import org.springframework.stereotype.Component 13 | import java.io.File 14 | import java.net.URLClassLoader 15 | import java.nio.file.Path 16 | import java.util.* 17 | import kotlin.streams.toList 18 | 19 | @Component() 20 | class JarLoader(@Value("\${jars.location:/jars}") jarDir: String?) { 21 | private val directory = File(jarDir).toPath() 22 | 23 | private fun loadJars(): List { 24 | return directory.ifExists { 25 | val cordappJars = directory.list { paths -> 26 | paths.filter { it.toString().endsWith(".jar") && it.fileName.toString() != "corda.jar" }.toList() 27 | } 28 | cordappJars; 29 | } ?: emptyList() 30 | } 31 | 32 | fun generateWhitelist(): Map> { 33 | 34 | val newWhiteList = loadJars().map { it -> ContractsJarFile(it) } 35 | .flatMap { jar -> (jar.scan()).map { it to jar.hash } } 36 | .toMultiMap() 37 | 38 | return (newWhiteList.keys).associateBy({ it }) { 39 | val newHashes = newWhiteList[it] ?: emptyList() 40 | newHashes.distinct() 41 | } 42 | } 43 | 44 | class ContractsJarFile(private val file: Path) : ContractsJar { 45 | override val hash: SecureHash by lazy(LazyThreadSafetyMode.NONE, file::hash) 46 | override fun scan(): List { 47 | val scanResult = ClassGraph() 48 | .enableClassInfo() 49 | // A set of a single element may look odd, but if this is removed "Path" which itself is an `Iterable` 50 | // is getting broken into pieces to scan individually, which doesn't yield desired effect. 51 | .overrideClasspath(Collections.singleton(file)) 52 | .scan() 53 | val contractClassNames = coreContractClasses 54 | .flatMap { scanResult.getClassesImplementing(it.qualifiedName) } 55 | .toSet() 56 | return URLClassLoader(arrayOf(file.toUri().toURL()), Contract::class.java.classLoader).use { cl -> 57 | contractClassNames.mapNotNull { 58 | val contractClass = cl.loadClass(it.name) 59 | // Only keep instantiable contracts 60 | if (contractClass.isConcreteClass) contractClass.name else null 61 | } 62 | } 63 | } 64 | } 65 | 66 | } 67 | 68 | fun Path.ifExists(block: (Path) -> R): R? { 69 | if (this.exists()) { 70 | return block.invoke(this); 71 | } else { 72 | return null 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/resources/nodes/.keep: -------------------------------------------------------------------------------- 1 | Put the node info config files in here for the notary nodes. Another option is to use 2 | a configuration property on starting up that specifies the directory to load the node infos from. -------------------------------------------------------------------------------- /src/main/resources/notaryconfigs/node.conf: -------------------------------------------------------------------------------- 1 | myLegalName="NOTARY_NAME_PLACEHOLDER" 2 | notary { 3 | validating=false 4 | } 5 | p2pAddress=${PUBLIC_ADDRESS}":10200" 6 | rpcSettings { 7 | address="localhost:10003" 8 | adminAddress="localhost:10004" 9 | } 10 | detectPublicIp=false 11 | rpcUsers=[] 12 | devMode=true 13 | compatibilityZoneURL=${NETWORK_SERVICES_URL} 14 | devModeOptions{ 15 | allowCompatibilityZone=true 16 | } 17 | -------------------------------------------------------------------------------- /src/main/resources/notaryconfigs/node1.conf: -------------------------------------------------------------------------------- 1 | myLegalName="O=NotaryONE Service,L=London,C=GB" 2 | notary { 3 | validating=false 4 | } 5 | p2pAddress=${PUBLIC_ADDRESS}":10200" 6 | rpcSettings { 7 | address="localhost:10003" 8 | adminAddress="localhost:10004" 9 | } 10 | detectPublicIp=false 11 | rpcUsers=[] 12 | devMode=true 13 | compatibilityZoneURL=${NETWORK_SERVICES_URL} 14 | devModeOptions{ 15 | allowCompatibilityZone=true 16 | } 17 | -------------------------------------------------------------------------------- /src/main/resources/notaryconfigs/node2.conf: -------------------------------------------------------------------------------- 1 | myLegalName="O=NotaryTWO Service,L=London,C=GB" 2 | notary { 3 | validating=false 4 | } 5 | p2pAddress=${PUBLIC_ADDRESS}":10200" 6 | rpcSettings { 7 | address="localhost:10003" 8 | adminAddress="localhost:10004" 9 | } 10 | detectPublicIp=false 11 | rpcUsers=[] 12 | devMode=true 13 | compatibilityZoneURL=${NETWORK_SERVICES_URL} 14 | devModeOptions{ 15 | allowCompatibilityZone=true 16 | } 17 | -------------------------------------------------------------------------------- /src/main/shell/run-notary-and-nms.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CORDA_JAR_NAME="corda.jar" 4 | DOORMAN_CN_TO_USE=${DOORMAN_CN:-BasicDoorman} 5 | NETWORKMAP_CN_TO_USE=${NETWORKMAP_CN:-BasicNetworkMap} 6 | 7 | rm -rf notary 8 | rm -rf spring-boot-network-map 9 | 10 | ( 11 | mkdir notary 12 | cd notary || exit 13 | 14 | if [[ ! -f ${CORDA_JAR_NAME} ]]; then 15 | wget -O ${CORDA_JAR_NAME} https://ci-artifactory.corda.r3cev.com/artifactory/corda-releases/net/corda/corda/4.5.1/corda-4.5.1.jar 16 | fi 17 | 18 | cat >node.conf <run-node.sh <