Merge branch 'release/os/4.9' into merge-release/os/4.8-release/os/4.9-2024-08-14-286

This commit is contained in:
Adel El-Beik 2024-08-14 15:48:45 +01:00 committed by GitHub
commit 8cfa851d86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
162 changed files with 2291 additions and 5038 deletions

View File

@ -13,13 +13,13 @@
* the branch name of origin branch, it should match the current branch * the branch name of origin branch, it should match the current branch
* and it acts as a fail-safe inside {@code forwardMerger} pipeline * and it acts as a fail-safe inside {@code forwardMerger} pipeline
*/ */
String originBranch = 'release/os/4.8' String originBranch = 'release/os/4.9'
/** /**
* the branch name of target branch, it should be the branch with the next version * the branch name of target branch, it should be the branch with the next version
* after the one in current branch. * after the one in current branch.
*/ */
String targetBranch = 'release/os/4.9' String targetBranch = 'release/os/4.10'
/** /**
* Forward merge any changes between #originBranch and #targetBranch * Forward merge any changes between #originBranch and #targetBranch

View File

@ -92,6 +92,7 @@ pipeline {
} }
stage('Recompile') { stage('Recompile') {
steps { steps {
authenticateGradleWrapper()
sh script: [ sh script: [
'./gradlew', './gradlew',
COMMON_GRADLE_PARAMS, COMMON_GRADLE_PARAMS,

View File

@ -6,9 +6,11 @@
@Library('corda-shared-build-pipeline-steps') @Library('corda-shared-build-pipeline-steps')
import com.r3.build.utils.GitUtils import com.r3.build.utils.GitUtils
import com.r3.build.enums.SnykOrganisation
import com.r3.build.utils.SnykUtils
GitUtils gitUtils = new GitUtils(this) GitUtils gitUtils = new GitUtils(this)
SnykUtils snykUtils = new SnykUtils(this)
/** /**
* Sense environment * Sense environment
*/ */
@ -16,7 +18,7 @@ boolean isReleaseBranch = (env.BRANCH_NAME =~ /^release\/os\/.*/)
boolean isReleaseTag = (env.TAG_NAME =~ /^release-.*(?<!_JDK11)$/) boolean isReleaseTag = (env.TAG_NAME =~ /^release-.*(?<!_JDK11)$/)
boolean isInternalRelease = (env.TAG_NAME =~ /^internal-release-.*$/) boolean isInternalRelease = (env.TAG_NAME =~ /^internal-release-.*$/)
boolean isReleaseCandidate = (env.TAG_NAME =~ /^(release-.*(RC|HC).*(?<!_JDK11))$/) boolean isReleaseCandidate = (env.TAG_NAME =~ /^(release-.*(RC|HC).*(?<!_JDK11))$/)
boolean isReleasePatch = (env.TAG_NAME =~ /^release.*([1-9]\d*|0)(\.([1-9]\d*|0)){2}$/) def buildEdition = (isReleaseTag || isReleaseCandidate) ? "Corda Community Edition" : "Corda Open Source"
/** /**
* Common Gradle arguments for all Gradle executions * Common Gradle arguments for all Gradle executions
@ -27,7 +29,8 @@ String COMMON_GRADLE_PARAMS = [
'--info', '--info',
'-Pcompilation.warningsAsErrors=false', '-Pcompilation.warningsAsErrors=false',
'-Ptests.failFast=true', '-Ptests.failFast=true',
'--build-cache' '--build-cache',
'-DexcludeShell'
].join(' ') ].join(' ')
pipeline { pipeline {
@ -59,11 +62,13 @@ pipeline {
CORDA_ARTIFACTORY_PASSWORD = "${env.ARTIFACTORY_CREDENTIALS_PSW}" CORDA_ARTIFACTORY_PASSWORD = "${env.ARTIFACTORY_CREDENTIALS_PSW}"
CORDA_ARTIFACTORY_USERNAME = "${env.ARTIFACTORY_CREDENTIALS_USR}" CORDA_ARTIFACTORY_USERNAME = "${env.ARTIFACTORY_CREDENTIALS_USR}"
CORDA_GRADLE_SCAN_KEY = credentials('gradle-build-scans-key') CORDA_GRADLE_SCAN_KEY = credentials('gradle-build-scans-key')
CORDA_BUILD_EDITION = "${buildEdition}"
CORDA_USE_CACHE = "corda-remotes" CORDA_USE_CACHE = "corda-remotes"
DOCKER_URL = "https://index.docker.io/v1/" DOCKER_URL = "https://index.docker.io/v1/"
EMAIL_RECIPIENTS = credentials('corda4-email-recipient') EMAIL_RECIPIENTS = credentials('corda4-email-recipient')
INTEGRATION_ID = credentials('snyk-artifactory-c4')
SNYK_API_KEY = "c4-os-snyk" //Jenkins credential type: Snyk Api token SNYK_API_KEY = "c4-os-snyk" //Jenkins credential type: Snyk Api token
SNYK_API_TOKEN = credentials('c4-os-snyk-api-token-secret') //Jenkins credential type: Secret text SNYK_TOKEN = credentials('c4-os-snyk-api-token-secret') //Jenkins credential type: Secret text
C4_OS_SNYK_ORG_ID = credentials('corda4-os-snyk-org-id') C4_OS_SNYK_ORG_ID = credentials('corda4-os-snyk-org-id')
} }
@ -110,7 +115,7 @@ pipeline {
expression { isReleaseTag || isReleaseCandidate || isReleaseBranch } expression { isReleaseTag || isReleaseCandidate || isReleaseBranch }
} }
steps { steps {
snykLicenseGeneration(env.SNYK_API_TOKEN, env.C4_OS_SNYK_ORG_ID) snykLicenseGeneration(env.SNYK_TOKEN, env.C4_OS_SNYK_ORG_ID)
} }
post { post {
always { always {
@ -274,7 +279,7 @@ pipeline {
rtGradleRun( rtGradleRun(
usesPlugin: true, usesPlugin: true,
useWrapper: true, useWrapper: true,
switches: '-s --info', switches: '-s --info -DpublishApiDocs',
tasks: 'artifactoryPublish', tasks: 'artifactoryPublish',
deployerId: 'deployer', deployerId: 'deployer',
buildName: env.ARTIFACTORY_BUILD_NAME buildName: env.ARTIFACTORY_BUILD_NAME
@ -288,7 +293,7 @@ pipeline {
stage('Publish Release Candidate to Internal Repository') { stage('Publish Release Candidate to Internal Repository') {
when { when {
expression { return false} // keeping stage to preserve Jenkins history on release branches, but not supported for patch builds pre 4.9 expression { isReleaseCandidate }
} }
steps { steps {
withCredentials([ withCredentials([
@ -310,7 +315,7 @@ pipeline {
stage('Publish Release to Docker Hub') { stage('Publish Release to Docker Hub') {
when { when {
expression { isReleaseTag && !isInternalRelease && !isReleaseCandidate && !isReleasePatch} expression { isReleaseTag && !isInternalRelease && !isReleaseCandidate}
} }
steps { steps {
withCredentials([ withCredentials([
@ -322,7 +327,7 @@ pipeline {
'./gradlew', './gradlew',
COMMON_GRADLE_PARAMS, COMMON_GRADLE_PARAMS,
'docker:pushDockerImage', 'docker:pushDockerImage',
'-Pdocker.image.repository=corda/corda', '-Pdocker.image.repository=corda/community',
'--image OFFICIAL' '--image OFFICIAL'
].join(' ') ].join(' ')
} }
@ -397,6 +402,13 @@ pipeline {
if (isReleaseTag || isReleaseCandidate || isReleaseBranch) { if (isReleaseTag || isReleaseCandidate || isReleaseBranch) {
snykSecurityScan.generateHtmlElements() snykSecurityScan.generateHtmlElements()
} }
if (isReleaseTag || isReleaseCandidate) {
// auto import and scanning of Docker images tag is dictated by below properties, so retrieve these first to scan the approproate tag
String cordaVersion = sh(script: 'grep "cordaVersion" constants.properties | awk -F= \'{print $2}\'', returnStdout: true).trim()
String versionSuffix = sh(script: 'grep "versionSuffix" constants.properties | awk -F= \'{print $2}\'', returnStdout: true).trim()
snykUtils.SnykApiImport(!versionSuffix.isEmpty() ? "${cordaVersion}-${versionSuffix}" : cordaVersion, SnykOrganisation.CORDA_4_OS, env.C4_OS_SNYK_ORG_ID)
}
} }
} }
unstable { unstable {

View File

@ -3,9 +3,9 @@
# PR Checklist: # PR Checklist:
- [ ] Have you run the unit, integration and smoke tests as described [here](https://docs.corda.net/head/testing.html)? - [ ] Have you run the unit, integration and smoke tests as described [here](https://docs.r3.com/en/platform/corda/4.8/open-source/testing.html)?
- [ ] If you added public APIs, did you write the JavaDocs? - [ ] If you added public APIs, did you write the JavaDocs/kdocs?
- [ ] If the changes are of interest to application developers, have you added them to the [changelog](https://docs.corda.net/head/changelog.html) (`/docs/source/changelog.rst`), and potentially the [release notes](https://docs.corda.net/head/release-notes.html) (`/docs/source/release-notes.rst`)? - [ ] If the changes are of interest to application developers, have you added them to the changelog, and potentially the [release notes](https://docs.corda.net/head/release-notes.html) (`https://docs.r3.com/en/platform/corda/4.8/open-source/release-notes.html`)?
- [ ] If you are contributing for the first time, please read the [contributor agreement](https://docs.corda.net/head/contributing.html) now and add a comment to this pull request stating that your PR is in accordance with the [Developer's Certificate of Origin](https://docs.corda.net/head/contributing.html#merging-the-changes-back-into-corda). - [ ] If you are contributing for the first time, please read the [contributor agreement](https://docs.r3.com/en/platform/corda/4.8/open-source/contributing.html) now and add a comment to this pull request stating that your PR is in accordance with the [Developer's Certificate of Origin](https://docs.r3.com/en/platform/corda/4.8/open-source/contributing.html#merging-the-changes-back-into-corda).
Thanks for your code, it's appreciated! :) Thanks for your code, it's appreciated! :)

231
.snyk Normal file
View File

@ -0,0 +1,231 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.25.0
# ignores vulnerabilities until expiry date; change duration by modifying expiry date
ignore:
SNYK-JAVA-COMGOOGLEGUAVA-1015415:
- '*':
reason: >-
Guavas Files.createTempDir() is used during integration tests only.
Users of Corda are advised not to use Guavas Files.createTempDir()
when building applications on Corda.
expires: 2023-09-01T11:38:11.478Z
created: 2022-12-29T11:38:11.489Z
SNYK-JAVA-COMH2DATABASE-31685:
- '*':
reason: >-
H2 console is not enabled for any of the applications we are running.
When it comes to DB connectivity parameters, we do not allow changing
them as they are supplied by Corda Node configuration file.
expires: 2023-09-01T11:39:26.763Z
created: 2022-12-29T11:39:26.775Z
SNYK-JAVA-COMH2DATABASE-2331071:
- '*':
reason: >-
H2 console is not enabled for any of the applications we are running.
When it comes to DB connectivity parameters, we do not allow changing
them as they are supplied by Corda Node configuration file.
expires: 2023-09-01T11:41:05.707Z
created: 2022-12-29T11:41:05.723Z
SNYK-JAVA-COMSQUAREUPOKHTTP3-2958044:
- '*':
reason: >-
The vulnerability in okhttps error handling is only exploitable in
services that receive and parse HTTP requests. Corda does not receive
HTTP requests and thus is not exposed to this issue.
expires: 2023-09-01T11:42:55.546Z
created: 2022-12-29T11:42:55.556Z
SNYK-JAVA-IONETTY-1042268:
- '*':
reason: >-
Corda does not rely on hostname verification in the P2P protocol to
identify a host, so is not impacted by this vulnerability. Corda uses
its own SSL identity check logic for the network model. Corda
validates based on the full X500 subject name and the fact that P2P
links use mutually authenticated TLS with the same trust roots. For
RPC SSL client connections Artemis is used which calls into netty. The
default value for verifyHost is true for Artemis client connectors so
verification of the host name in netty does occur.
expires: 2023-09-01T11:45:42.976Z
created: 2022-12-29T11:45:42.981Z
SNYK-JAVA-ORGJETBRAINSKOTLIN-2628385:
- '*':
reason: >-
This is a build time vulnerability. It relates to the inability to
lock dependencies for Kotlin Multiplatform Gradle Projects. At build
time for Corda we do not use Multiplatform Gradle Projects so are not
affected by this vulnerability. In addition as it is a build time
vulnerability released artifacts are not affected.
expires: 2023-09-01T11:52:35.855Z
created: 2022-12-29T11:52:35.870Z
SNYK-JAVA-ORGJETBRAINSKOTLIN-2393744:
- '*':
reason: >-
This vulnerability relates to information exposure via creation of
temporary files (via Kotlin functions) with insecure permissions.
Corda does not use any of the vulnerable functions so it not
susceptible to this vulnerability.
expires: 2023-09-01T13:39:03.244Z
created: 2022-12-29T13:39:03.262Z
SNYK-JAVA-ORGLIQUIBASE-2419059:
- '*':
reason: >-
This component is used to upgrade the node database schema either at
node startup or via the database migration tool. The XML input for the
database migration is generated by Corda from either R3 supplied XML
files included in corda.jar or those XML files written by the CorDapp
author included in a CorDapp that is installed in the node CorDapps
directory. Contract CorDapps received over the network are not a
source of XML files for this generation step. An attacker trying to
exploit this vulnerability would need access to the server with the
XML input files, and specifically the access and ability to change JAR
files on the file system that make up the Corda installation.
expires: 2023-09-01T13:42:11.552Z
created: 2022-12-29T13:42:11.570Z
SNYK-JAVA-COMH2DATABASE-2348247:
- '*':
reason: >-
H2 console is not enabled for any of the applications we are running.
When it comes to DB connectivity parameters, we do not allow changing
them as they are supplied by Corda Node configuration file.
expires: 2023-09-01T11:36:39.068Z
created: 2022-12-29T11:36:39.089Z
SNYK-JAVA-COMH2DATABASE-1769238:
- '*':
reason: >-
H2 is not invoked by Corda unless the node deployment configures an H2
database. This is not a supported configuration in Production and so
this vulnerability should be irrelevant except during development on
Corda. Corda itself does not store XML data within the database so
Corda is not susceptible to this vulnerability. If CorDapp developers
store XML data to the database they need to ascertain themselves that
they are not susceptible.
expires: 2023-09-01T11:40:29.871Z
created: 2022-12-29T11:40:29.896Z
SNYK-JAVA-ORGYAML-3152153:
- '*':
reason: >-
There is a transitive dependency on snakeyaml from the third party
components jackson-dataformat-yaml and liquidbase-core. The
jackson-dataformat-yaml component does not use the snakeyaml
databinding layer. For liquidbase we use xml in the changelog files
not yaml. So given this Corda is not susceptible to this
vulnerability.Cordapp authors should exercise their own judgment if
using this library directly in their cordapp.
expires: 2023-09-01T11:35:04.385Z
created: 2023-01-04T11:35:04.414Z
SNYK-JAVA-COMH2DATABASE-3146851:
- '*':
reason: >-
Corda does not make use of the H2 web admin console, so it not
susceptible to this reported vulnerability
expires: 2023-09-01T11:45:11.295Z
created: 2023-01-04T11:45:11.322Z
SNYK-JAVA-ORGBOUNCYCASTLE-2841508:
- '*':
reason: >-
This vulnerability relates to weak key-hash message authentication
code due to an error within the BKS version 1 keystore files. Corda
does not use BKS-V1 for its keystore files so is not susceptible to
this vulnerability.
expires: 2023-09-01T11:32:38.120Z
created: 2022-09-21T11:32:38.125Z
SNYK-JAVA-COMFASTERXMLJACKSONCORE-3038424:
- '*':
reason: >-
Corda does not set the non-default UNWRAP_SINGLE_VALUE_ARRAYS required
for this vulnerability. In addition Corda does not use Jackson for
deserialization except in the optional shell which we recommend using
standalone. The Corda node itself is not exposed. Corda does however
provide mappings of Corda types to allow CorDapps to use Jackson, and
CorDapps using Jackson should make their own assessment. This
vulnerability relates to deeply nested untyped Object or Array values
(3000 levels deep). Only CorDapps with these types at this level of
nesting are potentially susceptible.
expires: 2023-09-01T12:04:40.180Z
created: 2023-02-09T12:04:40.209Z
SNYK-JAVA-COMFASTERXMLJACKSONCORE-3038426:
- '*':
reason: >-
Corda does not set the non-default UNWRAP_SINGLE_VALUE_ARRAYS required
for this vulnerability. In addition Corda does not use Jackson for
deserialization except in the optional shell which we recommend using
standalone. The Corda node itself is not exposed. Corda does however
provide mappings of Corda types to allow CorDapps to use Jackson, and
CorDapps using Jackson should make their own assessment. This
vulnerability relates to deeply nested untyped Object or Array values
(3000 levels deep). Only CorDapps with these types at this level of
nesting are potentially susceptible.
expires: 2023-09-01T12:05:03.931Z
created: 2023-02-09T12:05:03.962Z
SNYK-JAVA-ORGYAML-2806360:
- '*':
reason: >-
Snakeyaml is being used by Jackson and liquidbase. Corda does not use
Jackson except in the optional shell which we recommend using
standalone. The Corda node itself is not exposed. Corda does however
provide mappings of Corda types to allow CorDapps to use Jackson, and
CorDapps using Jackson should make their own assessment. Liquibase is
used to apply the database migration changes. XML files are used here
to define the changes not YAML and therefore the Corda node itself is
not exposed to this DOS vulnerability.
expires: 2023-09-01T13:40:55.262Z
created: 2022-09-21T13:40:55.279Z
SNYK-JAVA-ORGYAML-3016891:
- '*':
reason: >-
Snakeyaml is being used by Jackson and liquidbase. Corda does not use
Jackson for deserialization except in the optional shell which we
recommend using standalone. The Corda node itself is not exposed.
Corda does however provide mappings of Corda types to allow CorDapps
to use Jackson, and CorDapps using Jackson should make their own
assessment. Liquibase is used to apply the database migration changes.
XML files are used here to define the changes not YAML and therefore
the Corda node itself is not exposed to this deserialisation
vulnerability.
expires: 2023-09-01T16:37:28.911Z
created: 2023-02-06T16:37:28.933Z
SNYK-JAVA-ORGYAML-3016888:
- '*':
reason: >-
Snakeyaml is being used by Jackson and liquidbase. Corda does not use
Jackson for deserialization except in the optional shell which we
recommend using standalone. The Corda node itself is not exposed.
Corda does however provide mappings of Corda types to allow CorDapps
to use Jackson, and CorDapps using Jackson should make their own
assessment. Liquibase is used to apply the database migration changes.
XML files are used here to define the changes not YAML and therefore
the Corda node itself is not exposed to this deserialisation
vulnerability.
expires: 2023-09-01T13:39:49.450Z
created: 2022-09-21T13:39:49.470Z
SNYK-JAVA-ORGYAML-3016889:
- '*':
reason: >-
Snakeyaml is being used by Jackson and liquidbase. Corda does not use
Jackson for deserialization except in the optional shell which we
recommend using standalone. The Corda node itself is not exposed.
Corda does however provide mappings of Corda types to allow CorDapps
to use Jackson, and CorDapps using Jackson should make their own
assessment. Liquibase is used to apply the database migration changes.
XML files are used here to define the changes not YAML and therefore
the Corda node itself is not exposed to this deserialisation
vulnerability.
expires: 2023-09-01T16:35:13.840Z
created: 2023-02-06T16:35:13.875Z
SNYK-JAVA-ORGYAML-3113851:
- '*':
reason: >-
Snakeyaml is being used by Jackson and liquidbase. Corda does not use
Jackson for deserialization except in the optional shell which we
recommend using standalone. The Corda node itself is not exposed.
Corda does however provide mappings of Corda types to allow CorDapps
to use Jackson, and CorDapps using Jackson should make their own
assessment. Liquibase is used to apply the database migration changes.
XML files are used here to define the changes not YAML and therefore
the Corda node itself is not exposed to this deserialisation
vulnerability.
expires: 2024-04-01T00:00:00.000Z
created: 2022-11-29T14:55:03.623Z
patch: {}

View File

@ -2,4 +2,4 @@
Corda is an open-source project and contributions are welcome! Corda is an open-source project and contributions are welcome!
To find out how to contribute, please see our [contributing docs](https://docs.corda.net/head/contributing-index.html). To find out how to contribute, please see our [contributing docs](https://docs.r3.com/en/platform/corda/4.8/open-source/contributing.html).

View File

@ -39,7 +39,7 @@ Corda is an open source blockchain project, designed for business from the start
Corda is an open-source project and contributions are welcome! Corda is an open-source project and contributions are welcome!
To find out how to contribute, please see our [contributing docs](https://docs.corda.net/head/contributing-index.html). To find out how to contribute, please see our [contributing docs](https://docs.r3.com/en/platform/corda/4.8/open-source/contributing.html).
## License ## License

View File

@ -13,6 +13,7 @@ buildscript {
ext.baseVersion = constants.getProperty("cordaVersion") ext.baseVersion = constants.getProperty("cordaVersion")
ext.versionSuffix = constants.getProperty("versionSuffix") ext.versionSuffix = constants.getProperty("versionSuffix")
ext.corda_build_edition = System.getenv("CORDA_BUILD_EDITION")?.trim() ?: "Corda Open Source"
ext.corda_platform_version = constants.getProperty("platformVersion") ext.corda_platform_version = constants.getProperty("platformVersion")
ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion") ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
@ -60,11 +61,11 @@ buildscript {
ext.capsule_version = '1.0.3' ext.capsule_version = '1.0.3'
ext.asm_version = '7.1' ext.asm_version = '7.1'
ext.artemis_version = '2.6.2' ext.artemis_version = '2.19.1'
// TODO Upgrade to Jackson Kotlin 2.10+ only when corda is using kotlin 1.3.10 // TODO Upgrade Jackson only when corda is using kotlin 1.3.10
ext.jackson_version = '2.13.5' ext.jackson_version = '2.17.2'
ext.jackson_kotlin_version = '2.9.7' ext.jackson_kotlin_version = '2.9.7'
ext.jetty_version = '9.4.19.v20190610' ext.jetty_version = '9.4.53.v20231009'
ext.jersey_version = '2.25' ext.jersey_version = '2.25'
ext.servlet_version = '4.0.1' ext.servlet_version = '4.0.1'
ext.assertj_version = '3.12.2' ext.assertj_version = '3.12.2'
@ -80,7 +81,7 @@ buildscript {
ext.deterministic_rt_version = constants.getProperty('deterministicRtVersion') ext.deterministic_rt_version = constants.getProperty('deterministicRtVersion')
ext.okhttp_version = '3.14.2' ext.okhttp_version = '3.14.2'
ext.netty_version = '4.1.77.Final' ext.netty_version = '4.1.77.Final'
ext.tcnative_version = '2.0.48.Final' ext.tcnative_version = constants.getProperty("tcnativeVersion")
ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion") ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion")
ext.fileupload_version = '1.4' ext.fileupload_version = '1.4'
ext.kryo_version = '4.0.2' ext.kryo_version = '4.0.2'
@ -106,7 +107,6 @@ buildscript {
ext.dependency_checker_version = '5.2.0' ext.dependency_checker_version = '5.2.0'
ext.commons_collections_version = '4.3' ext.commons_collections_version = '4.3'
ext.beanutils_version = '1.9.4' ext.beanutils_version = '1.9.4'
ext.crash_version = '1.7.6'
ext.jsr305_version = constants.getProperty("jsr305Version") ext.jsr305_version = constants.getProperty("jsr305Version")
ext.shiro_version = '1.10.0' ext.shiro_version = '1.10.0'
ext.artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion') ext.artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion')
@ -130,6 +130,8 @@ buildscript {
ext.controlsfx_version = '8.40.15' ext.controlsfx_version = '8.40.15'
ext.detekt_version = constants.getProperty('detektVersion') ext.detekt_version = constants.getProperty('detektVersion')
ext.docker_java_version = constants.getProperty("dockerJavaVersion") ext.docker_java_version = constants.getProperty("dockerJavaVersion")
ext.commons_configuration2_version = "2.10.1"
ext.commons_text_version = "1.10.0"
if (JavaVersion.current().isJava8()) { if (JavaVersion.current().isJava8()) {
ext.fontawesomefx_commons_version = '8.15' ext.fontawesomefx_commons_version = '8.15'
ext.fontawesomefx_fontawesome_version = '4.7.0-5' ext.fontawesomefx_fontawesome_version = '4.7.0-5'
@ -364,7 +366,7 @@ allprojects {
attributes('Corda-Release-Version': corda_release_version) attributes('Corda-Release-Version': corda_release_version)
attributes('Corda-Platform-Version': corda_platform_version) attributes('Corda-Platform-Version': corda_platform_version)
attributes('Corda-Revision': corda_revision) attributes('Corda-Revision': corda_revision)
attributes('Corda-Vendor': 'Corda Open Source') attributes('Corda-Vendor': corda_build_edition)
attributes('Automatic-Module-Name': "net.corda.${task.project.name.replaceAll('-', '.')}") attributes('Automatic-Module-Name': "net.corda.${task.project.name.replaceAll('-', '.')}")
attributes('Corda-Docs-Link': corda_docs_link) attributes('Corda-Docs-Link': corda_docs_link)
} }
@ -413,6 +415,12 @@ allprojects {
repositories { repositories {
mavenLocal() mavenLocal()
// Prevents cache giving use the wrong artemis
mavenCentral {
content {
includeGroup 'org.apache.activemq'
}
}
// Use system environment to activate caching with Artifactory, // Use system environment to activate caching with Artifactory,
// because it is actually easier to pass that during parallel build. // because it is actually easier to pass that during parallel build.
// NOTE: it has to be a name of a virtual repository with all // NOTE: it has to be a name of a virtual repository with all
@ -455,6 +463,12 @@ allprojects {
includeGroup 'com.github.detro' includeGroup 'com.github.detro'
} }
} }
maven {
url "${publicArtifactURL}/corda-releases"
content {
includeModule('net.corda', 'corda-shell')
}
}
mavenCentral() mavenCentral()
maven { maven {
url "${publicArtifactURL}/jcenter-backup" url "${publicArtifactURL}/jcenter-backup"
@ -473,7 +487,9 @@ allprojects {
// Force dependencies to use the same version of Guava as Corda. // Force dependencies to use the same version of Guava as Corda.
force "com.google.guava:guava:$guava_version" force "com.google.guava:guava:$guava_version"
// Demand that everything uses our given version of Netty. // Demand that everything uses our given versions of:
// * Netty
// * Apache commons-configuration2
eachDependency { details -> eachDependency { details ->
if (details.requested.group == 'io.netty' && details.requested.name.startsWith('netty-')) { if (details.requested.group == 'io.netty' && details.requested.name.startsWith('netty-')) {
if (details.requested.name.startsWith('netty-tcnative')){ if (details.requested.name.startsWith('netty-tcnative')){
@ -483,6 +499,13 @@ allprojects {
} }
} }
if (details.requested.group == 'org.apache.commons') {
if (details.requested.name == "commons-configuration2") {
details.useVersion commons_configuration2_version
} else if (details.requested.name == "commons-text") {
details.useVersion commons_text_version
}
}
if (details.requested.group == 'org.yaml' && details.requested.name == 'snakeyaml') { if (details.requested.group == 'org.yaml' && details.requested.name == 'snakeyaml') {
details.useVersion snake_yaml_version details.useVersion snake_yaml_version
} }

View File

@ -2,8 +2,21 @@ package net.corda.client.jackson
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.core.* import com.fasterxml.jackson.core.JsonFactory
import com.fasterxml.jackson.databind.* import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParseException
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import com.fasterxml.jackson.databind.BeanDescription
import com.fasterxml.jackson.databind.DeserializationConfig
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.Module
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.fasterxml.jackson.databind.cfg.ConstructorDetector import com.fasterxml.jackson.databind.cfg.ConstructorDetector
@ -22,9 +35,21 @@ import net.corda.core.DoNotImplement
import net.corda.core.contracts.Amount import net.corda.core.contracts.Amount
import net.corda.core.contracts.ContractState import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateRef import net.corda.core.contracts.StateRef
import net.corda.core.crypto.* import net.corda.core.crypto.Base58
import net.corda.core.identity.* import net.corda.core.crypto.MerkleTree
import net.corda.core.internal.* import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature
import net.corda.core.crypto.toStringShort
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.CertRole
import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.isStatic
import net.corda.core.internal.kotlinObjectInstance
import net.corda.core.internal.uncheckedCast
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.services.IdentityService import net.corda.core.node.services.IdentityService

View File

@ -2,16 +2,30 @@
package net.corda.client.jackson.internal package net.corda.client.jackson.internal
import com.fasterxml.jackson.annotation.*
import com.fasterxml.jackson.annotation.JsonAutoDetect.Value import com.fasterxml.jackson.annotation.JsonAutoDetect.Value
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonCreator.Mode.DISABLED import com.fasterxml.jackson.annotation.JsonCreator.Mode.DISABLED
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonInclude.Include import com.fasterxml.jackson.annotation.JsonInclude.Include
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.annotation.JsonValue
import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParseException import com.fasterxml.jackson.core.JsonParseException
import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken import com.fasterxml.jackson.core.JsonToken
import com.fasterxml.jackson.databind.* import com.fasterxml.jackson.databind.BeanDescription
import com.fasterxml.jackson.databind.BeanProperty
import com.fasterxml.jackson.databind.DeserializationConfig
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.Module
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationConfig
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.fasterxml.jackson.databind.cfg.MapperConfig import com.fasterxml.jackson.databind.cfg.MapperConfig
@ -32,12 +46,30 @@ import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer
import com.fasterxml.jackson.databind.ser.std.UUIDSerializer import com.fasterxml.jackson.databind.ser.std.UUIDSerializer
import com.google.common.primitives.Booleans import com.google.common.primitives.Booleans
import net.corda.client.jackson.JacksonSupport import net.corda.client.jackson.JacksonSupport
import net.corda.core.contracts.* import net.corda.core.contracts.Amount
import net.corda.core.crypto.* import net.corda.core.contracts.AttachmentConstraint
import net.corda.core.contracts.Command
import net.corda.core.contracts.CommandData
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.PrivacySalt
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TimeWindow
import net.corda.core.contracts.TransactionState
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.DigestService
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.PartialMerkleTree.PartialTree import net.corda.core.crypto.PartialMerkleTree.PartialTree
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SecureHash.Companion.SHA2_256 import net.corda.core.crypto.SecureHash.Companion.SHA2_256
import net.corda.core.crypto.SignatureMetadata
import net.corda.core.crypto.SignatureScheme
import net.corda.core.crypto.TransactionSignature
import net.corda.core.flows.StateMachineRunId import net.corda.core.flows.StateMachineRunId
import net.corda.core.identity.* import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.DigitalSignatureWithCert import net.corda.core.internal.DigitalSignatureWithCert
import net.corda.core.internal.createComponentGroups import net.corda.core.internal.createComponentGroups
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
@ -45,7 +77,12 @@ import net.corda.core.serialization.SerializeAsToken
import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.transactions.* import net.corda.core.transactions.ContractUpgradeFilteredTransaction
import net.corda.core.transactions.ContractUpgradeWireTransaction
import net.corda.core.transactions.FilteredTransaction
import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.ByteSequence
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.parseAsHex import net.corda.core.utilities.parseAsHex

View File

@ -55,7 +55,9 @@ dependencies {
// TODO: remove the forced update of commons-collections and beanutils when artemis updates them // TODO: remove the forced update of commons-collections and beanutils when artemis updates them
compile "org.apache.commons:commons-collections4:${commons_collections_version}" compile "org.apache.commons:commons-collections4:${commons_collections_version}"
compile "commons-beanutils:commons-beanutils:${beanutils_version}" compile "commons-beanutils:commons-beanutils:${beanutils_version}"
compile "org.apache.activemq:artemis-core-client:${artemis_version}" compile("org.apache.activemq:artemis-core-client:${artemis_version}") {
exclude group: 'org.jgroups', module: 'jgroups'
}
// Unit testing helpers. // Unit testing helpers.
testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}" testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}"

View File

@ -30,6 +30,7 @@ import net.corda.testing.node.internal.rpcTestUser
import net.corda.testing.node.internal.startRandomRpcClient import net.corda.testing.node.internal.startRandomRpcClient
import net.corda.testing.node.internal.startRpcClient import net.corda.testing.node.internal.startRpcClient
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration
import org.apache.activemq.artemis.api.core.QueueConfiguration
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.junit.After import org.junit.After
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
@ -551,7 +552,11 @@ class RPCStabilityTests {
// Construct an RPC session manually so that we can hang in the message handler // Construct an RPC session manually so that we can hang in the message handler
val myQueue = "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.test.${random63BitValue()}" val myQueue = "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.test.${random63BitValue()}"
val session = startArtemisSession(server.broker.hostAndPort!!) val session = startArtemisSession(server.broker.hostAndPort!!)
session.createTemporaryQueue(myQueue, ActiveMQDefaultConfiguration.getDefaultRoutingType(), myQueue) session.createQueue(QueueConfiguration(myQueue)
.setRoutingType(ActiveMQDefaultConfiguration.getDefaultRoutingType())
.setAddress(myQueue)
.setTemporary(true)
.setDurable(false))
val consumer = session.createConsumer(myQueue, null, -1, -1, false) val consumer = session.createConsumer(myQueue, null, -1, -1, false)
consumer.setMessageHandler { consumer.setMessageHandler {
Thread.sleep(5000) // Needs to be slower than one per second to get kicked. Thread.sleep(5000) // Needs to be slower than one per second to get kicked.
@ -588,7 +593,11 @@ class RPCStabilityTests {
// Construct an RPC client session manually // Construct an RPC client session manually
val myQueue = "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.test.${random63BitValue()}" val myQueue = "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.test.${random63BitValue()}"
val session = startArtemisSession(server.broker.hostAndPort!!) val session = startArtemisSession(server.broker.hostAndPort!!)
session.createTemporaryQueue(myQueue, ActiveMQDefaultConfiguration.getDefaultRoutingType(), myQueue) session.createQueue(QueueConfiguration(myQueue)
.setRoutingType(ActiveMQDefaultConfiguration.getDefaultRoutingType())
.setAddress(myQueue)
.setTemporary(true)
.setDurable(false))
val consumer = session.createConsumer(myQueue, null, -1, -1, false) val consumer = session.createConsumer(myQueue, null, -1, -1, false)
val replies = ArrayList<Any>() val replies = ArrayList<Any>()
consumer.setMessageHandler { consumer.setMessageHandler {

View File

@ -50,6 +50,7 @@ import kotlin.concurrent.thread
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue import kotlin.test.assertTrue
class CordaRPCClientReconnectionTest { class CordaRPCClientReconnectionTest {
@ -595,6 +596,29 @@ class CordaRPCClientReconnectionTest {
} }
} }
@Test(timeout=300_000)
fun `reconnecting 'reattachFlowWithClientId' rpc works if called with non-existent client id`() {
driver(DriverParameters(inMemoryDB = false, cordappsForAllNodes = listOf(this.enclosedCordapp()))) {
val address = NetworkHostAndPort("localhost", portAllocator.nextPort())
fun startNode(additionalCustomOverrides: Map<String, Any?> = emptyMap()): NodeHandle {
return startNode(
providedName = CHARLIE_NAME,
rpcUsers = listOf(CordaRPCClientTest.rpcUser),
customOverrides = mapOf("rpcSettings.address" to address.toString()) + additionalCustomOverrides
).getOrThrow()
}
val node = startNode()
val client = CordaRPCClient(node.rpcAddress, config)
(client.start(rpcUser.username, rpcUser.password, gracefulReconnect = gracefulReconnect)).use {
val rpcOps = it.proxy as ReconnectingCordaRPCOps
val nonExistentClientId = UUID.randomUUID().toString()
val flowHandle = rpcOps.reattachFlowWithClientId<Any>(nonExistentClientId)
assertNull(flowHandle)
}
}
}
@StartableByRPC @StartableByRPC
class SimpleFlow : FlowLogic<Int>() { class SimpleFlow : FlowLogic<Int>() {

View File

@ -98,6 +98,8 @@ class RPCClient<I : RPCOps>(
// By default RoundRobinConnectionLoadBalancingPolicy is used that picks first endpoint from the pool // By default RoundRobinConnectionLoadBalancingPolicy is used that picks first endpoint from the pool
// at random. This may be undesired and non-deterministic. For more information, see [RoundRobinConnectionPolicy] // at random. This may be undesired and non-deterministic. For more information, see [RoundRobinConnectionPolicy]
connectionLoadBalancingPolicyClassName = RoundRobinConnectionPolicy::class.java.canonicalName connectionLoadBalancingPolicyClassName = RoundRobinConnectionPolicy::class.java.canonicalName
// Without this any type of "send" time failures will not be delivered back to the client
isBlockOnNonDurableSend = true
} }
val sessionId = Trace.SessionId.newInstance() val sessionId = Trace.SessionId.newInstance()
val distributionMux = DistributionMux(listeners, username) val distributionMux = DistributionMux(listeners, username)

View File

@ -39,6 +39,7 @@ import net.corda.nodeapi.internal.rpc.client.RpcClientObservableDeSerializer
import net.corda.nodeapi.internal.rpc.client.RpcObservableMap import net.corda.nodeapi.internal.rpc.client.RpcObservableMap
import org.apache.activemq.artemis.api.core.ActiveMQException import org.apache.activemq.artemis.api.core.ActiveMQException
import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException
import org.apache.activemq.artemis.api.core.QueueConfiguration
import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.RoutingType
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
@ -60,6 +61,7 @@ import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.ScheduledFuture import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -380,12 +382,19 @@ internal class RPCClientProxyHandler(
targetLegalIdentity?.let { targetLegalIdentity?.let {
artemisMessage.putStringProperty(RPCApi.RPC_TARGET_LEGAL_IDENTITY, it.toString()) artemisMessage.putStringProperty(RPCApi.RPC_TARGET_LEGAL_IDENTITY, it.toString())
} }
sendExecutor!!.submit { val future: Future<*> = sendExecutor!!.submit {
artemisMessage.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, deduplicationSequenceNumber.getAndIncrement()) artemisMessage.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, deduplicationSequenceNumber.getAndIncrement())
log.debug { "-> RPC -> $message" } log.debug { "-> RPC -> $message" }
rpcProducer!!.send(artemisMessage) rpcProducer!!.let {
if (!it.isClosed) {
it.send(artemisMessage)
} else {
log.info("Producer is already closed. Not sending: $message")
} }
} }
}
future.getOrThrow()
}
// The handler for Artemis messages. // The handler for Artemis messages.
private fun artemisMessageHandler(message: ClientMessage) { private fun artemisMessageHandler(message: ClientMessage) {
@ -570,7 +579,12 @@ internal class RPCClientProxyHandler(
} }
if (observableIds != null) { if (observableIds != null) {
log.debug { "Reaping ${observableIds.size} observables" } log.debug { "Reaping ${observableIds.size} observables" }
@Suppress("TooGenericExceptionCaught")
try {
sendMessage(RPCApi.ClientToServer.ObservablesClosed(observableIds)) sendMessage(RPCApi.ClientToServer.ObservablesClosed(observableIds))
} catch(ex: Exception) {
log.warn("Unable to close observables", ex)
}
} }
} }
@ -632,7 +646,8 @@ internal class RPCClientProxyHandler(
consumerSession = sessionFactory!!.createSession(rpcUsername, rpcPassword, false, true, true, false, 16384) consumerSession = sessionFactory!!.createSession(rpcUsername, rpcPassword, false, true, true, false, 16384)
clientAddress = SimpleString("${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$rpcUsername.${random63BitValue()}") clientAddress = SimpleString("${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$rpcUsername.${random63BitValue()}")
log.debug { "Client address: $clientAddress" } log.debug { "Client address: $clientAddress" }
consumerSession!!.createTemporaryQueue(clientAddress, RoutingType.ANYCAST, clientAddress) consumerSession!!.createQueue(QueueConfiguration(clientAddress).setAddress(clientAddress).setRoutingType(RoutingType.ANYCAST)
.setTemporary(true).setDurable(false))
rpcConsumer = consumerSession!!.createConsumer(clientAddress) rpcConsumer = consumerSession!!.createConsumer(clientAddress)
rpcConsumer!!.setMessageHandler(this::artemisMessageHandler) rpcConsumer!!.setMessageHandler(this::artemisMessageHandler)
} }

View File

@ -393,10 +393,11 @@ class ReconnectingCordaRPCOps private constructor(
initialFeed.copy(updates = observable) initialFeed.copy(updates = observable)
} }
FlowHandleWithClientId::class.java -> { FlowHandleWithClientId::class.java -> {
val initialHandle: FlowHandleWithClientId<Any?> = uncheckedCast(doInvoke(method, args, // initialHandle can be null. See @CordaRPCOps.reattachFlowWithClientId.
val initialHandle: FlowHandleWithClientId<Any?>? = uncheckedCast(doInvoke(method, args,
reconnectingRPCConnection.gracefulReconnect.maxAttempts)) reconnectingRPCConnection.gracefulReconnect.maxAttempts))
val initialFuture = initialHandle.returnValue val initialFuture = initialHandle?.returnValue ?: return null
// This is the future that is returned to the client. It will get carried until we reconnect to the node. // This is the future that is returned to the client. It will get carried until we reconnect to the node.
val returnFuture = openFuture<Any?>() val returnFuture = openFuture<Any?>()

View File

@ -40,7 +40,7 @@ import net.corda.nodeapi.internal.config.User
import net.corda.sleeping.SleepingFlow import net.corda.sleeping.SleepingFlow
import net.corda.smoketesting.NodeConfig import net.corda.smoketesting.NodeConfig
import net.corda.smoketesting.NodeProcess import net.corda.smoketesting.NodeProcess
import org.apache.commons.io.output.NullOutputStream import org.apache.commons.io.output.NullOutputStream.NULL_OUTPUT_STREAM
import org.hamcrest.text.MatchesPattern import org.hamcrest.text.MatchesPattern
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
@ -117,7 +117,7 @@ class StandaloneCordaRPClientTest {
assertEquals(attachment.sha256, id, "Attachment has incorrect SHA256 hash") assertEquals(attachment.sha256, id, "Attachment has incorrect SHA256 hash")
val hash = HashingInputStream(Hashing.sha256(), rpcProxy.openAttachment(id)).use { it -> val hash = HashingInputStream(Hashing.sha256(), rpcProxy.openAttachment(id)).use { it ->
it.copyTo(NullOutputStream()) it.copyTo(NULL_OUTPUT_STREAM)
SecureHash.SHA256(it.hash().asBytes()) SecureHash.SHA256(it.hash().asBytes())
} }
assertEquals(attachment.sha256, hash) assertEquals(attachment.sha256, hash)
@ -132,7 +132,7 @@ class StandaloneCordaRPClientTest {
assertEquals(attachment.sha256, id, "Attachment has incorrect SHA256 hash") assertEquals(attachment.sha256, id, "Attachment has incorrect SHA256 hash")
val hash = HashingInputStream(Hashing.sha256(), rpcProxy.openAttachment(id)).use { it -> val hash = HashingInputStream(Hashing.sha256(), rpcProxy.openAttachment(id)).use { it ->
it.copyTo(NullOutputStream()) it.copyTo(NULL_OUTPUT_STREAM)
SecureHash.SHA256(it.hash().asBytes()) SecureHash.SHA256(it.hash().asBytes())
} }
assertEquals(attachment.sha256, hash) assertEquals(attachment.sha256, hash)

View File

@ -9,4 +9,4 @@ package net.corda.common.logging
* (originally added to source control for ease of use) * (originally added to source control for ease of use)
*/ */
internal const val CURRENT_MAJOR_RELEASE = "4.8-SNAPSHOT" internal const val CURRENT_MAJOR_RELEASE = "4.9-SNAPSHOT"

View File

@ -135,7 +135,7 @@
<DefaultRolloverStrategy min="1" max="100"> <DefaultRolloverStrategy min="1" max="100">
<Delete basePath="${archive}" maxDepth="1"> <Delete basePath="${archive}" maxDepth="1">
<IfFileName glob="${log-name}*.log.gz"/> <IfFileName glob="${diagnostic-log-name}*.log.gz"/>
<IfLastModified age="60d"> <IfLastModified age="60d">
<IfAny> <IfAny>
<IfAccumulatedFileSize exceeds="10 GB"/> <IfAccumulatedFileSize exceeds="10 GB"/>
@ -159,7 +159,7 @@
<DefaultRolloverStrategy min="1" max="100"> <DefaultRolloverStrategy min="1" max="100">
<Delete basePath="${archive}" maxDepth="1"> <Delete basePath="${archive}" maxDepth="1">
<IfFileName glob="${log-name}*.log.gz"/> <IfFileName glob="checkpoints_agent*.log.gz"/>
<IfLastModified age="60d"> <IfLastModified age="60d">
<IfAny> <IfAny>
<IfAccumulatedFileSize exceeds="10 GB"/> <IfAccumulatedFileSize exceeds="10 GB"/>
@ -202,7 +202,15 @@
<AppenderRef ref="Console-ErrorCode-Selector"/> <AppenderRef ref="Console-ErrorCode-Selector"/>
<AppenderRef ref="RollingFile-ErrorCode-Appender"/> <AppenderRef ref="RollingFile-ErrorCode-Appender"/>
</Logger> </Logger>
<Logger name="org.apache.activemq.artemis.core.server" level="error" additivity="false"> <Logger name="org.apache.activemq.artemis.core.server" level="warn" additivity="false">
<Filters>
<RegexFilter regex=".*AMQ222165.*" onMatch="DENY" onMismatch="NEUTRAL"/>
<RegexFilter regex=".*AMQ222166.*" onMatch="DENY" onMismatch="NEUTRAL"/>
</Filters>
<AppenderRef ref="Console-ErrorCode-Selector"/>
<AppenderRef ref="RollingFile-ErrorCode-Appender"/>
</Logger>
<Logger name="org.apache.activemq.audit" level="error" additivity="false">
<AppenderRef ref="Console-ErrorCode-Selector"/> <AppenderRef ref="Console-ErrorCode-Selector"/>
<AppenderRef ref="RollingFile-ErrorCode-Appender"/> <AppenderRef ref="RollingFile-ErrorCode-Appender"/>
</Logger> </Logger>

View File

@ -2,7 +2,7 @@
# because some versions here need to be matched by app authors in # because some versions here need to be matched by app authors in
# their own projects. So don't get fancy with syntax! # their own projects. So don't get fancy with syntax!
cordaVersion=4.8 cordaVersion=4.9
versionSuffix=SNAPSHOT versionSuffix=SNAPSHOT
gradlePluginsVersion=5.0.12 gradlePluginsVersion=5.0.12
kotlinVersion=1.2.71 kotlinVersion=1.2.71
@ -11,7 +11,7 @@ java8MinUpdateVersion=171
# When incrementing platformVersion make sure to update # # When incrementing platformVersion make sure to update #
# net.corda.core.internal.CordaUtilsKt.PLATFORM_VERSION as well. # # net.corda.core.internal.CordaUtilsKt.PLATFORM_VERSION as well. #
# ***************************************************************# # ***************************************************************#
platformVersion=10 platformVersion=11
guavaVersion=28.0-jre guavaVersion=28.0-jre
# Quasar version to use with Java 8: # Quasar version to use with Java 8:
quasarVersion=0.7.15_r3 quasarVersion=0.7.15_r3
@ -27,7 +27,7 @@ typesafeConfigVersion=1.3.4
jsr305Version=3.0.2 jsr305Version=3.0.2
artifactoryPluginVersion=4.16.1 artifactoryPluginVersion=4.16.1
snakeYamlVersion=1.33 snakeYamlVersion=1.33
caffeineVersion=2.7.0 caffeineVersion=2.9.3
metricsVersion=4.1.0 metricsVersion=4.1.0
metricsNewRelicVersion=1.1.1 metricsNewRelicVersion=1.1.1
djvmVersion=1.1.1 djvmVersion=1.1.1
@ -36,3 +36,4 @@ openSourceBranch=https://github.com/corda/corda/blob/release/os/4.4
openSourceSamplesBranch=https://github.com/corda/samples/blob/release-V4 openSourceSamplesBranch=https://github.com/corda/samples/blob/release-V4
jolokiaAgentVersion=1.6.1 jolokiaAgentVersion=1.6.1
detektVersion=1.0.1 detektVersion=1.0.1
tcnativeVersion=2.0.48.Final

View File

@ -1,7 +1,11 @@
package net.corda.core.flows package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.* import net.corda.core.contracts.AttachmentResolutionException
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.TransactionResolutionException
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.internal.ResolveTransactionsFlow import net.corda.core.internal.ResolveTransactionsFlow
import net.corda.core.internal.checkParameterHash import net.corda.core.internal.checkParameterHash
import net.corda.core.internal.pushToLoggingContext import net.corda.core.internal.pushToLoggingContext
@ -46,8 +50,8 @@ open class ReceiveTransactionFlow @JvmOverloads constructor(private val otherSid
val stx = otherSideSession.receive<SignedTransaction>().unwrap { val stx = otherSideSession.receive<SignedTransaction>().unwrap {
it.pushToLoggingContext() it.pushToLoggingContext()
logger.info("Received transaction acknowledgement request from party ${otherSideSession.counterparty}.") logger.info("Received transaction acknowledgement request from party ${otherSideSession.counterparty}.")
checkParameterHash(it.networkParametersHash)
subFlow(ResolveTransactionsFlow(it, otherSideSession, statesToRecord)) subFlow(ResolveTransactionsFlow(it, otherSideSession, statesToRecord))
checkParameterHash(it.networkParametersHash)
logger.info("Transaction dependencies resolution completed.") logger.info("Transaction dependencies resolution completed.")
try { try {
it.verify(serviceHub, checkSufficientSignatures) it.verify(serviceHub, checkSufficientSignatures)

View File

@ -28,7 +28,9 @@ import java.util.jar.JarInputStream
// *Internal* Corda-specific utilities. // *Internal* Corda-specific utilities.
const val PLATFORM_VERSION = 10
// When incrementing platformVersion make sure to update PLATFORM_VERSION in constants.properties as well.
const val PLATFORM_VERSION = 11
fun ServicesForResolution.ensureMinimumPlatformVersion(requiredMinPlatformVersion: Int, feature: String) { fun ServicesForResolution.ensureMinimumPlatformVersion(requiredMinPlatformVersion: Int, feature: String) {
checkMinimumPlatformVersion(networkParameters.minimumPlatformVersion, requiredMinPlatformVersion, feature) checkMinimumPlatformVersion(networkParameters.minimumPlatformVersion, requiredMinPlatformVersion, feature)

View File

@ -5,13 +5,20 @@ import net.corda.core.messaging.RPCOps
/** /**
* RPC operations to perform operations related to flows including management of associated persistent states like checkpoints. * RPC operations to perform operations related to flows including management of associated persistent states like checkpoints.
*/ */
@Deprecated(
"A public version of this interface has been exposed that should be interacted with using the MultiRPCClient",
ReplaceWith("net.corda.core.messaging.flows.FlowManagerRPCOps")
)
interface FlowManagerRPCOps : RPCOps { interface FlowManagerRPCOps : RPCOps {
/** /**
* Dump all the current flow checkpoints as JSON into a zip file in the node's log directory. * Dump all the current flow checkpoints as JSON into a zip file in the node's log directory.
*/ */
fun dumpCheckpoints() fun dumpCheckpoints()
/** Dump all the current flow checkpoints, alongside with the node's main jar, all CorDapps and driver jars /**
* into a zip file in the node's log directory. */ * Dump all the current flow checkpoints, alongside with the node's main jar, all CorDapps and driver jars into a zip file in the node's
* log directory.
*/
fun debugCheckpoints() fun debugCheckpoints()
} }

View File

@ -0,0 +1,20 @@
package net.corda.core.messaging.flows
import net.corda.core.messaging.RPCOps
/**
* RPC operations to perform operations related to flows including management of associated persistent states like checkpoints.
*/
interface FlowManagerRPCOps : RPCOps {
/**
* Dump all the current flow checkpoints as JSON into a zip file in the node's log directory.
*/
fun dumpCheckpoints()
/**
* Dump all the current flow checkpoints, alongside with the node's main jar, all CorDapps and driver jars into a zip file in the node's
* log directory.
*/
fun debugCheckpoints()
}

View File

@ -741,9 +741,11 @@ open class TransactionBuilder(
addReferenceState(resolvedStateAndRef.referenced()) addReferenceState(resolvedStateAndRef.referenced())
} }
} else { } else {
if (nextStatePointer.isResolved) {
log.warn("WARNING: You must pass in a ServiceHub reference to TransactionBuilder to resolve " + log.warn("WARNING: You must pass in a ServiceHub reference to TransactionBuilder to resolve " +
"state pointers outside of flows. If you are writing a unit test then pass in a " + "state pointers outside of flows. If you are writing a unit test then pass in a " +
"MockServices instance.") "MockServices instance.")
}
return return
} }
} }

View File

@ -1364,7 +1364,6 @@
<ID>ThrowsCount:AMQPTypeIdentifierParser.kt$AMQPTypeIdentifierParser$// Make sure our inputs aren't designed to blow things up. private fun validate(typeString: String)</ID> <ID>ThrowsCount:AMQPTypeIdentifierParser.kt$AMQPTypeIdentifierParser$// Make sure our inputs aren't designed to blow things up. private fun validate(typeString: String)</ID>
<ID>ThrowsCount:AbstractNode.kt$AbstractNode$private fun installCordaServices()</ID> <ID>ThrowsCount:AbstractNode.kt$AbstractNode$private fun installCordaServices()</ID>
<ID>ThrowsCount:ArtemisMessagingServer.kt$ArtemisMessagingServer$// TODO: Maybe wrap [IOException] on a key store load error so that it's clearly splitting key store loading from // Artemis IO errors @Throws(IOException::class, AddressBindingException::class, KeyStoreException::class) private fun configureAndStartServer()</ID> <ID>ThrowsCount:ArtemisMessagingServer.kt$ArtemisMessagingServer$// TODO: Maybe wrap [IOException] on a key store load error so that it's clearly splitting key store loading from // Artemis IO errors @Throws(IOException::class, AddressBindingException::class, KeyStoreException::class) private fun configureAndStartServer()</ID>
<ID>ThrowsCount:BrokerJaasLoginModule.kt$BaseBrokerJaasLoginModule$@Suppress("DEPRECATION") // should use java.security.cert.X509Certificate protected fun getUsernamePasswordAndCerts(): Triple&lt;String, String, Array&lt;javax.security.cert.X509Certificate&gt;?&gt;</ID>
<ID>ThrowsCount:CheckpointVerifier.kt$CheckpointVerifier$ fun verifyCheckpointsCompatible( checkpointStorage: CheckpointStorage, currentCordapps: List&lt;Cordapp&gt;, platformVersion: Int, serviceHub: ServiceHub, tokenizableServices: List&lt;Any&gt; )</ID> <ID>ThrowsCount:CheckpointVerifier.kt$CheckpointVerifier$ fun verifyCheckpointsCompatible( checkpointStorage: CheckpointStorage, currentCordapps: List&lt;Cordapp&gt;, platformVersion: Int, serviceHub: ServiceHub, tokenizableServices: List&lt;Any&gt; )</ID>
<ID>ThrowsCount:CheckpointVerifier.kt$CheckpointVerifier$// Throws exception when the flow is incompatible private fun checkFlowCompatible(subFlow: SubFlow, currentCordappsByHash: Map&lt;SecureHash.SHA256, Cordapp&gt;, platformVersion: Int)</ID> <ID>ThrowsCount:CheckpointVerifier.kt$CheckpointVerifier$// Throws exception when the flow is incompatible private fun checkFlowCompatible(subFlow: SubFlow, currentCordappsByHash: Map&lt;SecureHash.SHA256, Cordapp&gt;, platformVersion: Int)</ID>
<ID>ThrowsCount:ClassCarpenter.kt$ClassCarpenterImpl$ private fun validateSchema(schema: Schema)</ID> <ID>ThrowsCount:ClassCarpenter.kt$ClassCarpenterImpl$ private fun validateSchema(schema: Schema)</ID>

View File

@ -33,47 +33,34 @@ shadowJar {
} }
enum ImageVariant { enum ImageVariant {
UBUNTU_ZULU("zulu", "Dockerfile", "1.8"), UBUNTU_ZULU("Dockerfile", "1.8", "zulu-openjdk8"),
UBUNTU_ZULU_11("zulu", "Dockerfile11", "11"), UBUNTU_ZULU_11("Dockerfile11", "11", "zulu-openjdk11"),
AL_CORRETTO("corretto", "DockerfileAL", "1.8"), AL_CORRETTO("DockerfileAL", "1.8", "amazonlinux2"),
OFFICIAL(UBUNTU_ZULU) OFFICIAL(UBUNTU_ZULU)
String knownAs
String dockerFile String dockerFile
String javaVersion String javaVersion
String baseImgaeFullName
String versionString(String baseTag, String version) {
return "${baseTag}-${knownAs}" +
(knownAs.isEmpty() ? "" : "-") +
"java${javaVersion}-" + version
}
ImageVariant(ImageVariant other) { ImageVariant(ImageVariant other) {
this.knownAs = other.knownAs
this.dockerFile = other.dockerFile this.dockerFile = other.dockerFile
this.javaVersion = other.javaVersion this.javaVersion = other.javaVersion
this.baseImgaeFullName = other.baseImgaeFullName
} }
ImageVariant(String knownAs, String dockerFile, String javaVersion) { ImageVariant(String dockerFile, String javaVersion, String baseImgaeFullName) {
this.knownAs = knownAs
this.dockerFile = dockerFile this.dockerFile = dockerFile
this.javaVersion = javaVersion this.javaVersion = javaVersion
this.baseImgaeFullName = baseImgaeFullName
} }
static final String getRepository(Project project) { static final String getRepository(Project project) {
return project.properties.getOrDefault("docker.image.repository", "corda/corda") return project.properties.getOrDefault("docker.image.repository", "corda/corda")
} }
static private final String runTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
def getName(Project project) {
return versionString(getRepository(project), project.version.toString().toLowerCase())
}
Set<Identifier> buildTags(Project project) { Set<Identifier> buildTags(Project project) {
final String suffix = project.version.toString().toLowerCase().contains("snapshot") ? runTime : "RELEASE" return ["${project.version.toString().toLowerCase()}-${baseImgaeFullName}"].stream().map {
return [suffix, "latest"].stream().map { toAppend -> "${getRepository(project)}:${toAppend}".toString()
toAppend -> "${getName(project)}:${toAppend}".toString()
}.map(Identifier.&fromCompoundString).collect(Collectors.toSet()) }.map(Identifier.&fromCompoundString).collect(Collectors.toSet())
} }

View File

@ -1,8 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
NODE_LIST=("dockerNode1" "dockerNode2" "dockerNode3") NODE_LIST=("dockerNode1" "dockerNode2" "dockerNode3")
NETWORK_NAME=mininet NETWORK_NAME=mininet
CORDAPP_VERSION="4.8-SNAPSHOT" CORDAPP_VERSION="4.9-SNAPSHOT"
DOCKER_IMAGE_VERSION="corda-zulu-4.8-snapshot" DOCKER_IMAGE_VERSION="corda-zulu-4.9-snapshot"
mkdir cordapps mkdir cordapps
rm -f cordapps/* rm -f cordapps/*

View File

@ -1,4 +1,4 @@
FROM azul/zulu-openjdk:8u312 FROM azul/zulu-openjdk:8u382
## Remove Azul Zulu repo, as it is gone by now ## Remove Azul Zulu repo, as it is gone by now
RUN rm -rf /etc/apt/sources.list.d/zulu.list RUN rm -rf /etc/apt/sources.list.d/zulu.list

View File

@ -1,4 +1,4 @@
FROM azul/zulu-openjdk:8u312 FROM azul/zulu-openjdk:8u382
## Add packages, clean cache, create dirs, create corda user and change ownership ## Add packages, clean cache, create dirs, create corda user and change ownership
RUN apt-get update && \ RUN apt-get update && \

View File

@ -1,11 +1,10 @@
FROM amazonlinux:2 FROM amazoncorretto:8u382-al2
## Add packages, clean cache, create dirs, create corda user and change ownership ## Add packages, clean cache, create dirs, create corda user and change ownership
RUN amazon-linux-extras enable corretto8 && \ RUN yum -y install bash && \
yum -y install java-1.8.0-amazon-corretto-devel && \
yum -y install bash && \
yum -y install curl && \ yum -y install curl && \
yum -y install unzip && \ yum -y install unzip && \
yum -y install shadow-utils.x86_64 && \
yum clean all && \ yum clean all && \
rm -rf /var/cache/yum && \ rm -rf /var/cache/yum && \
mkdir -p /opt/corda/cordapps && \ mkdir -p /opt/corda/cordapps && \

View File

@ -56,10 +56,10 @@ task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) {
url = new URL("https://docs.oracle.com/javafx/2/api/") url = new URL("https://docs.oracle.com/javafx/2/api/")
} }
externalDocumentationLink { externalDocumentationLink {
url = new URL("https://downloads.bouncycastle.org/java/docs/bcpkix-jdk15to18-javadoc/") url = new URL("https://downloads.bouncycastle.org/java/docs/bcpkix-jdk18on-javadoc/")
} }
externalDocumentationLink { externalDocumentationLink {
url = new URL("https://downloads.bouncycastle.org/java/docs/bcprov-jdk15to18-javadoc/") url = new URL("https://downloads.bouncycastle.org/java/docs/bcprov-jdk18on-javadoc/")
} }
internalPackagePrefixes.collect { packagePrefix -> internalPackagePrefixes.collect { packagePrefix ->
perPackageOption { perPackageOption {

View File

@ -17,7 +17,9 @@ dependencies {
// TODO: remove the forced update of commons-collections and beanutils when artemis updates them // TODO: remove the forced update of commons-collections and beanutils when artemis updates them
compile "org.apache.commons:commons-collections4:${commons_collections_version}" compile "org.apache.commons:commons-collections4:${commons_collections_version}"
compile "commons-beanutils:commons-beanutils:${beanutils_version}" compile "commons-beanutils:commons-beanutils:${beanutils_version}"
compile "org.apache.activemq:artemis-core-client:${artemis_version}" compile("org.apache.activemq:artemis-core-client:${artemis_version}") {
exclude group: 'org.jgroups', module: 'jgroups'
}
compile "org.apache.activemq:artemis-commons:${artemis_version}" compile "org.apache.activemq:artemis-commons:${artemis_version}"
compile "io.netty:netty-handler-proxy:$netty_version" compile "io.netty:netty-handler-proxy:$netty_version"
@ -65,6 +67,7 @@ dependencies {
compile ("org.apache.activemq:artemis-amqp-protocol:${artemis_version}") { compile ("org.apache.activemq:artemis-amqp-protocol:${artemis_version}") {
// Gains our proton-j version from core module. // Gains our proton-j version from core module.
exclude group: 'org.apache.qpid', module: 'proton-j' exclude group: 'org.apache.qpid', module: 'proton-j'
exclude group: 'org.jgroups', module: 'jgroups'
} }
} }

View File

@ -67,7 +67,6 @@ class ArtemisMessagingClient(private val config: MutualSslConfiguration,
retryInterval = messagingServerConnectionConfig.retryInterval().toMillis() retryInterval = messagingServerConnectionConfig.retryInterval().toMillis()
retryIntervalMultiplier = messagingServerConnectionConfig.retryIntervalMultiplier() retryIntervalMultiplier = messagingServerConnectionConfig.retryIntervalMultiplier()
maxRetryInterval = messagingServerConnectionConfig.maxRetryInterval(isHA).toMillis() maxRetryInterval = messagingServerConnectionConfig.maxRetryInterval(isHA).toMillis()
isFailoverOnInitialConnection = messagingServerConnectionConfig.failoverOnInitialAttempt(isHA)
initialConnectAttempts = messagingServerConnectionConfig.initialConnectAttempts(isHA) initialConnectAttempts = messagingServerConnectionConfig.initialConnectAttempts(isHA)
} }
addIncomingInterceptor(ArtemisMessageSizeChecksInterceptor(maxMessageSize)) addIncomingInterceptor(ArtemisMessageSizeChecksInterceptor(maxMessageSize))

View File

@ -53,7 +53,7 @@ class ArtemisTcpTransport {
keyStore?.let { keyStore?.let {
with (it) { with (it) {
path.requireOnDefaultFileSystem() path.requireOnDefaultFileSystem()
options[TransportConstants.KEYSTORE_PROVIDER_PROP_NAME] = "JKS" options[TransportConstants.KEYSTORE_TYPE_PROP_NAME] = "JKS"
options[TransportConstants.KEYSTORE_PATH_PROP_NAME] = path options[TransportConstants.KEYSTORE_PATH_PROP_NAME] = path
options[TransportConstants.KEYSTORE_PASSWORD_PROP_NAME] = get().password options[TransportConstants.KEYSTORE_PASSWORD_PROP_NAME] = get().password
} }
@ -61,7 +61,7 @@ class ArtemisTcpTransport {
trustStore?.let { trustStore?.let {
with (it) { with (it) {
path.requireOnDefaultFileSystem() path.requireOnDefaultFileSystem()
options[TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME] = "JKS" options[TransportConstants.TRUSTSTORE_TYPE_PROP_NAME] = "JKS"
options[TransportConstants.TRUSTSTORE_PATH_PROP_NAME] = path options[TransportConstants.TRUSTSTORE_PATH_PROP_NAME] = path
options[TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME] = get().password options[TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME] = get().password
} }
@ -72,13 +72,13 @@ class ArtemisTcpTransport {
private fun ClientRpcSslOptions.toTransportOptions() = mapOf( private fun ClientRpcSslOptions.toTransportOptions() = mapOf(
TransportConstants.SSL_ENABLED_PROP_NAME to true, TransportConstants.SSL_ENABLED_PROP_NAME to true,
TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME to trustStoreProvider, TransportConstants.TRUSTSTORE_TYPE_PROP_NAME to trustStoreProvider,
TransportConstants.TRUSTSTORE_PATH_PROP_NAME to trustStorePath, TransportConstants.TRUSTSTORE_PATH_PROP_NAME to trustStorePath,
TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to trustStorePassword) TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to trustStorePassword)
private fun BrokerRpcSslOptions.toTransportOptions() = mapOf( private fun BrokerRpcSslOptions.toTransportOptions() = mapOf(
TransportConstants.SSL_ENABLED_PROP_NAME to true, TransportConstants.SSL_ENABLED_PROP_NAME to true,
TransportConstants.KEYSTORE_PROVIDER_PROP_NAME to "JKS", TransportConstants.KEYSTORE_TYPE_PROP_NAME to "JKS",
TransportConstants.KEYSTORE_PATH_PROP_NAME to keyStorePath, TransportConstants.KEYSTORE_PATH_PROP_NAME to keyStorePath,
TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to keyStorePassword, TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to keyStorePassword,
TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to false) TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to false)
@ -186,10 +186,7 @@ class ArtemisTcpTransport {
options[TransportConstants.HANDSHAKE_TIMEOUT] = 0 options[TransportConstants.HANDSHAKE_TIMEOUT] = 0
if (trustManagerFactory != null) { if (trustManagerFactory != null) {
// NettyAcceptor only creates default TrustManagerFactorys with the provided trust store details. However, we need to use // NettyAcceptor only creates default TrustManagerFactorys with the provided trust store details. However, we need to use
// more customised instances which use our revocation checkers, which we pass directly into NodeNettyAcceptorFactory. // more customised instances which use our revocation checkers, so we pass them in, to be picked up by Node(Open)SSLContextFactory.
//
// This, however, requires copying a lot of code from NettyAcceptor into NodeNettyAcceptor. The version of Artemis in
// Corda 4.9 solves this problem by introducing a "trustManagerFactoryPlugin" config option.
options[TRUST_MANAGER_FACTORY_NAME] = trustManagerFactory options[TRUST_MANAGER_FACTORY_NAME] = trustManagerFactory
} }
return createTransport( return createTransport(
@ -211,6 +208,10 @@ class ArtemisTcpTransport {
threadPoolName: String, threadPoolName: String,
trace: Boolean, trace: Boolean,
remotingThreads: Int?): TransportConfiguration { remotingThreads: Int?): TransportConfiguration {
if (enableSSL) {
// This is required to stop Client checking URL address vs. Server provided certificate
options[TransportConstants.VERIFY_HOST_PROP_NAME] = false
}
return createTransport( return createTransport(
CordaNettyConnectorFactory::class.java.name, CordaNettyConnectorFactory::class.java.name,
hostAndPort, hostAndPort,

View File

@ -10,11 +10,11 @@ import org.apache.activemq.artemis.protocol.amqp.broker.AmqpInterceptor
import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection
class ArtemisMessageSizeChecksInterceptor(maxMessageSize: Int) : MessageSizeChecksInterceptor<Packet>(maxMessageSize), Interceptor { class ArtemisMessageSizeChecksInterceptor(maxMessageSize: Int) : MessageSizeChecksInterceptor<Packet>(maxMessageSize), Interceptor {
override fun getMessageSize(packet: Packet?): Int? { override fun getMessageSize(packet: Packet?): Long? {
return when (packet) { return when (packet) {
// This is an estimate of how much memory a Message body takes up. // This is an estimate of how much memory a Message body takes up.
// Note, it is only an estimate // Note, it is only an estimate
is MessagePacket -> (packet.message.persistentSize - packet.message.headersAndPropertiesEncodeSize - 4).toInt() is MessagePacket -> (packet.message.persistentSize - packet.message.headersAndPropertiesEncodeSize - 4)
// Skip all artemis control messages. // Skip all artemis control messages.
else -> null else -> null
} }
@ -22,7 +22,7 @@ class ArtemisMessageSizeChecksInterceptor(maxMessageSize: Int) : MessageSizeChec
} }
class AmqpMessageSizeChecksInterceptor(maxMessageSize: Int) : MessageSizeChecksInterceptor<AMQPMessage>(maxMessageSize), AmqpInterceptor { class AmqpMessageSizeChecksInterceptor(maxMessageSize: Int) : MessageSizeChecksInterceptor<AMQPMessage>(maxMessageSize), AmqpInterceptor {
override fun getMessageSize(packet: AMQPMessage?): Int? = packet?.encodeSize override fun getMessageSize(packet: AMQPMessage?): Long? = packet?.wholeMessageSize
} }
/** /**
@ -45,6 +45,6 @@ sealed class MessageSizeChecksInterceptor<T : Any>(private val maxMessageSize: I
} }
// get size of the message in byte, returns null if the message is null or size don't need to be checked. // get size of the message in byte, returns null if the message is null or size don't need to be checked.
abstract fun getMessageSize(packet: T?): Int? abstract fun getMessageSize(packet: T?): Long?
} }

View File

@ -201,9 +201,13 @@ open class AMQPBridgeManager(keyStore: CertificateStore,
logInfoWithMDC("Stopping Artemis because stopping AMQP bridge") logInfoWithMDC("Stopping Artemis because stopping AMQP bridge")
closeConsumer() closeConsumer()
consumer = null consumer = null
val closingSession = session
eventLoop.execute { eventLoop.execute {
artemis(ArtemisState.STOPPING) { artemis(ArtemisState.STOPPING) {
stopSession() stopSession(session)
if(session != closingSession) {
stopSession(closingSession)
}
session = null session = null
ArtemisState.STOPPED ArtemisState.STOPPED
} }
@ -271,21 +275,30 @@ open class AMQPBridgeManager(keyStore: CertificateStore,
logInfoWithMDC("Stopping Artemis because AMQP bridge disconnected") logInfoWithMDC("Stopping Artemis because AMQP bridge disconnected")
closeConsumer() closeConsumer()
consumer = null consumer = null
val closingSession = session
eventLoop.execute { eventLoop.execute {
synchronized(artemis!!) {
if (session == closingSession) {
artemis(ArtemisState.STOPPING) { artemis(ArtemisState.STOPPING) {
stopSession() stopSession(session)
session = null session = null
when (precedingState) { when (precedingState) {
ArtemisState.AMQP_STOPPED -> ArtemisState.AMQP_STOPPED ->
ArtemisState.STOPPED_AMQP_START_SCHEDULED(scheduledArtemis(artemisHeartbeatPlusBackoff, ArtemisState.STOPPED_AMQP_START_SCHEDULED(scheduledArtemis(artemisHeartbeatPlusBackoff,
TimeUnit.MILLISECONDS, ArtemisState.AMQP_STARTING) { startOutbound() }) TimeUnit.MILLISECONDS, ArtemisState.AMQP_STARTING) { startOutbound() })
ArtemisState.AMQP_RESTARTED -> { ArtemisState.AMQP_RESTARTED -> {
artemis(ArtemisState.AMQP_STARTING) { startOutbound() } artemis(ArtemisState.AMQP_STARTING) { startOutbound() }
ArtemisState.AMQP_STARTING ArtemisState.AMQP_STARTING
} }
else -> ArtemisState.STOPPED else -> ArtemisState.STOPPED
} }
} }
} else {
stopSession(closingSession)
}
}
} }
ArtemisState.STOPPING ArtemisState.STOPPING
} }
@ -339,10 +352,10 @@ open class AMQPBridgeManager(keyStore: CertificateStore,
} }
} }
private fun stopSession(): Boolean { private fun stopSession(localSession: ClientSession?): Boolean {
var stopped = false var stopped = false
try { try {
session?.apply { localSession?.apply {
if (!isClosed) { if (!isClosed) {
stop() stop()
} }
@ -356,7 +369,7 @@ open class AMQPBridgeManager(keyStore: CertificateStore,
} }
private fun restartSession(): Boolean { private fun restartSession(): Boolean {
if (!stopSession()) { if (!stopSession(session)) {
// Session timed out stopping. The request/responses can be out of sequence on the session now, so abandon it. // Session timed out stopping. The request/responses can be out of sequence on the session now, so abandon it.
session = null session = null
// The consumer is also dead now too as attached to the dead session. // The consumer is also dead now too as attached to the dead session.

View File

@ -17,6 +17,7 @@ import net.corda.nodeapi.internal.protonwrapper.netty.ProxyConfig
import net.corda.nodeapi.internal.protonwrapper.netty.RevocationConfig import net.corda.nodeapi.internal.protonwrapper.netty.RevocationConfig
import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException
import org.apache.activemq.artemis.api.core.ActiveMQQueueExistsException import org.apache.activemq.artemis.api.core.ActiveMQQueueExistsException
import org.apache.activemq.artemis.api.core.QueueConfiguration
import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.RoutingType
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.client.ClientConsumer import org.apache.activemq.artemis.api.core.client.ClientConsumer
@ -105,7 +106,9 @@ class BridgeControlListener(private val keyStore: CertificateStore,
private fun registerBridgeControlListener(artemisSession: ClientSession) { private fun registerBridgeControlListener(artemisSession: ClientSession) {
try { try {
artemisSession.createTemporaryQueue(BRIDGE_CONTROL, RoutingType.MULTICAST, bridgeControlQueue) artemisSession.createQueue(
QueueConfiguration(bridgeControlQueue).setAddress(BRIDGE_CONTROL).setRoutingType(RoutingType.MULTICAST)
.setTemporary(true).setDurable(false))
} catch (ex: ActiveMQQueueExistsException) { } catch (ex: ActiveMQQueueExistsException) {
// Ignore if there is a queue still not cleaned up // Ignore if there is a queue still not cleaned up
} }
@ -125,7 +128,9 @@ class BridgeControlListener(private val keyStore: CertificateStore,
private fun registerBridgeDuplicateChecker(artemisSession: ClientSession) { private fun registerBridgeDuplicateChecker(artemisSession: ClientSession) {
try { try {
artemisSession.createTemporaryQueue(BRIDGE_NOTIFY, RoutingType.MULTICAST, bridgeNotifyQueue) artemisSession.createQueue(
QueueConfiguration(bridgeNotifyQueue).setAddress(BRIDGE_NOTIFY).setRoutingType(RoutingType.MULTICAST)
.setTemporary(true).setDurable(false))
} catch (ex: ActiveMQQueueExistsException) { } catch (ex: ActiveMQQueueExistsException) {
// Ignore if there is a queue still not cleaned up // Ignore if there is a queue still not cleaned up
} }

View File

@ -15,7 +15,6 @@ import java.time.Duration
* *
* totalFailoverDuration = 5 + 5 * 1.5 + 5 * (1.5)^2 + 5 * (1.5)^3 + 5 * (1.5)^4 = ~66 seconds * totalFailoverDuration = 5 + 5 * 1.5 + 5 * (1.5)^2 + 5 * (1.5)^3 + 5 * (1.5)^4 = ~66 seconds
* *
* @param failoverOnInitialAttempt Determines whether failover is triggered if initial connection fails.
* @param initialConnectAttempts The number of reconnect attempts if failover is enabled for initial connection. A value * @param initialConnectAttempts The number of reconnect attempts if failover is enabled for initial connection. A value
* of -1 represents infinite attempts. * of -1 represents infinite attempts.
* @param reconnectAttempts The number of reconnect attempts for failover after initial connection is done. A value * @param reconnectAttempts The number of reconnect attempts for failover after initial connection is done. A value
@ -27,7 +26,6 @@ import java.time.Duration
enum class MessagingServerConnectionConfiguration { enum class MessagingServerConnectionConfiguration {
DEFAULT { DEFAULT {
override fun failoverOnInitialAttempt(isHa: Boolean) = true
override fun initialConnectAttempts(isHa: Boolean) = 5 override fun initialConnectAttempts(isHa: Boolean) = 5
override fun reconnectAttempts(isHa: Boolean) = 5 override fun reconnectAttempts(isHa: Boolean) = 5
override fun retryInterval() = 5.seconds override fun retryInterval() = 5.seconds
@ -36,7 +34,6 @@ enum class MessagingServerConnectionConfiguration {
}, },
FAIL_FAST { FAIL_FAST {
override fun failoverOnInitialAttempt(isHa: Boolean) = isHa
override fun initialConnectAttempts(isHa: Boolean) = 0 override fun initialConnectAttempts(isHa: Boolean) = 0
// Client die too fast during failover/failback, need a few reconnect attempts to allow new master to become active // Client die too fast during failover/failback, need a few reconnect attempts to allow new master to become active
override fun reconnectAttempts(isHa: Boolean) = if (isHa) 3 else 0 override fun reconnectAttempts(isHa: Boolean) = if (isHa) 3 else 0
@ -46,7 +43,6 @@ enum class MessagingServerConnectionConfiguration {
}, },
CONTINUOUS_RETRY { CONTINUOUS_RETRY {
override fun failoverOnInitialAttempt(isHa: Boolean) = true
override fun initialConnectAttempts(isHa: Boolean) = if (isHa) 0 else -1 override fun initialConnectAttempts(isHa: Boolean) = if (isHa) 0 else -1
override fun reconnectAttempts(isHa: Boolean) = -1 override fun reconnectAttempts(isHa: Boolean) = -1
override fun retryInterval() = 5.seconds override fun retryInterval() = 5.seconds
@ -54,7 +50,6 @@ enum class MessagingServerConnectionConfiguration {
override fun maxRetryInterval(isHa: Boolean) = if (isHa) 3.minutes else 5.minutes override fun maxRetryInterval(isHa: Boolean) = if (isHa) 3.minutes else 5.minutes
}; };
abstract fun failoverOnInitialAttempt(isHa: Boolean): Boolean
abstract fun initialConnectAttempts(isHa: Boolean): Int abstract fun initialConnectAttempts(isHa: Boolean): Int
abstract fun reconnectAttempts(isHa: Boolean): Int abstract fun reconnectAttempts(isHa: Boolean): Int
abstract fun retryInterval(): Duration abstract fun retryInterval(): Duration

View File

@ -112,8 +112,6 @@ class DatabaseTransaction(
} finally { } finally {
clearException() clearException()
contextTransactionOrNull = outerTransaction contextTransactionOrNull = outerTransaction
}
if (outerTransaction == null) { if (outerTransaction == null) {
synchronized(this) { synchronized(this) {
closed = true closed = true
@ -121,6 +119,7 @@ class DatabaseTransaction(
} }
} }
} }
}
fun onCommit(callback: () -> Unit) { fun onCommit(callback: () -> Unit) {
boundary.filter { it.success }.subscribe { callback() } boundary.filter { it.success }.subscribe { callback() }

View File

@ -47,6 +47,7 @@ internal class ConnectionStateMachine(private val serverMode: Boolean,
companion object { companion object {
private const val CORDA_AMQP_FRAME_SIZE_PROP_NAME = "net.corda.nodeapi.connectionstatemachine.AmqpMaxFrameSize" private const val CORDA_AMQP_FRAME_SIZE_PROP_NAME = "net.corda.nodeapi.connectionstatemachine.AmqpMaxFrameSize"
private const val CORDA_AMQP_IDLE_TIMEOUT_PROP_NAME = "net.corda.nodeapi.connectionstatemachine.AmqpIdleTimeout" private const val CORDA_AMQP_IDLE_TIMEOUT_PROP_NAME = "net.corda.nodeapi.connectionstatemachine.AmqpIdleTimeout"
private const val CREATE_ADDRESS_PERMISSION_ERROR = "AMQ229032"
private val MAX_FRAME_SIZE = Integer.getInteger(CORDA_AMQP_FRAME_SIZE_PROP_NAME, 128 * 1024) private val MAX_FRAME_SIZE = Integer.getInteger(CORDA_AMQP_FRAME_SIZE_PROP_NAME, 128 * 1024)
private val IDLE_TIMEOUT = Integer.getInteger(CORDA_AMQP_IDLE_TIMEOUT_PROP_NAME, 10 * 1000) private val IDLE_TIMEOUT = Integer.getInteger(CORDA_AMQP_IDLE_TIMEOUT_PROP_NAME, 10 * 1000)
@ -351,13 +352,34 @@ internal class ConnectionStateMachine(private val serverMode: Boolean,
override fun onLinkRemoteClose(e: Event) { override fun onLinkRemoteClose(e: Event) {
val link = e.link val link = e.link
if (link.remoteCondition != null) { if (link.remoteCondition != null) {
logWarnWithMDC("Connection closed due to error on remote side: `${link.remoteCondition.description}`") val remoteConditionDescription = link.remoteCondition.description
logWarnWithMDC("Connection closed due to error on remote side: `$remoteConditionDescription`")
// Description normally looks as follows:
// "AMQ229032: User: SystemUsers/Peer does not have permission='CREATE_ADDRESS' on address p2p.inbound.Test"
if (remoteConditionDescription.contains(CREATE_ADDRESS_PERMISSION_ERROR)) {
handleRemoteCreatePermissionError(e)
}
transport.condition = link.condition transport.condition = link.condition
transport.close_tail() transport.close_tail()
transport.pop(Math.max(0, transport.pending())) // Force generation of TRANSPORT_HEAD_CLOSE (not in C code) transport.pop(Math.max(0, transport.pending())) // Force generation of TRANSPORT_HEAD_CLOSE (not in C code)
} }
} }
/**
* If an the artemis channel does not exist on the counterparty, then a create permission error is returned in the [event].
* Do not retry messages to this channel as it will result in an infinite loop of retries.
* Log the error, mark the messages as acknowledged and clear them from the message queue.
*/
private fun handleRemoteCreatePermissionError(event: Event) {
val remoteP2PAddress = event.sender.source.address
logWarnWithMDC("Address does not exist on peer: $remoteP2PAddress. Marking messages sent to this address as Acknowledged.")
messageQueues[remoteP2PAddress]?.apply {
forEach { it.doComplete(MessageStatus.Acknowledged) }
clear()
}
}
override fun onLinkFinal(event: Event) { override fun onLinkFinal(event: Event) {
val link = event.link val link = event.link
if (link is Sender) { if (link is Sender) {

View File

@ -300,6 +300,7 @@ internal class AMQPChannelHandler(private val serverMode: Boolean,
cause is SSLException && (cause.message?.contains("close_notify") == true) -> logWarnWithMDC("Received close_notify during handshake") cause is SSLException && (cause.message?.contains("close_notify") == true) -> logWarnWithMDC("Received close_notify during handshake")
// io.netty.handler.ssl.SslHandler.setHandshakeFailureTransportFailure() // io.netty.handler.ssl.SslHandler.setHandshakeFailureTransportFailure()
cause is SSLException && (cause.message?.contains("writing TLS control frames") == true) -> logWarnWithMDC(cause.message!!) cause is SSLException && (cause.message?.contains("writing TLS control frames") == true) -> logWarnWithMDC(cause.message!!)
cause is SSLException && (cause.message?.contains("internal_error") == true) -> logWarnWithMDC("Received internal_error during handshake")
else -> badCert = true else -> badCert = true
} }
if (log.isTraceEnabled) { if (log.isTraceEnabled) {

View File

@ -94,7 +94,7 @@ processTestResources {
dependencies { dependencies {
compile project(':node-api') compile project(':node-api')
compile project(':client:rpc') compile project(':client:rpc')
compile project(':tools:shell') compile project(':client:jackson')
compile project(':tools:cliutils') compile project(':tools:cliutils')
compile project(':common-validation') compile project(':common-validation')
compile project(':common-configuration-parsing') compile project(':common-configuration-parsing')
@ -127,11 +127,17 @@ dependencies {
// TODO: remove the forced update of commons-collections and beanutils when artemis updates them // TODO: remove the forced update of commons-collections and beanutils when artemis updates them
compile "org.apache.commons:commons-collections4:${commons_collections_version}" compile "org.apache.commons:commons-collections4:${commons_collections_version}"
compile "commons-beanutils:commons-beanutils:${beanutils_version}" compile "commons-beanutils:commons-beanutils:${beanutils_version}"
compile "org.apache.activemq:artemis-server:${artemis_version}" compile("org.apache.activemq:artemis-server:${artemis_version}") {
compile "org.apache.activemq:artemis-core-client:${artemis_version}" exclude group: 'org.apache.commons', module: 'commons-dbcp2'
exclude group: 'org.jgroups', module: 'jgroups'
}
compile("org.apache.activemq:artemis-core-client:${artemis_version}") {
exclude group: 'org.jgroups', module: 'jgroups'
}
runtime("org.apache.activemq:artemis-amqp-protocol:${artemis_version}") { runtime("org.apache.activemq:artemis-amqp-protocol:${artemis_version}") {
// Gains our proton-j version from core module. // Gains our proton-j version from core module.
exclude group: 'org.apache.qpid', module: 'proton-j' exclude group: 'org.apache.qpid', module: 'proton-j'
exclude group: 'org.jgroups', module: 'jgroups'
} }
// Manifests: for reading stuff from the manifest file // Manifests: for reading stuff from the manifest file
@ -200,7 +206,6 @@ dependencies {
// BFT-Smart dependencies // BFT-Smart dependencies
compile 'com.github.bft-smart:library:master-v1.1-beta-g6215ec8-87' compile 'com.github.bft-smart:library:master-v1.1-beta-g6215ec8-87'
compile 'commons-codec:commons-codec:1.13'
// Java Atomix: RAFT library // Java Atomix: RAFT library
compile 'io.atomix.copycat:copycat-client:1.2.3' compile 'io.atomix.copycat:copycat-client:1.2.3'

View File

@ -3,6 +3,7 @@ package net.corda.node
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClient
import net.corda.client.rpc.PermissionException import net.corda.client.rpc.PermissionException
import net.corda.client.rpc.RPCException
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
@ -151,7 +152,7 @@ class AuthDBTests : NodeBasedTest(cordappPackages = CORDAPPS) {
proxy.stateMachinesFeed() proxy.stateMachinesFeed()
assertFailsWith( assertFailsWith(
PermissionException::class, PermissionException::class,
"This user should not be authorized to call 'nodeInfo'") { "This user should not be authorized to call 'stateMachinesFeed'") {
proxy.nodeInfo() proxy.nodeInfo()
} }
} }
@ -185,7 +186,7 @@ class AuthDBTests : NodeBasedTest(cordappPackages = CORDAPPS) {
val proxy = it.proxy val proxy = it.proxy
assertFailsWith( assertFailsWith(
PermissionException::class, PermissionException::class,
"This user should not be authorized to call 'nodeInfo'") { "This user should not be authorized to call 'stateMachinesFeed'") {
proxy.stateMachinesFeed() proxy.stateMachinesFeed()
} }
db.addRoleToUser("user3", "default") db.addRoleToUser("user3", "default")
@ -207,8 +208,8 @@ class AuthDBTests : NodeBasedTest(cordappPackages = CORDAPPS) {
db.deleteUser("user4") db.deleteUser("user4")
Thread.sleep(1500) Thread.sleep(1500)
assertFailsWith( assertFailsWith(
PermissionException::class, RPCException::class,
"This user should not be authorized to call 'nodeInfo'") { "This user should not be authorized to call 'stateMachinesFeed'") {
proxy.stateMachinesFeed() proxy.stateMachinesFeed()
} }
} }

View File

@ -13,6 +13,7 @@ import kotlin.test.assertTrue
class NodeRPCTests { class NodeRPCTests {
private val CORDA_VERSION_REGEX = "\\d+(\\.\\d+)?(\\.\\d+)?(-\\w+)?".toRegex() private val CORDA_VERSION_REGEX = "\\d+(\\.\\d+)?(\\.\\d+)?(-\\w+)?".toRegex()
private val CORDA_VENDOR = "Corda Open Source" private val CORDA_VENDOR = "Corda Open Source"
private val CORDA_VENDOR_CE = "Corda Community Edition"
private val CORDAPPS = listOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP) private val CORDAPPS = listOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP)
private val CORDAPP_TYPES = setOf("Contract CorDapp", "Workflow CorDapp") private val CORDAPP_TYPES = setOf("Contract CorDapp", "Workflow CorDapp")
private val CLASSIFIER = if (SystemUtils.IS_JAVA_11) "-jdk11" else "" private val CLASSIFIER = if (SystemUtils.IS_JAVA_11) "-jdk11" else ""
@ -29,7 +30,7 @@ class NodeRPCTests {
val nodeDiagnosticInfo = startNode().get().rpc.nodeDiagnosticInfo() val nodeDiagnosticInfo = startNode().get().rpc.nodeDiagnosticInfo()
assertTrue(nodeDiagnosticInfo.version.matches(CORDA_VERSION_REGEX)) assertTrue(nodeDiagnosticInfo.version.matches(CORDA_VERSION_REGEX))
assertEquals(PLATFORM_VERSION, nodeDiagnosticInfo.platformVersion) assertEquals(PLATFORM_VERSION, nodeDiagnosticInfo.platformVersion)
assertEquals(CORDA_VENDOR, nodeDiagnosticInfo.vendor) assertTrue(nodeDiagnosticInfo.vendor == CORDA_VENDOR || nodeDiagnosticInfo.vendor == CORDA_VENDOR_CE)
nodeDiagnosticInfo.cordapps.forEach { println("${it.shortName} ${it.type}") } nodeDiagnosticInfo.cordapps.forEach { println("${it.shortName} ${it.type}") }
assertEquals(CORDAPPS.size, nodeDiagnosticInfo.cordapps.size) assertEquals(CORDAPPS.size, nodeDiagnosticInfo.cordapps.size)
assertEquals(CORDAPP_TYPES, nodeDiagnosticInfo.cordapps.map { it.type }.toSet()) assertEquals(CORDAPP_TYPES, nodeDiagnosticInfo.cordapps.map { it.type }.toSet())

View File

@ -24,6 +24,7 @@ import net.corda.coretesting.internal.rigorousMock
import net.corda.coretesting.internal.stubs.CertificateStoreStubs import net.corda.coretesting.internal.stubs.CertificateStoreStubs
import net.corda.nodeapi.internal.protonwrapper.netty.toRevocationConfig import net.corda.nodeapi.internal.protonwrapper.netty.toRevocationConfig
import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID
import org.apache.activemq.artemis.api.core.QueueConfiguration
import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.RoutingType
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertArrayEquals
@ -222,7 +223,8 @@ class AMQPBridgeTest {
val artemis = artemisClient.started!! val artemis = artemisClient.started!!
if (sourceQueueName != null) { if (sourceQueueName != null) {
// Local queue for outgoing messages // Local queue for outgoing messages
artemis.session.createQueue(sourceQueueName, RoutingType.ANYCAST, sourceQueueName, true) artemis.session.createQueue(
QueueConfiguration(sourceQueueName).setRoutingType(RoutingType.ANYCAST).setAddress(sourceQueueName).setDurable(true))
bridgeManager.deployBridge(ALICE_NAME.toString(), sourceQueueName, listOf(amqpAddress), setOf(BOB.name)) bridgeManager.deployBridge(ALICE_NAME.toString(), sourceQueueName, listOf(amqpAddress), setOf(BOB.name))
} }
return Triple(artemisServer, artemisClient, bridgeManager) return Triple(artemisServer, artemisClient, bridgeManager)

View File

@ -37,6 +37,7 @@ import net.corda.testing.node.internal.network.CrlServer
import net.corda.testing.node.internal.network.CrlServer.Companion.EMPTY_CRL import net.corda.testing.node.internal.network.CrlServer.Companion.EMPTY_CRL
import net.corda.testing.node.internal.network.CrlServer.Companion.NODE_CRL import net.corda.testing.node.internal.network.CrlServer.Companion.NODE_CRL
import net.corda.testing.node.internal.network.CrlServer.Companion.withCrlDistPoint import net.corda.testing.node.internal.network.CrlServer.Companion.withCrlDistPoint
import org.apache.activemq.artemis.api.core.QueueConfiguration
import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.RoutingType
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.jce.provider.BouncyCastleProvider
@ -496,7 +497,9 @@ class ArtemisServerRevocationTest : AbstractServerRevocationTest() {
} }
val queueName = "${P2P_PREFIX}Test" val queueName = "${P2P_PREFIX}Test"
artemisNode.client.started!!.session.createQueue(queueName, RoutingType.ANYCAST, queueName, true) artemisNode.client.started!!.session.createQueue(
QueueConfiguration(queueName).setRoutingType(RoutingType.ANYCAST).setAddress(queueName).setDurable(true)
)
val clientConnectionChangeStatus = client.waitForInitialConnectionAndCaptureChanges(expectedConnectedStatus) val clientConnectionChangeStatus = client.waitForInitialConnectionAndCaptureChanges(expectedConnectedStatus)

View File

@ -36,6 +36,7 @@ import net.corda.testing.core.CHARLIE_NAME
import net.corda.testing.core.MAX_MESSAGE_SIZE import net.corda.testing.core.MAX_MESSAGE_SIZE
import net.corda.testing.driver.internal.incrementalPortAllocation import net.corda.testing.driver.internal.incrementalPortAllocation
import net.corda.testing.internal.createDevIntermediateCaCertPath import net.corda.testing.internal.createDevIntermediateCaCertPath
import org.apache.activemq.artemis.api.core.QueueConfiguration
import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.RoutingType
import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertArrayEquals
@ -272,7 +273,8 @@ class ProtonWrapperTests {
assertEquals(CHARLIE_NAME, CordaX500Name.build(clientConnected.get().remoteCert!!.subjectX500Principal)) assertEquals(CHARLIE_NAME, CordaX500Name.build(clientConnected.get().remoteCert!!.subjectX500Principal))
val artemis = artemisClient.started!! val artemis = artemisClient.started!!
val sendAddress = P2P_PREFIX + "Test" val sendAddress = P2P_PREFIX + "Test"
artemis.session.createQueue(sendAddress, RoutingType.ANYCAST, "queue", true) artemis.session.createQueue(QueueConfiguration("queue")
.setRoutingType(RoutingType.ANYCAST).setAddress(sendAddress).setDurable(true))
val consumer = artemis.session.createConsumer("queue") val consumer = artemis.session.createConsumer("queue")
val testData = "Test".toByteArray() val testData = "Test".toByteArray()
val testProperty = mutableMapOf<String, Any?>() val testProperty = mutableMapOf<String, Any?>()
@ -290,23 +292,26 @@ class ProtonWrapperTests {
@Test(timeout=300_000) @Test(timeout=300_000)
fun `Send a message larger then maxMessageSize from AMQP to Artemis inbox`() { fun `Send a message larger then maxMessageSize from AMQP to Artemis inbox`() {
val maxMessageSize = 100_000 val maxUserPayloadSize = 100_000
val (server, artemisClient) = createArtemisServerAndClient(maxMessageSize) val maxMessageSizeWithHeaders = maxUserPayloadSize + 512 // Adding a small "shim" to account for headers
val amqpClient = createClient(maxMessageSize) // and other non-payload bits of data
val (server, artemisClient) = createArtemisServerAndClient(maxMessageSizeWithHeaders)
val amqpClient = createClient(maxMessageSizeWithHeaders)
val clientConnected = amqpClient.onConnection.toFuture() val clientConnected = amqpClient.onConnection.toFuture()
amqpClient.start() amqpClient.start()
assertEquals(true, clientConnected.get().connected) assertEquals(true, clientConnected.get().connected)
assertEquals(CHARLIE_NAME, CordaX500Name.build(clientConnected.get().remoteCert!!.subjectX500Principal)) assertEquals(CHARLIE_NAME, CordaX500Name.build(clientConnected.get().remoteCert!!.subjectX500Principal))
val artemis = artemisClient.started!! val artemis = artemisClient.started!!
val sendAddress = P2P_PREFIX + "Test" val sendAddress = P2P_PREFIX + "Test"
artemis.session.createQueue(sendAddress, RoutingType.ANYCAST, "queue", true) artemis.session.createQueue(QueueConfiguration("queue")
.setRoutingType(RoutingType.ANYCAST).setAddress(sendAddress).setDurable(true))
val consumer = artemis.session.createConsumer("queue") val consumer = artemis.session.createConsumer("queue")
val testProperty = mutableMapOf<String, Any?>() val testProperty = mutableMapOf<String, Any?>()
testProperty["TestProp"] = "1" testProperty["TestProp"] = "1"
// Send normal message. // Send normal message.
val testData = ByteArray(maxMessageSize) val testData = ByteArray(maxUserPayloadSize)
val message = amqpClient.createMessage(testData, sendAddress, CHARLIE_NAME.toString(), testProperty) val message = amqpClient.createMessage(testData, sendAddress, CHARLIE_NAME.toString(), testProperty)
amqpClient.write(message) amqpClient.write(message)
assertEquals(MessageStatus.Acknowledged, message.onComplete.get()) assertEquals(MessageStatus.Acknowledged, message.onComplete.get())
@ -314,8 +319,8 @@ class ProtonWrapperTests {
assertEquals("1", received.getStringProperty("TestProp")) assertEquals("1", received.getStringProperty("TestProp"))
assertArrayEquals(testData, ByteArray(received.bodySize).apply { received.bodyBuffer.readBytes(this) }) assertArrayEquals(testData, ByteArray(received.bodySize).apply { received.bodyBuffer.readBytes(this) })
// Send message larger then max message size. // Send message larger than max message size.
val largeData = ByteArray(maxMessageSize + 1) val largeData = ByteArray(maxMessageSizeWithHeaders + 1)
// Create message will fail. // Create message will fail.
assertThatThrownBy { assertThatThrownBy {
amqpClient.createMessage(largeData, sendAddress, CHARLIE_NAME.toString(), testProperty) amqpClient.createMessage(largeData, sendAddress, CHARLIE_NAME.toString(), testProperty)
@ -393,7 +398,7 @@ class ProtonWrapperTests {
} }
@Test(timeout=300_000) @Test(timeout=300_000)
fun `Message sent from AMQP to non-existent Artemis inbox is rejected and client disconnects`() { fun `Message sent from AMQP to non-existent Artemis inbox is marked as acknowledged to avoid infinite retries`() {
val (server, artemisClient) = createArtemisServerAndClient() val (server, artemisClient) = createArtemisServerAndClient()
val amqpClient = createClient() val amqpClient = createClient()
// AmqpClient is set to auto-reconnect, there might be multiple connect/disconnect rounds // AmqpClient is set to auto-reconnect, there might be multiple connect/disconnect rounds
@ -413,8 +418,9 @@ class ProtonWrapperTests {
testProperty["TestProp"] = "1" testProperty["TestProp"] = "1"
val message = amqpClient.createMessage(testData, sendAddress, CHARLIE_NAME.toString(), testProperty) val message = amqpClient.createMessage(testData, sendAddress, CHARLIE_NAME.toString(), testProperty)
amqpClient.write(message) amqpClient.write(message)
assertEquals(MessageStatus.Rejected, message.onComplete.get()) assertEquals(MessageStatus.Acknowledged, message.onComplete.get())
assertTrue(connectedStack.contains(false)) assertTrue(connectedStack.contains(true))
assertEquals(1, connectedStack.size)
amqpClient.stop() amqpClient.stop()
artemisClient.stop() artemisClient.stop()
server.stop() server.stop()

View File

@ -31,6 +31,7 @@ import java.security.PublicKey
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNotEquals import kotlin.test.assertNotEquals
import kotlin.test.assertNull import kotlin.test.assertNull
import kotlin.test.assertNotNull
class CertificateRotationTest { class CertificateRotationTest {
private val ref = OpaqueBytes.of(0x01) private val ref = OpaqueBytes.of(0x01)
@ -180,7 +181,7 @@ class CertificateRotationTest {
advertiseNodesToNetwork(mockNet.defaultNotaryNode, bob2, charlie) advertiseNodesToNetwork(mockNet.defaultNotaryNode, bob2, charlie)
assertNull(bob2.services.identityService.wellKnownPartyFromX500Name(ALICE_NAME)) assertNotNull(bob2.services.identityService.wellKnownPartyFromX500Name(ALICE_NAME))
assertNull(charlie.services.identityService.wellKnownPartyFromX500Name(ALICE_NAME)) assertNull(charlie.services.identityService.wellKnownPartyFromX500Name(ALICE_NAME))
bob2.services.startFlow(CashPaymentFlow(1000.DOLLARS, charlie.party, false)) bob2.services.startFlow(CashPaymentFlow(1000.DOLLARS, charlie.party, false))

View File

@ -1,14 +1,33 @@
package net.corda.node.services.identity package net.corda.node.services.identity
import co.paralleluniverse.fibers.Suspendable
import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.ReceiveTransactionFlow
import net.corda.core.flows.SendTransactionFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party
import net.corda.core.internal.createDirectories import net.corda.core.internal.createDirectories
import net.corda.core.node.StatesToRecord
import net.corda.core.node.services.Vault
import net.corda.core.node.services.queryBy
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.node.services.vault.builder
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.finance.DOLLARS import net.corda.finance.DOLLARS
import net.corda.finance.USD import net.corda.finance.USD
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.flows.CashIssueAndPaymentFlow import net.corda.finance.flows.CashIssueAndPaymentFlow
import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashIssueFlow
import net.corda.finance.flows.CashPaymentFlow import net.corda.finance.flows.CashPaymentFlow
import net.corda.finance.schemas.CashSchemaV1
import net.corda.finance.workflows.getCashBalance import net.corda.finance.workflows.getCashBalance
import net.corda.node.services.config.NotaryConfig import net.corda.node.services.config.NotaryConfig
import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.DevIdentityGenerator
@ -25,12 +44,16 @@ import net.corda.testing.node.internal.FINANCE_CORDAPPS
import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.InternalMockNodeParameters import net.corda.testing.node.internal.InternalMockNodeParameters
import net.corda.testing.node.internal.TestStartedNode import net.corda.testing.node.internal.TestStartedNode
import net.corda.testing.node.internal.enclosedCordapp
import net.corda.testing.node.internal.startFlow import net.corda.testing.node.internal.startFlow
import org.junit.After import org.junit.After
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.Parameterized import org.junit.runners.Parameterized
import java.util.*
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
@RunWith(Parameterized::class) @RunWith(Parameterized::class)
class NotaryCertificateRotationTest(private val validating: Boolean) { class NotaryCertificateRotationTest(private val validating: Boolean) {
@ -91,8 +114,9 @@ class NotaryCertificateRotationTest(private val validating: Boolean) {
val bob2 = mockNet.restartNode(bob) val bob2 = mockNet.restartNode(bob)
val charlie = mockNet.createPartyNode(CHARLIE_NAME) val charlie = mockNet.createPartyNode(CHARLIE_NAME)
// Save previous network parameters for subsequent backchain verification. // Save previous network parameters for subsequent backchain verification, because not persistent in mock network
mockNet.nodes.forEach { it.services.networkParametersService.saveParameters(ca.sign(mockNet.networkParameters)) } alice2.internals.services.networkParametersService.saveParameters(ca.sign(mockNet.networkParameters))
bob2.internals.services.networkParametersService.saveParameters(ca.sign(mockNet.networkParameters))
// Verify that notary identity has been changed. // Verify that notary identity has been changed.
assertEquals(listOf(newNotaryIdentity), alice2.services.networkMapCache.notaryIdentities) assertEquals(listOf(newNotaryIdentity), alice2.services.networkMapCache.notaryIdentities)
@ -126,4 +150,116 @@ class NotaryCertificateRotationTest(private val validating: Boolean) {
assertEquals(0.DOLLARS, bob2.services.getCashBalance(USD)) assertEquals(0.DOLLARS, bob2.services.getCashBalance(USD))
assertEquals(7300.DOLLARS, charlie.services.getCashBalance(USD)) assertEquals(7300.DOLLARS, charlie.services.getCashBalance(USD))
} }
@Test(timeout = 300_000)
fun `rotate notary identity and new node receives netparams and understands old notary`() {
mockNet = InternalMockNetwork(
cordappsForAllNodes = FINANCE_CORDAPPS + enclosedCordapp(),
notarySpecs = listOf(MockNetworkNotarySpec(DUMMY_NOTARY_NAME, validating)),
initialNetworkParameters = testNetworkParameters()
)
val alice = mockNet.createPartyNode(ALICE_NAME)
val bob = mockNet.createPartyNode(BOB_NAME)
// Issue states and notarize them with initial notary identity.
alice.services.startFlow(CashIssueFlow(1000.DOLLARS, ref, mockNet.defaultNotaryIdentity))
alice.services.startFlow(CashIssueAndPaymentFlow(2000.DOLLARS, ref, alice.party, false, mockNet.defaultNotaryIdentity))
alice.services.startFlow(CashIssueAndPaymentFlow(4000.DOLLARS, ref, bob.party, false, mockNet.defaultNotaryIdentity))
mockNet.runNetwork()
val oldHash = alice.services.networkParametersService.currentHash
// Rotate notary identity and update network parameters.
val newNotaryIdentity = DevIdentityGenerator.installKeyStoreWithNodeIdentity(
mockNet.baseDirectory(mockNet.nextNodeId),
DUMMY_NOTARY_NAME
)
val newNetworkParameters = testNetworkParameters(epoch = 2)
.addNotary(mockNet.defaultNotaryIdentity, validating)
.addNotary(newNotaryIdentity, validating)
val ca = createDevNetworkMapCa()
NetworkParametersCopier(newNetworkParameters, ca, overwriteFile = true).apply {
install(mockNet.baseDirectory(alice))
install(mockNet.baseDirectory(bob))
install(mockNet.baseDirectory(mockNet.nextNodeId))
install(mockNet.baseDirectory(mockNet.nextNodeId + 1).apply { createDirectories() })
}
// Start notary with new identity and restart nodes.
mockNet.createNode(InternalMockNodeParameters(
legalName = DUMMY_NOTARY_NAME,
configOverrides = { doReturn(NotaryConfig(validating)).whenever(it).notary }
))
val alice2 = mockNet.restartNode(alice)
val bob2 = mockNet.restartNode(bob)
// We hide the old notary as trying to simulate it's replacement
mockNet.hideNode(mockNet.defaultNotaryNode)
val charlie = mockNet.createPartyNode(CHARLIE_NAME)
// Save previous network parameters for subsequent backchain verification, because not persistent in mock network
alice2.internals.services.networkParametersService.saveParameters(ca.sign(mockNet.networkParameters))
bob2.internals.services.networkParametersService.saveParameters(ca.sign(mockNet.networkParameters))
assertNotNull(alice2.services.networkParametersService.lookup(oldHash))
assertNotNull(bob2.services.networkParametersService.lookup(oldHash))
assertNull(charlie.services.networkParametersService.lookup(oldHash))
// Verify that notary identity has been changed.
assertEquals(listOf(newNotaryIdentity), alice2.services.networkMapCache.notaryIdentities)
assertEquals(listOf(newNotaryIdentity), bob2.services.networkMapCache.notaryIdentities)
assertEquals(listOf(newNotaryIdentity), charlie.services.networkMapCache.notaryIdentities)
assertEquals(newNotaryIdentity, alice2.services.identityService.wellKnownPartyFromX500Name(DUMMY_NOTARY_NAME))
assertEquals(newNotaryIdentity, bob2.services.identityService.wellKnownPartyFromX500Name(DUMMY_NOTARY_NAME))
assertEquals(newNotaryIdentity, charlie.services.identityService.wellKnownPartyFromX500Name(DUMMY_NOTARY_NAME))
assertEquals(newNotaryIdentity, alice2.services.identityService.wellKnownPartyFromAnonymous(mockNet.defaultNotaryIdentity))
assertEquals(newNotaryIdentity, bob2.services.identityService.wellKnownPartyFromAnonymous(mockNet.defaultNotaryIdentity))
assertEquals(newNotaryIdentity, charlie.services.identityService.wellKnownPartyFromAnonymous(mockNet.defaultNotaryIdentity))
// Now send an existing transaction on Bob (from before rotation) to Charlie
val bobVault: Vault.Page<Cash.State> = bob2.services.vaultService.queryBy(generateCashCriteria(USD))
assertEquals(1, bobVault.states.size)
val handle = bob2.services.startFlow(RpcSendTransactionFlow(bobVault.states[0].ref.txhash, charlie.party))
mockNet.runNetwork()
// Check flow completed successfully
assertEquals(handle.resultFuture.getOrThrow(), Unit)
// Check Charlie recorded it in the vault (could resolve notary, for example)
val charlieVault: Vault.Page<Cash.State> = charlie.services.vaultService.queryBy(generateCashCriteria(USD))
assertEquals(1, charlieVault.states.size)
// Check Charlie gained the network parameters from before the rotation
assertNotNull(charlie.services.networkParametersService.lookup(oldHash))
// We unhide the old notary so it can be shutdown
mockNet.unhideNode(mockNet.defaultNotaryNode)
}
private fun generateCashCriteria(currency: Currency): QueryCriteria {
val stateCriteria = QueryCriteria.FungibleAssetQueryCriteria()
val ccyIndex = builder { CashSchemaV1.PersistentCashState::currency.equal(currency.currencyCode) }
// This query should only return cash states the calling node is a participant of (meaning they can be modified/spent).
val ccyCriteria = QueryCriteria.VaultCustomQueryCriteria(ccyIndex, relevancyStatus = Vault.RelevancyStatus.ALL)
return stateCriteria.and(ccyCriteria)
}
@StartableByRPC
@InitiatingFlow
class RpcSendTransactionFlow(private val tx: SecureHash, private val party: Party) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val session = initiateFlow(party)
val stx: SignedTransaction = serviceHub.validatedTransactions.getTransaction(tx)!!
subFlow(SendTransactionFlow(session, stx))
}
}
@InitiatedBy(RpcSendTransactionFlow::class)
class RpcSendTransactionResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
subFlow(ReceiveTransactionFlow(otherSide, statesToRecord = StatesToRecord.ALL_VISIBLE))
}
}
} }

View File

@ -183,6 +183,7 @@ class ArtemisMessagingTest {
messagingClient.send(tooLagerMessage, messagingClient.myAddress) messagingClient.send(tooLagerMessage, messagingClient.myAddress)
}.isInstanceOf(ActiveMQConnectionTimedOutException::class.java) }.isInstanceOf(ActiveMQConnectionTimedOutException::class.java)
assertNull(receivedMessages.poll(200, MILLISECONDS)) assertNull(receivedMessages.poll(200, MILLISECONDS))
this.messagingClient = null
} }
@Test(timeout=300_000) @Test(timeout=300_000)
@ -232,7 +233,9 @@ class ArtemisMessagingTest {
MetricRegistry(), MetricRegistry(),
TestingNamedCacheFactory(), TestingNamedCacheFactory(),
isDrainingModeOn = { false }, isDrainingModeOn = { false },
drainingModeWasChangedEvents = PublishSubject.create<Pair<Boolean, Boolean>>()).apply { drainingModeWasChangedEvents = PublishSubject.create<Pair<Boolean, Boolean>>(),
terminateOnConnectionError = false,
timeoutConfig = P2PMessagingClient.TimeoutConfig(10.seconds, 10.seconds, 10.seconds)).apply {
config.configureWithDevSSLCertificate() config.configureWithDevSSLCertificate()
messagingClient = this messagingClient = this
} }

View File

@ -0,0 +1,67 @@
@file:Suppress("DEPRECATION")
package net.corda.node.services.messaging
import net.corda.client.rpc.ext.MultiRPCClient
import net.corda.core.internal.createDirectories
import net.corda.core.internal.div
import net.corda.core.internal.isRegularFile
import net.corda.core.internal.list
import net.corda.core.messaging.flows.FlowManagerRPCOps
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.seconds
import net.corda.node.internal.NodeStartup
import net.corda.node.services.Permissions
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver
import net.corda.testing.node.User
import org.junit.Test
import kotlin.test.assertNotNull
import net.corda.core.internal.messaging.FlowManagerRPCOps as InternalFlowManagerRPCOps
class FlowManagerRPCOpsTest {
@Test(timeout = 300_000)
fun `net_corda_core_internal_messaging_FlowManagerRPCOps can be accessed using the MultiRPCClient`() {
val user = User("user", "password", setOf(Permissions.all()))
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val nodeAHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
val client = MultiRPCClient(nodeAHandle.rpcAddress, InternalFlowManagerRPCOps::class.java, user.username, user.password)
val logDirPath = nodeAHandle.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME
logDirPath.createDirectories()
client.use {
val rpcOps = it.start().getOrThrow(20.seconds).proxy
rpcOps.dumpCheckpoints()
it.stop()
}
assertNotNull(logDirPath.list().singleOrNull { it.isRegularFile() })
}
}
@Test(timeout = 300_000)
fun `net_corda_core_messaging_flows_FlowManagerRPCOps can be accessed using the MultiRPCClient`() {
val user = User("user", "password", setOf(Permissions.all()))
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val nodeAHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
val client = MultiRPCClient(nodeAHandle.rpcAddress, FlowManagerRPCOps::class.java, user.username, user.password)
val logDirPath = nodeAHandle.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME
logDirPath.createDirectories()
client.use {
val rpcOps = it.start().getOrThrow(20.seconds).proxy
rpcOps.dumpCheckpoints()
it.stop()
}
assertNotNull(logDirPath.list().singleOrNull { it.isRegularFile() })
}
}
}

View File

@ -4,6 +4,8 @@ import co.paralleluniverse.fibers.Suspendable
import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClient
import net.corda.core.contracts.PartyAndReference import net.corda.core.contracts.PartyAndReference
import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateAndRef
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.CollectSignaturesFlow
import net.corda.core.flows.FinalityFlow import net.corda.core.flows.FinalityFlow
import net.corda.core.flows.FlowException import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
@ -12,20 +14,29 @@ import net.corda.core.flows.HospitalizeFlowException
import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.NotaryException import net.corda.core.flows.NotaryException
import net.corda.core.flows.NotaryFlow
import net.corda.core.flows.ReceiveFinalityFlow import net.corda.core.flows.ReceiveFinalityFlow
import net.corda.core.flows.SignTransactionFlow
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.flows.UnexpectedFlowEndException
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.concurrent.transpose import net.corda.core.internal.concurrent.transpose
import net.corda.core.messaging.StateMachineUpdate import net.corda.core.messaging.StateMachineUpdate
import net.corda.core.messaging.startFlow import net.corda.core.messaging.startFlow
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import net.corda.core.utilities.unwrap
import net.corda.node.services.Permissions import net.corda.node.services.Permissions
import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContract
import net.corda.testing.contracts.DummyContract.SingleOwnerState import net.corda.testing.contracts.DummyContract.SingleOwnerState
import net.corda.testing.contracts.DummyState
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.CHARLIE_NAME import net.corda.testing.core.CHARLIE_NAME
import net.corda.testing.core.singleIdentity
import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver import net.corda.testing.driver.driver
import net.corda.testing.node.User import net.corda.testing.node.User
@ -33,6 +44,7 @@ import net.corda.testing.node.internal.enclosedCordapp
import net.corda.testing.node.internal.findCordapp import net.corda.testing.node.internal.findCordapp
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Before
import org.junit.Test import org.junit.Test
import java.sql.SQLException import java.sql.SQLException
import java.util.* import java.util.*
@ -47,6 +59,12 @@ class FlowHospitalTest {
private val rpcUser = User("user1", "test", permissions = setOf(Permissions.all())) private val rpcUser = User("user1", "test", permissions = setOf(Permissions.all()))
@Before
fun before() {
SpendStateAndCatchDoubleSpendResponderFlow.exceptionSeenInUserFlow = false
CreateTransactionButDontFinalizeResponderFlow.exceptionSeenInUserFlow = false
}
@Test(timeout = 300_000) @Test(timeout = 300_000)
fun `when double spend occurs, the flow is successfully deleted on the counterparty`() { fun `when double spend occurs, the flow is successfully deleted on the counterparty`() {
driver(DriverParameters(cordappsForAllNodes = listOf(enclosedCordapp(), findCordapp("net.corda.testing.contracts")))) { driver(DriverParameters(cordappsForAllNodes = listOf(enclosedCordapp(), findCordapp("net.corda.testing.contracts")))) {
@ -172,7 +190,7 @@ class FlowHospitalTest {
@Test(timeout = 300_000) @Test(timeout = 300_000)
fun `HospitalizeFlowException cloaking an important exception thrown`() { fun `HospitalizeFlowException cloaking an important exception thrown`() {
var dischargedCounter = 0 var dischargedCounter = 0
var observationCounter: Int = 0 var observationCounter = 0
StaffedFlowHospital.onFlowDischarged.add { _, _ -> StaffedFlowHospital.onFlowDischarged.add { _, _ ->
++dischargedCounter ++dischargedCounter
} }
@ -197,6 +215,84 @@ class FlowHospitalTest {
} }
} }
@Test(timeout = 300_000)
fun `catching a notary error will cause a peer to fail with unexpected session end during ReceiveFinalityFlow that passes through user code`() {
var dischargedCounter = 0
StaffedFlowHospital.onFlowErrorPropagated.add { _, _ ->
++dischargedCounter
}
val user = User("mark", "dadada", setOf(Permissions.all()))
driver(DriverParameters(isDebug = false, startNodesInProcess = true)) {
val nodeAHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
val nodeBHandle = startNode(providedName = BOB_NAME, rpcUsers = listOf(user)).getOrThrow()
nodeAHandle.rpc.let {
val ref = it.startFlow(::CreateTransactionFlow, nodeBHandle.nodeInfo.singleIdentity()).returnValue.getOrThrow(20.seconds)
it.startFlow(::SpendStateAndCatchDoubleSpendFlow, nodeBHandle.nodeInfo.singleIdentity(), ref).returnValue.getOrThrow(20.seconds)
it.startFlow(::SpendStateAndCatchDoubleSpendFlow, nodeBHandle.nodeInfo.singleIdentity(), ref).returnValue.getOrThrow(20.seconds)
}
}
// 1 is the notary failing to notarise and propagating the error
// 2 is the receiving flow failing due to the unexpected session end error
assertEquals(2, dischargedCounter)
assertTrue(SpendStateAndCatchDoubleSpendResponderFlow.exceptionSeenInUserFlow)
}
@Test(timeout = 300_000)
fun `unexpected session end errors outside of ReceiveFinalityFlow are not handled`() {
var dischargedCounter = 0
var observationCounter = 0
StaffedFlowHospital.onFlowErrorPropagated.add { _, _ ->
++dischargedCounter
}
StaffedFlowHospital.onFlowKeptForOvernightObservation.add { _, _ ->
++observationCounter
}
val user = User("mark", "dadada", setOf(Permissions.all()))
driver(DriverParameters(isDebug = false, startNodesInProcess = true)) {
val nodeAHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
val nodeBHandle = startNode(providedName = BOB_NAME, rpcUsers = listOf(user)).getOrThrow()
val nodeCHandle = startNode(providedName = CHARLIE_NAME, rpcUsers = listOf(user)).getOrThrow()
nodeAHandle.rpc.let {
val ref = it.startFlow(::CreateTransactionFlow, nodeBHandle.nodeInfo.singleIdentity()).returnValue.getOrThrow(20.seconds)
val ref2 = it.startFlow(::SpendStateAndCatchDoubleSpendFlow, nodeBHandle.nodeInfo.singleIdentity(), ref).returnValue.getOrThrow(20.seconds)
val ref3 = it.startFlow(::SpendStateAndCatchDoubleSpendFlow, nodeCHandle.nodeInfo.singleIdentity(), ref2).returnValue.getOrThrow(20.seconds)
it.startFlow(::CreateTransactionButDontFinalizeFlow, nodeBHandle.nodeInfo.singleIdentity(), ref3).returnValue.getOrThrow(20.seconds)
}
}
assertEquals(0, dischargedCounter)
assertEquals(1, observationCounter)
assertTrue(CreateTransactionButDontFinalizeResponderFlow.exceptionSeenInUserFlow)
}
@Test(timeout = 300_000)
fun `unexpected session end errors within ReceiveFinalityFlow can be caught and the flow can end gracefully`() {
var dischargedCounter = 0
var observationCounter = 0
StaffedFlowHospital.onFlowErrorPropagated.add { _, _ ->
++dischargedCounter
}
StaffedFlowHospital.onFlowKeptForOvernightObservation.add { _, _ ->
++observationCounter
}
val user = User("mark", "dadada", setOf(Permissions.all()))
driver(DriverParameters(isDebug = false, startNodesInProcess = true)) {
val nodeAHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
val nodeBHandle = startNode(providedName = BOB_NAME, rpcUsers = listOf(user)).getOrThrow()
nodeAHandle.rpc.let {
val ref = it.startFlow(::CreateTransactionFlow, nodeBHandle.nodeInfo.singleIdentity()).returnValue.getOrThrow(20.seconds)
it.startFlow(::SpendStateAndCatchDoubleSpendFlow, nodeBHandle.nodeInfo.singleIdentity(), ref).returnValue.getOrThrow(20.seconds)
it.startFlow(::SpendStateAndCatchDoubleSpendFlow, nodeBHandle.nodeInfo.singleIdentity(), ref, true).returnValue.getOrThrow(20.seconds)
}
}
// 1 is the notary failing to notarise and propagating the error
assertEquals(1, dischargedCounter)
assertEquals(0, observationCounter)
assertTrue(SpendStateAndCatchDoubleSpendResponderFlow.exceptionSeenInUserFlow)
}
@StartableByRPC @StartableByRPC
class IssueFlow(val notary: Party) : FlowLogic<StateAndRef<SingleOwnerState>>() { class IssueFlow(val notary: Party) : FlowLogic<StateAndRef<SingleOwnerState>>() {
@ -296,4 +392,136 @@ class FlowHospitalTest {
setCause(SQLException("deadlock")) setCause(SQLException("deadlock"))
} }
} }
@InitiatingFlow
@StartableByRPC
class CreateTransactionFlow(private val peer: Party) : FlowLogic<StateAndRef<DummyState>>() {
@Suspendable
override fun call(): StateAndRef<DummyState> {
val tx = TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first()).apply {
addOutputState(DummyState(participants = listOf(ourIdentity)))
addCommand(DummyContract.Commands.Create(), listOf(ourIdentity.owningKey, peer.owningKey))
}
val stx = serviceHub.signInitialTransaction(tx)
val session = initiateFlow(peer)
val ftx = subFlow(CollectSignaturesFlow(stx, listOf(session)))
subFlow(FinalityFlow(ftx, session))
return ftx.coreTransaction.outRef(0)
}
}
@InitiatedBy(CreateTransactionFlow::class)
class CreateTransactionResponderFlow(private val session: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
logger.info("CREATE TX - WAITING TO SIGN TX")
val stx = subFlow(object : SignTransactionFlow(session) {
override fun checkTransaction(stx: SignedTransaction) {
}
})
logger.info("CREATE TX - SIGNED TO SIGN TX")
subFlow(ReceiveFinalityFlow(session, stx.id))
logger.info("CREATE TX - RECEIVED TX")
}
}
@InitiatingFlow
@StartableByRPC
class SpendStateAndCatchDoubleSpendFlow(
private val peer: Party,
private val ref: StateAndRef<DummyState>,
private val consumePeerError: Boolean
) : FlowLogic<StateAndRef<DummyState>>() {
constructor(peer: Party, ref: StateAndRef<DummyState>): this(peer, ref, false)
@Suspendable
override fun call(): StateAndRef<DummyState> {
val tx = TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first()).apply {
addInputState(ref)
addOutputState(DummyState(participants = listOf(ourIdentity)))
addCommand(DummyContract.Commands.Move(), listOf(ourIdentity.owningKey, peer.owningKey))
}
val stx = serviceHub.signInitialTransaction(tx)
val session = initiateFlow(peer)
session.send(consumePeerError)
val ftx = subFlow(CollectSignaturesFlow(stx, listOf(session)))
try {
subFlow(FinalityFlow(ftx, session))
} catch(e: NotaryException) {
logger.info("Caught notary exception")
}
return ftx.coreTransaction.outRef(0)
}
}
@InitiatedBy(SpendStateAndCatchDoubleSpendFlow::class)
class SpendStateAndCatchDoubleSpendResponderFlow(private val session: FlowSession) : FlowLogic<Unit>() {
companion object {
var exceptionSeenInUserFlow = false
}
@Suspendable
override fun call() {
val consumeError = session.receive<Boolean>().unwrap { it }
val stx = subFlow(object : SignTransactionFlow(session) {
override fun checkTransaction(stx: SignedTransaction) {
}
})
try {
subFlow(ReceiveFinalityFlow(session, stx.id))
} catch (e: UnexpectedFlowEndException) {
exceptionSeenInUserFlow = true
if (!consumeError) {
throw e
}
}
}
}
@InitiatingFlow
@StartableByRPC
class CreateTransactionButDontFinalizeFlow(private val peer: Party, private val ref: StateAndRef<DummyState>) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val tx = TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first()).apply {
addInputState(ref)
addOutputState(DummyState(participants = listOf(ourIdentity)))
addCommand(DummyContract.Commands.Move(), listOf(ourIdentity.owningKey))
}
val stx = serviceHub.signInitialTransaction(tx)
val session = initiateFlow(peer)
// Send the transaction id to the peer instead of the transaction.
// This allows transaction dependency resolution to occur within the peer's [ReceiveTransactionFlow].
session.send(stx.id)
// Mimic notarisation from [FinalityFlow] so that failing inside [ResolveTransactionsFlow] can be achieved.
val notarySignatures = subFlow(NotaryFlow.Client(stx, skipVerification = true))
val notarisedTx = stx + notarySignatures
session.send(notarisedTx)
}
}
@InitiatedBy(CreateTransactionButDontFinalizeFlow::class)
class CreateTransactionButDontFinalizeResponderFlow(private val session: FlowSession) : FlowLogic<Unit>() {
companion object {
var exceptionSeenInUserFlow = false
}
@Suspendable
override fun call() {
val id = session.receive<SecureHash>().unwrap { it }
try {
subFlow(ReceiveFinalityFlow(session, id))
} catch (e: UnexpectedFlowEndException) {
exceptionSeenInUserFlow = true
throw e
}
}
}
} }

View File

@ -28,6 +28,7 @@ import net.corda.testing.node.internal.NodeBasedTest
import net.corda.testing.node.internal.startFlow import net.corda.testing.node.internal.startFlow
import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
import org.apache.activemq.artemis.api.core.QueueConfiguration
import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.RoutingType
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.assertj.core.api.Assertions.assertThatExceptionOfType
@ -130,7 +131,11 @@ abstract class MQSecurityTest : NodeBasedTest() {
fun assertTempQueueCreationAttackFails(queue: String) { fun assertTempQueueCreationAttackFails(queue: String) {
assertAttackFails(queue, "CREATE_NON_DURABLE_QUEUE") { assertAttackFails(queue, "CREATE_NON_DURABLE_QUEUE") {
attacker.session.createTemporaryQueue(queue, RoutingType.MULTICAST, queue) attacker.session.createQueue(QueueConfiguration(queue)
.setRoutingType(RoutingType.MULTICAST)
.setAddress(queue)
.setTemporary(true)
.setDurable(false))
} }
// Double-check // Double-check
assertThatExceptionOfType(ActiveMQNonExistentQueueException::class.java).isThrownBy { assertThatExceptionOfType(ActiveMQNonExistentQueueException::class.java).isThrownBy {
@ -147,7 +152,8 @@ abstract class MQSecurityTest : NodeBasedTest() {
fun assertNonTempQueueCreationAttackFails(queue: String, durable: Boolean) { fun assertNonTempQueueCreationAttackFails(queue: String, durable: Boolean) {
val permission = if (durable) "CREATE_DURABLE_QUEUE" else "CREATE_NON_DURABLE_QUEUE" val permission = if (durable) "CREATE_DURABLE_QUEUE" else "CREATE_NON_DURABLE_QUEUE"
assertAttackFails(queue, permission) { assertAttackFails(queue, permission) {
attacker.session.createQueue(queue, RoutingType.MULTICAST, queue, durable) attacker.session.createQueue(
QueueConfiguration(queue).setAddress(queue).setRoutingType(RoutingType.MULTICAST).setDurable(durable))
} }
// Double-check // Double-check
assertThatExceptionOfType(ActiveMQNonExistentQueueException::class.java).isThrownBy { assertThatExceptionOfType(ActiveMQNonExistentQueueException::class.java).isThrownBy {

View File

@ -39,13 +39,13 @@ import net.corda.core.internal.concurrent.map
import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.div import net.corda.core.internal.div
import net.corda.core.internal.messaging.AttachmentTrustInfoRPCOps import net.corda.core.internal.messaging.AttachmentTrustInfoRPCOps
import net.corda.core.internal.messaging.FlowManagerRPCOps
import net.corda.core.internal.notary.NotaryService import net.corda.core.internal.notary.NotaryService
import net.corda.core.internal.rootMessage import net.corda.core.internal.rootMessage
import net.corda.core.internal.uncheckedCast import net.corda.core.internal.uncheckedCast
import net.corda.core.messaging.ClientRpcSslOptions import net.corda.core.messaging.ClientRpcSslOptions
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
import net.corda.core.messaging.flows.FlowManagerRPCOps
import net.corda.core.node.AppServiceHub import net.corda.core.node.AppServiceHub
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
@ -84,6 +84,7 @@ import net.corda.node.internal.cordapp.JarScanningCordappLoader
import net.corda.node.internal.cordapp.VirtualCordapp import net.corda.node.internal.cordapp.VirtualCordapp
import net.corda.node.internal.rpc.proxies.AuthenticatedRpcOpsProxy import net.corda.node.internal.rpc.proxies.AuthenticatedRpcOpsProxy
import net.corda.node.internal.rpc.proxies.ThreadContextAdjustingRpcOpsProxy import net.corda.node.internal.rpc.proxies.ThreadContextAdjustingRpcOpsProxy
import net.corda.node.internal.shell.InteractiveShell
import net.corda.node.services.ContractUpgradeHandler import net.corda.node.services.ContractUpgradeHandler
import net.corda.node.services.FinalityHandler import net.corda.node.services.FinalityHandler
import net.corda.node.services.NotaryChangeHandler import net.corda.node.services.NotaryChangeHandler
@ -100,8 +101,6 @@ import net.corda.node.services.api.WritableTransactionStorage
import net.corda.node.services.attachments.NodeAttachmentTrustCalculator import net.corda.node.services.attachments.NodeAttachmentTrustCalculator
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.rpc.NodeRpcOptions import net.corda.node.services.config.rpc.NodeRpcOptions
import net.corda.node.services.config.shell.determineUnsafeUsers
import net.corda.node.services.config.shell.toShellConfig
import net.corda.node.services.config.shouldInitCrashShell import net.corda.node.services.config.shouldInitCrashShell
import net.corda.node.services.diagnostics.NodeDiagnosticsService import net.corda.node.services.diagnostics.NodeDiagnosticsService
import net.corda.node.services.events.NodeSchedulerService import net.corda.node.services.events.NodeSchedulerService
@ -168,7 +167,6 @@ import net.corda.nodeapi.internal.persistence.SchemaMigration
import net.corda.nodeapi.internal.persistence.contextDatabase import net.corda.nodeapi.internal.persistence.contextDatabase
import net.corda.nodeapi.internal.persistence.withoutDatabaseAccess import net.corda.nodeapi.internal.persistence.withoutDatabaseAccess
import net.corda.nodeapi.internal.namedThreadPoolExecutor import net.corda.nodeapi.internal.namedThreadPoolExecutor
import net.corda.tools.shell.InteractiveShell
import org.apache.activemq.artemis.utils.ReusableLatch import org.apache.activemq.artemis.utils.ReusableLatch
import org.jolokia.jvmagent.JolokiaServer import org.jolokia.jvmagent.JolokiaServer
import org.jolokia.jvmagent.JolokiaServerConfig import org.jolokia.jvmagent.JolokiaServerConfig
@ -180,6 +178,7 @@ import java.sql.Savepoint
import java.time.Clock import java.time.Clock
import java.time.Duration import java.time.Duration
import java.time.format.DateTimeParseException import java.time.format.DateTimeParseException
import java.util.ArrayList
import java.util.Properties import java.util.Properties
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
@ -401,23 +400,21 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
} }
/** The implementation of the [RPCOps] interfaces used by this node. */ /** The implementation of the [RPCOps] interfaces used by this node. */
@Suppress("DEPRECATION")
open fun makeRPCOps(cordappLoader: CordappLoader): List<RPCOps> { open fun makeRPCOps(cordappLoader: CordappLoader): List<RPCOps> {
val cordaRPCOpsImpl = Pair(CordaRPCOps::class.java, CordaRPCOpsImpl( val cordaRPCOps = CordaRPCOpsImpl(services, smm, flowStarter) { shutdownExecutor.submit(::stop) }
services, cordaRPCOps.closeOnStop()
smm, val flowManagerRPCOps = FlowManagerRPCOpsImpl(checkpointDumper)
flowStarter val attachmentTrustInfoRPCOps = AttachmentTrustInfoRPCOpsImpl(services.attachmentTrustCalculator)
) {
shutdownExecutor.submit(::stop)
}.also { it.closeOnStop() })
val checkpointRPCOpsImpl = Pair(FlowManagerRPCOps::class.java, FlowManagerRPCOpsImpl(checkpointDumper)) return listOf(
CordaRPCOps::class.java to cordaRPCOps,
val attachmentTrustInfoRPCOps = Pair(AttachmentTrustInfoRPCOps::class.java, AttachmentTrustInfoRPCOpsImpl(services.attachmentTrustCalculator)) FlowManagerRPCOps::class.java to flowManagerRPCOps,
net.corda.core.internal.messaging.FlowManagerRPCOps::class.java to flowManagerRPCOps,
return listOf(cordaRPCOpsImpl, checkpointRPCOpsImpl, attachmentTrustInfoRPCOps).map { rpcOpsImplPair -> AttachmentTrustInfoRPCOps::class.java to attachmentTrustInfoRPCOps
).map { (targetInterface, implementation) ->
// Mind that order of proxies is important // Mind that order of proxies is important
val targetInterface = rpcOpsImplPair.first val stage1Proxy = AuthenticatedRpcOpsProxy.proxy(implementation, targetInterface)
val stage1Proxy = AuthenticatedRpcOpsProxy.proxy(rpcOpsImplPair.second, targetInterface)
val stage2Proxy = ThreadContextAdjustingRpcOpsProxy.proxy(stage1Proxy, targetInterface, cordappLoader.appClassLoader) val stage2Proxy = ThreadContextAdjustingRpcOpsProxy.proxy(stage1Proxy, targetInterface, cordappLoader.appClassLoader)
stage2Proxy stage2Proxy
@ -448,7 +445,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
} }
} }
fun clearNetworkMapCache() { open fun clearNetworkMapCache() {
Node.printBasicNodeInfo("Clearing network map cache entries") Node.printBasicNodeInfo("Clearing network map cache entries")
log.info("Starting clearing of network map cache entries...") log.info("Starting clearing of network map cache entries...")
startDatabase() startDatabase()
@ -680,16 +677,18 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
open fun startShell() { open fun startShell() {
if (configuration.shouldInitCrashShell()) { if (configuration.shouldInitCrashShell()) {
val shellConfiguration = configuration.toShellConfig() val isShellStarted = InteractiveShell.startShellIfInstalled(configuration, cordappLoader)
shellConfiguration.sshdPort?.let { configuration.sshd?.port?.let {
log.info("Binding Shell SSHD server on port $it.") if (isShellStarted) {
Node.printBasicNodeInfo("SSH server listening on port", configuration.sshd!!.port.toString())
log.info("SSH server listening on port: $it.")
} else {
Node.printBasicNodeInfo(
"SSH server not started. SSH port is defined but the corda-shell is not installed in node's drivers directory"
)
log.info("SSH server not started. SSH port is defined but the corda-shell is not installed in node's drivers directory")
}
} }
val unsafeUsers = determineUnsafeUsers(configuration)
org.crsh.ssh.term.CRaSHCommand.setUserInfo(unsafeUsers, true, false)
log.info("Setting unsafe users as: ${unsafeUsers}")
InteractiveShell.startShell(shellConfiguration, cordappLoader.appClassLoader)
} }
} }

View File

@ -566,6 +566,11 @@ open class Node(configuration: NodeConfiguration,
return super.generateAndSaveNodeInfo() return super.generateAndSaveNodeInfo()
} }
override fun clearNetworkMapCache() {
initialiseSerialization()
super.clearNetworkMapCache()
}
override fun runDatabaseMigrationScripts( override fun runDatabaseMigrationScripts(
updateCoreSchemas: Boolean, updateCoreSchemas: Boolean,
updateAppSchemas: Boolean, updateAppSchemas: Boolean,

View File

@ -8,38 +8,56 @@ import net.corda.cliutils.printError
import net.corda.common.logging.CordaVersion import net.corda.common.logging.CordaVersion
import net.corda.common.logging.errorReporting.CordaErrorContextProvider import net.corda.common.logging.errorReporting.CordaErrorContextProvider
import net.corda.common.logging.errorReporting.ErrorCode import net.corda.common.logging.errorReporting.ErrorCode
import net.corda.common.logging.errorReporting.ErrorReporting
import net.corda.common.logging.errorReporting.report
import net.corda.core.contracts.HashAttachmentConstraint import net.corda.core.contracts.HashAttachmentConstraint
import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto
import net.corda.core.internal.* import net.corda.core.internal.Emoji
import net.corda.core.internal.HashAgility
import net.corda.core.internal.PLATFORM_VERSION
import net.corda.core.internal.concurrent.thenMatch import net.corda.core.internal.concurrent.thenMatch
import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.internal.createDirectories
import net.corda.core.internal.div
import net.corda.core.internal.errors.AddressBindingException import net.corda.core.internal.errors.AddressBindingException
import net.corda.core.internal.exists
import net.corda.core.internal.isDirectory
import net.corda.core.internal.location
import net.corda.core.internal.randomOrNull
import net.corda.core.internal.safeSymbolicRead
import net.corda.core.utilities.Try import net.corda.core.utilities.Try
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.node.* import net.corda.node.NodeCmdLineOptions
import net.corda.common.logging.errorReporting.ErrorReporting import net.corda.node.SerialFilter
import net.corda.common.logging.errorReporting.report import net.corda.node.SharedNodeCmdLineOptions
import net.corda.node.VersionInfo
import net.corda.node.defaultSerialFilter
import net.corda.node.internal.Node.Companion.isInvalidJavaVersion import net.corda.node.internal.Node.Companion.isInvalidJavaVersion
import net.corda.node.internal.cordapp.MultipleCordappsForFlowException import net.corda.node.internal.cordapp.MultipleCordappsForFlowException
import net.corda.node.internal.subcommands.* import net.corda.node.internal.shell.InteractiveShell
import net.corda.node.internal.subcommands.ClearNetworkCacheCli
import net.corda.node.internal.subcommands.GenerateNodeInfoCli
import net.corda.node.internal.subcommands.GenerateRpcSslCertsCli
import net.corda.node.internal.subcommands.InitialRegistration
import net.corda.node.internal.subcommands.InitialRegistrationCli
import net.corda.node.internal.subcommands.RunMigrationScriptsCli
import net.corda.node.internal.subcommands.SynchroniseSchemasCli
import net.corda.node.internal.subcommands.ValidateConfigurationCli
import net.corda.node.internal.subcommands.ValidateConfigurationCli.Companion.logConfigurationErrors import net.corda.node.internal.subcommands.ValidateConfigurationCli.Companion.logConfigurationErrors
import net.corda.node.internal.subcommands.ValidateConfigurationCli.Companion.logRawConfig import net.corda.node.internal.subcommands.ValidateConfigurationCli.Companion.logRawConfig
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.shouldStartLocalShell import net.corda.node.services.config.shouldStartLocalShell
import net.corda.node.services.config.shouldStartSSHDaemon
import net.corda.node.utilities.registration.NodeRegistrationException import net.corda.node.utilities.registration.NodeRegistrationException
import net.corda.nodeapi.internal.JVMAgentUtilities import net.corda.nodeapi.internal.JVMAgentUtilities
import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.addShutdownHook
import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException
import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException
import net.corda.tools.shell.InteractiveShell
import org.fusesource.jansi.Ansi import org.fusesource.jansi.Ansi
import org.slf4j.bridge.SLF4JBridgeHandler import org.slf4j.bridge.SLF4JBridgeHandler
import picocli.CommandLine.Mixin import picocli.CommandLine.Mixin
import java.io.IOException import java.io.IOException
import java.io.RandomAccessFile import java.io.RandomAccessFile
import java.lang.NullPointerException
import java.lang.management.ManagementFactory import java.lang.management.ManagementFactory
import java.net.InetAddress import java.net.InetAddress
import java.nio.channels.UnresolvedAddressException import java.nio.channels.UnresolvedAddressException
@ -236,7 +254,8 @@ open class NodeStartup : NodeStartupLogging {
val loadedCodapps = node.services.cordappProvider.cordapps.filter { it.isLoaded } val loadedCodapps = node.services.cordappProvider.cordapps.filter { it.isLoaded }
logLoadedCorDapps(loadedCodapps) logLoadedCorDapps(loadedCodapps)
node.nodeReadyFuture.thenMatch({ node.nodeReadyFuture.thenMatch(
{
// Elapsed time in seconds. We used 10 / 100.0 and not directly / 1000.0 to only keep two decimal digits. // Elapsed time in seconds. We used 10 / 100.0 and not directly / 1000.0 to only keep two decimal digits.
val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0 val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0
val name = nodeInfo.legalIdentitiesAndCerts.first().name.organisation val name = nodeInfo.legalIdentitiesAndCerts.first().name.organisation
@ -244,16 +263,7 @@ open class NodeStartup : NodeStartupLogging {
// Don't start the shell if there's no console attached. // Don't start the shell if there's no console attached.
if (node.configuration.shouldStartLocalShell()) { if (node.configuration.shouldStartLocalShell()) {
node.startupComplete.then { InteractiveShell.runLocalShellIfInstalled(node::stop)
try {
InteractiveShell.runLocalShell(node::stop)
} catch (e: Exception) {
logger.error("Shell failed to start", e)
}
}
}
if (node.configuration.shouldStartSSHDaemon()) {
Node.printBasicNodeInfo("SSH server listening on port", node.configuration.sshd!!.port.toString())
} }
}, },
{ th -> { th ->

View File

@ -1,6 +1,5 @@
package net.corda.node.internal.artemis package net.corda.node.internal.artemis
import io.netty.channel.unix.Errors
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.internal.LifecycleSupport import net.corda.node.internal.LifecycleSupport
import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
@ -18,4 +17,11 @@ data class BrokerAddresses(val primary: NetworkHostAndPort, private val adminArg
val admin = adminArg ?: primary val admin = adminArg ?: primary
} }
fun java.io.IOException.isBindingError() = this is BindException || this is Errors.NativeIoException && message?.contains("Address already in use") == true fun Throwable.isBindingError(): Boolean {
val addressAlreadyUsedMsg = "Address already in use"
// This is not an exact science here.
// Depending on the underlying OS it can be either [Errors.NativeIoException] on Linux or [BindException] on Windows
// and of course this is dependent on the version of Artemis library used.
return this is BindException ||
this is IllegalStateException && cause.let { it is BindException || it?.message?.contains(addressAlreadyUsedMsg) == true }
}

View File

@ -14,6 +14,7 @@ import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal
import java.io.IOException import java.io.IOException
import java.security.KeyStore import java.security.KeyStore
import java.security.Principal import java.security.Principal
import java.security.cert.X509Certificate
import java.util.* import java.util.*
import javax.security.auth.Subject import javax.security.auth.Subject
import javax.security.auth.callback.CallbackHandler import javax.security.auth.callback.CallbackHandler
@ -119,19 +120,18 @@ class BrokerJaasLoginModule : BaseBrokerJaasLoginModule() {
// The Main authentication logic, responsible for running all the configured checks for each user type // The Main authentication logic, responsible for running all the configured checks for each user type
// and return the actual User and principals // and return the actual User and principals
@Suppress("DEPRECATION") // should use java.security.cert.X509Certificate private fun authenticateAndAuthorise(username: String, certificates: Array<X509Certificate>, password: String): Pair<String, List<RolePrincipal>> {
private fun authenticateAndAuthorise(username: String, certificates: Array<javax.security.cert.X509Certificate>?, password: String): Pair<String, List<RolePrincipal>> { fun requireTls(certificates: Array<X509Certificate>?) = requireNotNull(certificates) { "No client certificates presented." }
fun requireTls(certificates: Array<javax.security.cert.X509Certificate>?) = requireNotNull(certificates) { "No client certificates presented." }
return when (username) { return when (username) {
ArtemisMessagingComponent.NODE_P2P_USER -> { ArtemisMessagingComponent.NODE_P2P_USER -> {
requireTls(certificates) requireTls(certificates)
CertificateChainCheckPolicy.LeafMustMatch.createCheck(nodeJaasConfig.keyStore, nodeJaasConfig.trustStore).checkCertificateChain(certificates!!) CertificateChainCheckPolicy.LeafMustMatch.createCheck(nodeJaasConfig.keyStore, nodeJaasConfig.trustStore).checkCertificateChain(certificates)
Pair(certificates.first().subjectDN.name, listOf(RolePrincipal(NODE_P2P_ROLE))) Pair(certificates.first().subjectDN.name, listOf(RolePrincipal(NODE_P2P_ROLE)))
} }
ArtemisMessagingComponent.NODE_RPC_USER -> { ArtemisMessagingComponent.NODE_RPC_USER -> {
requireTls(certificates) requireTls(certificates)
CertificateChainCheckPolicy.LeafMustMatch.createCheck(nodeJaasConfig.keyStore, nodeJaasConfig.trustStore).checkCertificateChain(certificates!!) CertificateChainCheckPolicy.LeafMustMatch.createCheck(nodeJaasConfig.keyStore, nodeJaasConfig.trustStore).checkCertificateChain(certificates)
Pair(ArtemisMessagingComponent.NODE_RPC_USER, listOf(RolePrincipal(NODE_RPC_ROLE))) Pair(ArtemisMessagingComponent.NODE_RPC_USER, listOf(RolePrincipal(NODE_RPC_ROLE)))
} }
ArtemisMessagingComponent.PEER_USER -> { ArtemisMessagingComponent.PEER_USER -> {
@ -140,7 +140,7 @@ class BrokerJaasLoginModule : BaseBrokerJaasLoginModule() {
// This check is redundant as it was performed already during the SSL handshake // This check is redundant as it was performed already during the SSL handshake
CertificateChainCheckPolicy.RootMustMatch CertificateChainCheckPolicy.RootMustMatch
.createCheck(p2pJaasConfig.keyStore, p2pJaasConfig.trustStore) .createCheck(p2pJaasConfig.keyStore, p2pJaasConfig.trustStore)
.checkCertificateChain(certificates!!) .checkCertificateChain(certificates)
Pair(certificates.first().subjectDN.name, listOf(RolePrincipal(PEER_ROLE))) Pair(certificates.first().subjectDN.name, listOf(RolePrincipal(PEER_ROLE)))
} }
else -> { else -> {
@ -176,8 +176,8 @@ abstract class BaseBrokerJaasLoginModule : LoginModule {
protected lateinit var callbackHandler: CallbackHandler protected lateinit var callbackHandler: CallbackHandler
protected val principals = ArrayList<Principal>() protected val principals = ArrayList<Principal>()
@Suppress("DEPRECATION") // should use java.security.cert.X509Certificate @Suppress("ThrowsCount")
protected fun getUsernamePasswordAndCerts(): Triple<String, String, Array<javax.security.cert.X509Certificate>?> { protected fun getUsernamePasswordAndCerts(): Triple<String, String, Array<X509Certificate>> {
val nameCallback = NameCallback("Username: ") val nameCallback = NameCallback("Username: ")
val passwordCallback = PasswordCallback("Password: ", false) val passwordCallback = PasswordCallback("Password: ", false)
val certificateCallback = CertificateCallback() val certificateCallback = CertificateCallback()

View File

@ -13,7 +13,7 @@ sealed class CertificateChainCheckPolicy {
@FunctionalInterface @FunctionalInterface
interface Check { interface Check {
fun checkCertificateChain(theirChain: Array<javax.security.cert.X509Certificate>) fun checkCertificateChain(theirChain: Array<java.security.cert.X509Certificate>)
} }
abstract fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check abstract fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check
@ -21,7 +21,7 @@ sealed class CertificateChainCheckPolicy {
object Any : CertificateChainCheckPolicy() { object Any : CertificateChainCheckPolicy() {
override fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check { override fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check {
return object : Check { return object : Check {
override fun checkCertificateChain(theirChain: Array<javax.security.cert.X509Certificate>) { override fun checkCertificateChain(theirChain: Array<java.security.cert.X509Certificate>) {
// nothing to do here // nothing to do here
} }
} }
@ -33,7 +33,7 @@ sealed class CertificateChainCheckPolicy {
val rootAliases = trustStore.aliases().asSequence().filter { it.startsWith(X509Utilities.CORDA_ROOT_CA) } val rootAliases = trustStore.aliases().asSequence().filter { it.startsWith(X509Utilities.CORDA_ROOT_CA) }
val rootPublicKeys = rootAliases.map { trustStore.getCertificate(it).publicKey }.toSet() val rootPublicKeys = rootAliases.map { trustStore.getCertificate(it).publicKey }.toSet()
return object : Check { return object : Check {
override fun checkCertificateChain(theirChain: Array<javax.security.cert.X509Certificate>) { override fun checkCertificateChain(theirChain: Array<java.security.cert.X509Certificate>) {
val theirRoot = theirChain.last().publicKey val theirRoot = theirChain.last().publicKey
if (theirRoot !in rootPublicKeys) { if (theirRoot !in rootPublicKeys) {
throw CertificateException("Root certificate mismatch, their root = $theirRoot") throw CertificateException("Root certificate mismatch, their root = $theirRoot")
@ -47,7 +47,7 @@ sealed class CertificateChainCheckPolicy {
override fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check { override fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check {
val ourPublicKey = keyStore.getCertificate(X509Utilities.CORDA_CLIENT_TLS).publicKey val ourPublicKey = keyStore.getCertificate(X509Utilities.CORDA_CLIENT_TLS).publicKey
return object : Check { return object : Check {
override fun checkCertificateChain(theirChain: Array<javax.security.cert.X509Certificate>) { override fun checkCertificateChain(theirChain: Array<java.security.cert.X509Certificate>) {
val theirLeaf = theirChain.first().publicKey val theirLeaf = theirChain.first().publicKey
if (ourPublicKey != theirLeaf) { if (ourPublicKey != theirLeaf) {
throw CertificateException("Leaf certificate mismatch, their leaf = $theirLeaf") throw CertificateException("Leaf certificate mismatch, their leaf = $theirLeaf")
@ -61,7 +61,7 @@ sealed class CertificateChainCheckPolicy {
override fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check { override fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check {
val trustedPublicKeys = trustedAliases.map { trustStore.getCertificate(it).publicKey }.toSet() val trustedPublicKeys = trustedAliases.map { trustStore.getCertificate(it).publicKey }.toSet()
return object : Check { return object : Check {
override fun checkCertificateChain(theirChain: Array<javax.security.cert.X509Certificate>) { override fun checkCertificateChain(theirChain: Array<java.security.cert.X509Certificate>) {
if (!theirChain.any { it.publicKey in trustedPublicKeys }) { if (!theirChain.any { it.publicKey in trustedPublicKeys }) {
throw CertificateException("Their certificate chain contained none of the trusted ones") throw CertificateException("Their certificate chain contained none of the trusted ones")
} }
@ -78,7 +78,7 @@ sealed class CertificateChainCheckPolicy {
class UsernameMustMatchCommonNameCheck : Check { class UsernameMustMatchCommonNameCheck : Check {
lateinit var username: String lateinit var username: String
override fun checkCertificateChain(theirChain: Array<javax.security.cert.X509Certificate>) { override fun checkCertificateChain(theirChain: Array<java.security.cert.X509Certificate>) {
if (!theirChain.any { certificate -> CordaX500Name.parse(certificate.subjectDN.name).commonName == username }) { if (!theirChain.any { certificate -> CordaX500Name.parse(certificate.subjectDN.name).commonName == username }) {
throw CertificateException("Client certificate does not match login username.") throw CertificateException("Client certificate does not match login username.")
} }

View File

@ -1,13 +1,15 @@
@file:Suppress("DEPRECATION")
package net.corda.node.internal.checkpoints package net.corda.node.internal.checkpoints
import net.corda.core.internal.PLATFORM_VERSION import net.corda.core.internal.PLATFORM_VERSION
import net.corda.core.internal.messaging.FlowManagerRPCOps import net.corda.core.messaging.flows.FlowManagerRPCOps
import net.corda.node.services.rpc.CheckpointDumperImpl import net.corda.node.services.rpc.CheckpointDumperImpl
import net.corda.core.internal.messaging.FlowManagerRPCOps as InternalFlowManagerRPCOps
/** /**
* Implementation of [FlowManagerRPCOps] * Implementation of [FlowManagerRPCOps]
*/ */
internal class FlowManagerRPCOpsImpl(private val checkpointDumper: CheckpointDumperImpl) : FlowManagerRPCOps { internal class FlowManagerRPCOpsImpl(private val checkpointDumper: CheckpointDumperImpl) : FlowManagerRPCOps, InternalFlowManagerRPCOps {
override val protocolVersion: Int = PLATFORM_VERSION override val protocolVersion: Int = PLATFORM_VERSION

View File

@ -0,0 +1,87 @@
package net.corda.node.internal.shell
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.shell.determineUnsafeUsers
import net.corda.node.services.config.shell.toShellConfigMap
import net.corda.nodeapi.internal.cordapp.CordappLoader
import org.slf4j.LoggerFactory
object InteractiveShell {
private val log = LoggerFactory.getLogger(InteractiveShell::class.java)
private const val INTERACTIVE_SHELL_CLASS = "net.corda.tools.shell.InteractiveShell"
private const val CRASH_COMMAND_CLASS = "org.crsh.ssh.term.CRaSHCommand"
private const val START_SHELL_METHOD = "startShell"
private const val RUN_LOCAL_SHELL_METHOD = "runLocalShell"
private const val SET_USER_INFO_METHOD = "setUserInfo"
fun startShellIfInstalled(configuration: NodeConfiguration, cordappLoader: CordappLoader): Boolean {
return if (isShellInstalled()) {
try {
val shellConfiguration = configuration.toShellConfigMap()
setUnsafeUsers(configuration)
startShell(shellConfiguration, cordappLoader)
true
} catch (e: Exception) {
log.error("Shell failed to start", e)
false
}
} else {
false
}
}
/**
* Only call this after [startShellIfInstalled] has been called or the required classes will not be loaded into the current classloader.
*/
fun runLocalShellIfInstalled(onExit: () -> Unit = {}): Boolean {
return if (isShellInstalled()) {
try {
runLocalShell(onExit)
true
} catch (e: Exception) {
log.error("Shell failed to start", e)
false
}
} else {
false
}
}
private fun isShellInstalled(): Boolean {
return try {
javaClass.classLoader.loadClass(INTERACTIVE_SHELL_CLASS)
true
} catch (e: ClassNotFoundException) {
false
}
}
private fun setUnsafeUsers(configuration: NodeConfiguration) {
val unsafeUsers = determineUnsafeUsers(configuration)
val clazz = javaClass.classLoader.loadClass(CRASH_COMMAND_CLASS)
clazz.getDeclaredMethod(SET_USER_INFO_METHOD, Set::class.java, Boolean::class.java, Boolean::class.java)
.invoke(null, unsafeUsers, true, false)
log.info("Setting unsafe users as: $unsafeUsers")
}
private fun startShell(shellConfiguration: Map<String, Any?>, cordappLoader: CordappLoader) {
val clazz = javaClass.classLoader.loadClass(INTERACTIVE_SHELL_CLASS)
val instance = clazz.getDeclaredConstructor()
.apply { this.isAccessible = true }
.newInstance()
clazz.getDeclaredMethod(START_SHELL_METHOD, Map::class.java, ClassLoader::class.java, Boolean::class.java)
.invoke(instance, shellConfiguration, cordappLoader.appClassLoader, false)
}
private fun runLocalShell(onExit: () -> Unit = {}) {
val clazz = javaClass.classLoader.loadClass(INTERACTIVE_SHELL_CLASS)
// Gets the existing instance created by [startShell] as [InteractiveShell] is a static instance
val instance = clazz.getDeclaredConstructor()
.apply { this.isAccessible = true }
.newInstance()
clazz.getDeclaredMethod(RUN_LOCAL_SHELL_METHOD, Function0::class.java).invoke(instance, onExit)
}
}

View File

@ -13,6 +13,7 @@ import net.corda.node.internal.DBNetworkParametersStorage
import net.corda.node.internal.schemas.NodeInfoSchemaV1 import net.corda.node.internal.schemas.NodeInfoSchemaV1
import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.identity.PersistentIdentityService
import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.node.services.keys.BasicHSMKeyManagementService
import net.corda.node.services.network.PersistentNetworkMapCache
import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.DBTransactionStorage
import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.persistence.NodeAttachmentService
import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.NodeVaultService
@ -132,7 +133,8 @@ object VaultMigrationSchemaV1 : MappedSchema(schemaFamily = VaultMigrationSchema
PersistentIdentityService.PersistentPublicKeyHashToParty::class.java, PersistentIdentityService.PersistentPublicKeyHashToParty::class.java,
BasicHSMKeyManagementService.PersistentKey::class.java, BasicHSMKeyManagementService.PersistentKey::class.java,
NodeAttachmentService.DBAttachment::class.java, NodeAttachmentService.DBAttachment::class.java,
DBNetworkParametersStorage.PersistentNetworkParameters::class.java DBNetworkParametersStorage.PersistentNetworkParameters::class.java,
PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java
) )
) )

View File

@ -15,4 +15,6 @@ interface IdentityServiceInternal : IdentityService {
fun verifyAndRegisterNewRandomIdentity(identity: PartyAndCertificate) fun verifyAndRegisterNewRandomIdentity(identity: PartyAndCertificate)
fun invalidateCaches(name: CordaX500Name) {} fun invalidateCaches(name: CordaX500Name) {}
fun archiveNamedIdentity(name:String, publicKeyHash: String?) {}
} }

View File

@ -11,17 +11,18 @@ import net.corda.core.internal.notary.NotaryServiceFlow
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.services.config.rpc.NodeRpcOptions import net.corda.node.services.config.rpc.NodeRpcOptions
import net.corda.node.services.config.schema.v1.V1NodeConfigurationSpec import net.corda.node.services.config.schema.v1.V1NodeConfigurationSpec
import net.corda.node.services.config.shell.SSHDConfiguration
import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier
import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.config.MutualSslConfiguration
import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.notary.experimental.bftsmart.BFTSmartConfig import net.corda.notary.experimental.bftsmart.BFTSmartConfig
import net.corda.notary.experimental.raft.RaftConfig import net.corda.notary.experimental.raft.RaftConfig
import net.corda.tools.shell.SSHDConfiguration
import java.net.URL import java.net.URL
import java.nio.file.Path import java.nio.file.Path
import java.time.Duration import java.time.Duration
import java.util.* import java.util.Properties
import java.util.UUID
import javax.security.auth.x500.X500Principal import javax.security.auth.x500.X500Principal
val Int.MB: Long get() = this * 1024L * 1024L val Int.MB: Long get() = this * 1024L * 1024L

View File

@ -8,6 +8,7 @@ import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import net.corda.node.services.config.rpc.NodeRpcOptions import net.corda.node.services.config.rpc.NodeRpcOptions
import net.corda.node.services.config.shell.SSHDConfiguration
import net.corda.nodeapi.BrokerRpcSslOptions import net.corda.nodeapi.BrokerRpcSslOptions
import net.corda.nodeapi.internal.DEV_PUB_KEY_HASHES import net.corda.nodeapi.internal.DEV_PUB_KEY_HASHES
import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier
@ -15,11 +16,11 @@ import net.corda.nodeapi.internal.config.MutualSslConfiguration
import net.corda.nodeapi.internal.config.SslConfiguration import net.corda.nodeapi.internal.config.SslConfiguration
import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.tools.shell.SSHDConfiguration
import java.net.URL import java.net.URL
import java.nio.file.Path import java.nio.file.Path
import java.time.Duration import java.time.Duration
import java.util.* import java.util.Properties
import java.util.UUID
import javax.security.auth.x500.X500Principal import javax.security.auth.x500.X500Principal
data class NodeConfigurationImpl( data class NodeConfigurationImpl(

View File

@ -40,13 +40,14 @@ import net.corda.node.services.config.schema.parsers.toProperties
import net.corda.node.services.config.schema.parsers.toURL import net.corda.node.services.config.schema.parsers.toURL
import net.corda.node.services.config.schema.parsers.toUUID import net.corda.node.services.config.schema.parsers.toUUID
import net.corda.node.services.config.schema.parsers.validValue import net.corda.node.services.config.schema.parsers.validValue
import net.corda.node.services.config.shell.SSHDConfiguration
import net.corda.nodeapi.BrokerRpcSslOptions import net.corda.nodeapi.BrokerRpcSslOptions
import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel
import net.corda.notary.experimental.bftsmart.BFTSmartConfig import net.corda.notary.experimental.bftsmart.BFTSmartConfig
import net.corda.notary.experimental.raft.RaftConfig import net.corda.notary.experimental.raft.RaftConfig
import net.corda.tools.shell.SSHDConfiguration import java.util.Properties
internal object UserSpec : Configuration.Specification<User>("User") { internal object UserSpec : Configuration.Specification<User>("User") {
private val username by string().optional() private val username by string().optional()
@ -67,9 +68,32 @@ internal object UserSpec : Configuration.Specification<User>("User") {
internal object SecurityConfigurationSpec : Configuration.Specification<SecurityConfiguration>("SecurityConfiguration") { internal object SecurityConfigurationSpec : Configuration.Specification<SecurityConfiguration>("SecurityConfiguration") {
private object AuthServiceSpec : Configuration.Specification<SecurityConfiguration.AuthService>("AuthService") { private object AuthServiceSpec : Configuration.Specification<SecurityConfiguration.AuthService>("AuthService") {
private object DataSourceSpec : Configuration.Specification<SecurityConfiguration.AuthService.DataSource>("DataSource") { private object DataSourceSpec : Configuration.Specification<SecurityConfiguration.AuthService.DataSource>("DataSource") {
fun Properties.enablePasswordMasking(): Properties {
class PwMasking : Properties() {
fun maskPassword(): Properties {
if (!containsKey("password")) return this
val propsNoPassword = Properties()
// if the properties are passed in to the constructor as defaults
// they don't get printed so adding all keys explicitly
propsNoPassword.putAll(this)
propsNoPassword.setProperty("password", "***")
return propsNoPassword
}
override fun toString(): String {
val props = maskPassword()
return props.toString()
}
}
val masker = PwMasking()
masker.putAll(this)
return masker
}
private val type by enum(AuthDataSourceType::class) private val type by enum(AuthDataSourceType::class)
private val passwordEncryption by enum(PasswordEncryption::class).optional().withDefaultValue(SecurityConfiguration.AuthService.DataSource.Defaults.passwordEncryption) private val passwordEncryption by enum(PasswordEncryption::class).optional().withDefaultValue(SecurityConfiguration.AuthService.DataSource.Defaults.passwordEncryption)
private val connection by nestedObject(sensitive = true).map(::toProperties).optional() private val connection by nestedObject(sensitive = true).map{ toProperties(it).enablePasswordMasking() }.optional()
private val users by nested(UserSpec).list().optional() private val users by nested(UserSpec).list().optional()
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<SecurityConfiguration.AuthService.DataSource> { override fun parseValid(configuration: Config, options: Configuration.Options): Valid<SecurityConfiguration.AuthService.DataSource> {

View File

@ -1,4 +1,4 @@
package net.corda.tools.shell package net.corda.node.services.config.shell
data class SSHDConfiguration(val port: Int) { data class SSHDConfiguration(val port: Int) {
companion object { companion object {
@ -11,7 +11,7 @@ data class SSHDConfiguration(val port: Int) {
*/ */
@JvmStatic @JvmStatic
fun parse(str: String): SSHDConfiguration { fun parse(str: String): SSHDConfiguration {
require(!str.isBlank()) { SSHDConfiguration.MISSING_PORT_FORMAT.format(str) } require(str.isNotBlank()) { MISSING_PORT_FORMAT.format(str) }
val port = try { val port = try {
str.toInt() str.toInt()
} catch (ex: NumberFormatException) { } catch (ex: NumberFormatException) {

View File

@ -3,22 +3,23 @@ package net.corda.node.services.config.shell
import net.corda.core.internal.div import net.corda.core.internal.div
import net.corda.node.internal.clientSslOptionsCompatibleWith import net.corda.node.internal.clientSslOptionsCompatibleWith
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.tools.shell.ShellConfiguration
import net.corda.tools.shell.ShellConfiguration.Companion.COMMANDS_DIR private const val COMMANDS_DIR = "shell-commands"
import net.corda.tools.shell.ShellConfiguration.Companion.CORDAPPS_DIR private const val CORDAPPS_DIR = "cordapps"
import net.corda.tools.shell.ShellConfiguration.Companion.SSHD_HOSTKEY_DIR private const val SSHD_HOSTKEY_DIR = "ssh"
//re-packs data to Shell specific classes //re-packs data to Shell specific classes
fun NodeConfiguration.toShellConfig() = ShellConfiguration( fun NodeConfiguration.toShellConfigMap() = mapOf(
commandsDirectory = this.baseDirectory / COMMANDS_DIR, "commandsDirectory" to this.baseDirectory / COMMANDS_DIR,
cordappsDirectory = this.baseDirectory.toString() / CORDAPPS_DIR, "cordappsDirectory" to this.baseDirectory.toString() / CORDAPPS_DIR,
user = INTERNAL_SHELL_USER, "user" to INTERNAL_SHELL_USER,
password = internalShellPassword, "password" to internalShellPassword,
permissions = internalShellPermissions(!this.localShellUnsafe), "permissions" to internalShellPermissions(!this.localShellUnsafe),
localShellAllowExitInSafeMode = this.localShellAllowExitInSafeMode, "localShellAllowExitInSafeMode" to this.localShellAllowExitInSafeMode,
localShellUnsafe = this.localShellUnsafe, "localShellUnsafe" to this.localShellUnsafe,
hostAndPort = this.rpcOptions.address, "hostAndPort" to this.rpcOptions.address,
ssl = clientSslOptionsCompatibleWith(this.rpcOptions), "ssl" to clientSslOptionsCompatibleWith(this.rpcOptions),
sshdPort = this.sshd?.port, "sshdPort" to this.sshd?.port,
sshHostKeyDirectory = this.baseDirectory / SSHD_HOSTKEY_DIR, "sshHostKeyDirectory" to this.baseDirectory / SSHD_HOSTKEY_DIR,
noLocalShell = this.noLocalShell) "noLocalShell" to this.noLocalShell
)

View File

@ -1,5 +1,6 @@
package net.corda.node.services.identity package net.corda.node.services.identity
import com.google.common.util.concurrent.ThreadFactoryBuilder
import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto
import net.corda.core.crypto.toStringShort import net.corda.core.crypto.toStringShort
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
@ -22,6 +23,7 @@ import net.corda.node.internal.schemas.NodeInfoSchemaV1
import net.corda.node.services.api.IdentityServiceInternal import net.corda.node.services.api.IdentityServiceInternal
import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.node.services.keys.BasicHSMKeyManagementService
import net.corda.node.services.network.NotaryUpdateListener import net.corda.node.services.network.NotaryUpdateListener
import net.corda.node.services.network.PersistentNetworkMapCache
import net.corda.node.services.persistence.PublicKeyHashToExternalId import net.corda.node.services.persistence.PublicKeyHashToExternalId
import net.corda.node.services.persistence.WritablePublicKeyToOwningIdentityCache import net.corda.node.services.persistence.WritablePublicKeyToOwningIdentityCache
import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.node.utilities.AppendOnlyPersistentMap
@ -46,6 +48,8 @@ import java.security.cert.CollectionCertStoreParameters
import java.security.cert.TrustAnchor import java.security.cert.TrustAnchor
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.* import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.stream.Stream import java.util.stream.Stream
import javax.annotation.concurrent.ThreadSafe import javax.annotation.concurrent.ThreadSafe
import javax.persistence.Column import javax.persistence.Column
@ -140,6 +144,8 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri
private fun mapToKey(party: PartyAndCertificate) = party.owningKey.toStringShort() private fun mapToKey(party: PartyAndCertificate) = party.owningKey.toStringShort()
} }
val archiveIdentityExecutor: ExecutorService = Executors.newCachedThreadPool(ThreadFactoryBuilder().setNameFormat("archive-named-identity-thread-%d").build())
@Entity @Entity
@javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}identities") @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}identities")
class PersistentPublicKeyHashToCertificate( class PersistentPublicKeyHashToCertificate(
@ -312,7 +318,76 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri
} }
override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = database.transaction { override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = database.transaction {
nameToParty[name]?.orElse(null) if (nameToParty[name]?.isPresent == true) {
nameToParty[name]?.get()
}
else {
retrievePartyFromArchive(name)
}
}
private fun retrievePartyFromArchive(name: CordaX500Name): Party? {
val hashKey = database.transaction {
val query = session.criteriaBuilder.createQuery(PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java)
val queryRoot = query.from(PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java)
query.where(session.criteriaBuilder.equal(queryRoot.get<String>("name"), name.toString()))
val resultList = session.createQuery(query).resultList
if (resultList.isNotEmpty()) {
resultList?.first()?.publicKeyHash
}
else {
retrieveHashKeyAndCacheParty(name)
}
}
return hashKey?.let { keyToPartyAndCert[it]?.party }
}
private fun retrieveHashKeyAndCacheParty(name: CordaX500Name): String? {
return database.transaction {
val cb = session.criteriaBuilder
val query = cb.createQuery(PersistentPublicKeyHashToParty::class.java)
val root = query.from(PersistentPublicKeyHashToParty::class.java)
val isNotConfidentialIdentity = cb.equal(root.get<String>("publicKeyHash"), root.get<String>("owningKeyHash"))
val matchName = cb.equal(root.get<String>("name"), name.toString())
query.select(root).where(cb.and(matchName, isNotConfidentialIdentity))
val resultList = session.createQuery(query).resultList
var hashKey: String? = if (resultList.isNotEmpty()) {
if (resultList.size == 1) {
resultList?.single()?.owningKeyHash
}
else {
selectIdentityHash(session, resultList.mapNotNull { it.publicKeyHash }, name)
}
} else {
null
}
archiveNamedIdentity(name.toString(), hashKey)
hashKey
}
}
private fun selectIdentityHash(session: Session, hashList: List<String>, name: CordaX500Name): String? {
val cb = session.criteriaBuilder
val query = cb.createQuery(PersistentPublicKeyHashToCertificate::class.java)
val root = query.from(PersistentPublicKeyHashToCertificate::class.java)
query.select(root).where(root.get<String>("publicKeyHash").`in`(hashList))
val resultList = session.createQuery(query).resultList
resultList.sortWith(compareBy { PartyAndCertificate(X509CertificateFactory().delegate.generateCertPath(it.identity.inputStream())).certificate.notBefore })
log.warn("Retrieving identity hash for removed identity '${name}', more that one hash found, returning last one according to notBefore validity of certificate." +
" Hash returned is ${resultList.last().publicKeyHash}")
return resultList.last().publicKeyHash
}
override fun archiveNamedIdentity(name:String, publicKeyHash: String?) {
archiveIdentityExecutor.submit {
database.transaction {
val deleteQuery = session.criteriaBuilder.createCriteriaDelete(PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java)
val queryRoot = deleteQuery.from(PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java)
deleteQuery.where(session.criteriaBuilder.equal(queryRoot.get<String>("name"), name))
session.createQuery(deleteQuery).executeUpdate()
session.save(PersistentNetworkMapCache.PersistentPartyToPublicKeyHash(name, publicKeyHash))
}
}.get()
} }
override fun wellKnownPartyFromAnonymous(party: AbstractParty): Party? { override fun wellKnownPartyFromAnonymous(party: AbstractParty): Party? {
@ -323,7 +398,12 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri
if (candidate != null && candidate != party) { if (candidate != null && candidate != party) {
// Party doesn't match existing well-known party: check that the key is registered, otherwise return null. // Party doesn't match existing well-known party: check that the key is registered, otherwise return null.
require(party.name == candidate.name) { "Candidate party $candidate does not match expected $party" } require(party.name == candidate.name) { "Candidate party $candidate does not match expected $party" }
// If the party is a whitelisted notary, then it was just a rotated notary key
if (party in notaryIdentityCache) {
candidate
} else {
keyToParty[party.owningKey.toStringShort()]?.let { candidate } keyToParty[party.owningKey.toStringShort()]?.let { candidate }
}
} else { } else {
// Party is a well-known party or well-known party doesn't exist: skip checks. // Party is a well-known party or well-known party doesn't exist: skip checks.
// If the notary is not in the network map cache, try getting it from the network parameters // If the notary is not in the network map cache, try getting it from the network parameters

View File

@ -18,6 +18,7 @@ import net.corda.node.internal.artemis.SecureArtemisConfiguration
import net.corda.node.internal.artemis.UserValidationPlugin import net.corda.node.internal.artemis.UserValidationPlugin
import net.corda.node.internal.artemis.isBindingError import net.corda.node.internal.artemis.isBindingError
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.node.utilities.artemis.startSynchronously
import net.corda.nodeapi.internal.AmqpMessageSizeChecksInterceptor import net.corda.nodeapi.internal.AmqpMessageSizeChecksInterceptor
import net.corda.nodeapi.internal.ArtemisMessageSizeChecksInterceptor import net.corda.nodeapi.internal.ArtemisMessageSizeChecksInterceptor
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX
@ -41,7 +42,6 @@ import org.apache.activemq.artemis.core.security.Role
import org.apache.activemq.artemis.core.server.ActiveMQServer import org.apache.activemq.artemis.core.server.ActiveMQServer
import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager
import java.io.IOException
import java.lang.Long.max import java.lang.Long.max
import javax.annotation.concurrent.ThreadSafe import javax.annotation.concurrent.ThreadSafe
import javax.security.auth.login.AppConfigurationEntry import javax.security.auth.login.AppConfigurationEntry
@ -107,21 +107,21 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
val artemisConfig = createArtemisConfig() val artemisConfig = createArtemisConfig()
val securityManager = createArtemisSecurityManager() val securityManager = createArtemisSecurityManager()
activeMQServer = ActiveMQServerImpl(artemisConfig, securityManager).apply { activeMQServer = ActiveMQServerImpl(artemisConfig, securityManager).apply {
// Throw any exceptions which are detected during startup
registerActivationFailureListener { exception -> throw exception }
// Some types of queue might need special preparation on our side, like dialling back or preparing // Some types of queue might need special preparation on our side, like dialling back or preparing
// a lazily initialised subsystem. // a lazily initialised subsystem.
registerPostQueueCreationCallback { log.debug { "Queue Created: $it" } } registerPostQueueCreationCallback { log.debug { "Queue Created: $it" } }
registerPostQueueDeletionCallback { address, qName -> log.debug { "Queue deleted: $qName for $address" } } registerPostQueueDeletionCallback { address, qName -> log.debug { "Queue deleted: $qName for $address" } }
} }
@Suppress("TooGenericExceptionCaught")
try { try {
activeMQServer.start() activeMQServer.startSynchronously()
} catch (e: IOException) { } catch (e: Throwable) {
log.error("Unable to start message broker", e) log.error("Unable to start message broker", e)
if (e.isBindingError()) { if (e.isBindingError()) {
throw AddressBindingException(config.p2pAddress) throw AddressBindingException(config.p2pAddress)
} else { } else {
log.error("Unexpected error starting message broker", e)
throw e throw e
} }
} }

View File

@ -5,50 +5,37 @@ import io.netty.channel.ChannelHandlerContext
import io.netty.channel.group.ChannelGroup import io.netty.channel.group.ChannelGroup
import io.netty.handler.logging.LogLevel import io.netty.handler.logging.LogLevel
import io.netty.handler.logging.LoggingHandler import io.netty.handler.logging.LoggingHandler
import io.netty.handler.ssl.SslContext
import io.netty.handler.ssl.SslContextBuilder
import io.netty.handler.ssl.SslHandler import io.netty.handler.ssl.SslHandler
import io.netty.handler.ssl.SslHandshakeTimeoutException import io.netty.handler.ssl.SslHandshakeTimeoutException
import io.netty.handler.ssl.SslProvider
import net.corda.core.internal.declaredField import net.corda.core.internal.declaredField
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.trace import net.corda.core.utilities.trace
import net.corda.nodeapi.internal.ArtemisTcpTransport import net.corda.nodeapi.internal.ArtemisTcpTransport
import net.corda.nodeapi.internal.config.CertificateStore
import net.corda.nodeapi.internal.protonwrapper.netty.createAndInitSslContext
import net.corda.nodeapi.internal.protonwrapper.netty.keyManagerFactory
import net.corda.nodeapi.internal.protonwrapper.netty.sslDelegatedTaskExecutor import net.corda.nodeapi.internal.protonwrapper.netty.sslDelegatedTaskExecutor
import net.corda.nodeapi.internal.setThreadPoolName import net.corda.nodeapi.internal.setThreadPoolName
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration
import org.apache.activemq.artemis.api.core.BaseInterceptor import org.apache.activemq.artemis.api.core.BaseInterceptor
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptor import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptor
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants import org.apache.activemq.artemis.core.server.balancing.RedirectHandler
import org.apache.activemq.artemis.core.remoting.impl.ssl.SSLSupport
import org.apache.activemq.artemis.core.server.ActiveMQServerLogger
import org.apache.activemq.artemis.core.server.cluster.ClusterConnection import org.apache.activemq.artemis.core.server.cluster.ClusterConnection
import org.apache.activemq.artemis.spi.core.protocol.ProtocolManager import org.apache.activemq.artemis.spi.core.protocol.ProtocolManager
import org.apache.activemq.artemis.spi.core.remoting.Acceptor import org.apache.activemq.artemis.spi.core.remoting.Acceptor
import org.apache.activemq.artemis.spi.core.remoting.AcceptorFactory import org.apache.activemq.artemis.spi.core.remoting.AcceptorFactory
import org.apache.activemq.artemis.spi.core.remoting.BufferHandler import org.apache.activemq.artemis.spi.core.remoting.BufferHandler
import org.apache.activemq.artemis.spi.core.remoting.ServerConnectionLifeCycleListener import org.apache.activemq.artemis.spi.core.remoting.ServerConnectionLifeCycleListener
import org.apache.activemq.artemis.spi.core.remoting.ssl.OpenSSLContextFactoryProvider
import org.apache.activemq.artemis.spi.core.remoting.ssl.SSLContextFactoryProvider
import org.apache.activemq.artemis.utils.ConfigurationHelper import org.apache.activemq.artemis.utils.ConfigurationHelper
import org.apache.activemq.artemis.utils.actors.OrderedExecutor import org.apache.activemq.artemis.utils.actors.OrderedExecutor
import java.net.SocketAddress import java.net.SocketAddress
import java.nio.channels.ClosedChannelException import java.nio.channels.ClosedChannelException
import java.nio.file.Paths
import java.security.PrivilegedExceptionAction
import java.time.Duration import java.time.Duration
import java.util.concurrent.Executor import java.util.concurrent.Executor
import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.ScheduledExecutorService
import java.util.regex.Pattern import java.util.regex.Pattern
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLEngine import javax.net.ssl.SSLEngine
import javax.net.ssl.SSLPeerUnverifiedException import javax.net.ssl.SSLPeerUnverifiedException
import javax.net.ssl.TrustManagerFactory
import javax.security.auth.Subject
@Suppress("unused", "TooGenericExceptionCaught", "ComplexMethod", "MagicNumber", "TooManyFunctions") @Suppress("unused") // Used via reflection in ArtemisTcpTransport
class NodeNettyAcceptorFactory : AcceptorFactory { class NodeNettyAcceptorFactory : AcceptorFactory {
override fun createAcceptor(name: String?, override fun createAcceptor(name: String?,
clusterConnection: ClusterConnection?, clusterConnection: ClusterConnection?,
@ -57,7 +44,7 @@ class NodeNettyAcceptorFactory : AcceptorFactory {
listener: ServerConnectionLifeCycleListener?, listener: ServerConnectionLifeCycleListener?,
threadPool: Executor, threadPool: Executor,
scheduledThreadPool: ScheduledExecutorService, scheduledThreadPool: ScheduledExecutorService,
protocolMap: Map<String, ProtocolManager<BaseInterceptor<*>>>?): Acceptor { protocolMap: MutableMap<String, ProtocolManager<BaseInterceptor<*>, RedirectHandler<*>>>?): Acceptor {
val threadPoolName = ConfigurationHelper.getStringProperty(ArtemisTcpTransport.THREAD_POOL_NAME_NAME, "Acceptor", configuration) val threadPoolName = ConfigurationHelper.getStringProperty(ArtemisTcpTransport.THREAD_POOL_NAME_NAME, "Acceptor", configuration)
threadPool.setThreadPoolName("$threadPoolName-artemis") threadPool.setThreadPoolName("$threadPoolName-artemis")
scheduledThreadPool.setThreadPoolName("$threadPoolName-artemis-scheduler") scheduledThreadPool.setThreadPoolName("$threadPoolName-artemis-scheduler")
@ -83,12 +70,18 @@ class NodeNettyAcceptorFactory : AcceptorFactory {
listener: ServerConnectionLifeCycleListener?, listener: ServerConnectionLifeCycleListener?,
scheduledThreadPool: ScheduledExecutorService?, scheduledThreadPool: ScheduledExecutorService?,
failureExecutor: Executor, failureExecutor: Executor,
protocolMap: Map<String, ProtocolManager<BaseInterceptor<*>>>?, protocolMap: MutableMap<String, ProtocolManager<BaseInterceptor<*>, RedirectHandler<*>>>?,
private val threadPoolName: String) : private val threadPoolName: String) :
NettyAcceptor(name, clusterConnection, configuration, handler, listener, scheduledThreadPool, failureExecutor, protocolMap) NettyAcceptor(name, clusterConnection, configuration, handler, listener, scheduledThreadPool, failureExecutor, protocolMap)
{ {
companion object { companion object {
private val defaultThreadPoolNamePattern = Pattern.compile("""Thread-(\d+) \(activemq-netty-threads\)""") private val defaultThreadPoolNamePattern = Pattern.compile("""Thread-(\d+) \(activemq-netty-threads\)""")
init {
// Make sure Artemis isn't using another (Open)SSLContextFactory
check(SSLContextFactoryProvider.getSSLContextFactory() is NodeSSLContextFactory)
check(OpenSSLContextFactoryProvider.getOpenSSLContextFactory() is NodeOpenSSLContextFactory)
}
} }
private val sslDelegatedTaskExecutor = sslDelegatedTaskExecutor(threadPoolName) private val sslDelegatedTaskExecutor = sslDelegatedTaskExecutor(threadPoolName)
@ -112,9 +105,9 @@ class NodeNettyAcceptorFactory : AcceptorFactory {
} }
@Synchronized @Synchronized
override fun getSslHandler(alloc: ByteBufAllocator?): SslHandler { override fun getSslHandler(alloc: ByteBufAllocator?, peerHost: String?, peerPort: Int): SslHandler {
applyThreadPoolName() applyThreadPoolName()
val engine = getSSLEngine(alloc) val engine = super.getSslHandler(alloc, peerHost, peerPort).engine()
val sslHandler = NodeAcceptorSslHandler(engine, sslDelegatedTaskExecutor, trace) val sslHandler = NodeAcceptorSslHandler(engine, sslDelegatedTaskExecutor, trace)
val handshakeTimeout = configuration[ArtemisTcpTransport.SSL_HANDSHAKE_TIMEOUT_NAME] as Duration? val handshakeTimeout = configuration[ArtemisTcpTransport.SSL_HANDSHAKE_TIMEOUT_NAME] as Duration?
if (handshakeTimeout != null) { if (handshakeTimeout != null) {
@ -132,111 +125,6 @@ class NodeNettyAcceptorFactory : AcceptorFactory {
Thread.currentThread().name = "$threadPoolName-${matcher.group(1)}" // Preserve the pool thread number Thread.currentThread().name = "$threadPoolName-${matcher.group(1)}" // Preserve the pool thread number
} }
} }
/**
* This is a copy of [NettyAcceptor.getSslHandler] so that we can provide different implementations for [loadOpenSslEngine] and
* [loadJdkSslEngine]. [NodeNettyAcceptor], instead of creating a default [TrustManagerFactory], will simply use the provided one in
* the [ArtemisTcpTransport.TRUST_MANAGER_FACTORY_NAME] configuration.
*/
private fun getSSLEngine(alloc: ByteBufAllocator?): SSLEngine {
val engine = if (sslProvider == TransportConstants.OPENSSL_PROVIDER) {
loadOpenSslEngine(alloc)
} else {
loadJdkSslEngine()
}
engine.useClientMode = false
if (needClientAuth) {
engine.needClientAuth = true
}
// setting the enabled cipher suites resets the enabled protocols so we need
// to save the enabled protocols so that after the customer cipher suite is enabled
// we can reset the enabled protocols if a customer protocol isn't specified
val originalProtocols = engine.enabledProtocols
if (enabledCipherSuites != null) {
try {
engine.enabledCipherSuites = SSLSupport.parseCommaSeparatedListIntoArray(enabledCipherSuites)
} catch (e: IllegalArgumentException) {
ActiveMQServerLogger.LOGGER.invalidCipherSuite(SSLSupport.parseArrayIntoCommandSeparatedList(engine.supportedCipherSuites))
throw e
}
}
if (enabledProtocols != null) {
try {
engine.enabledProtocols = SSLSupport.parseCommaSeparatedListIntoArray(enabledProtocols)
} catch (e: IllegalArgumentException) {
ActiveMQServerLogger.LOGGER.invalidProtocol(SSLSupport.parseArrayIntoCommandSeparatedList(engine.supportedProtocols))
throw e
}
} else {
engine.enabledProtocols = originalProtocols
}
return engine
}
/**
* Copy of [NettyAcceptor.loadOpenSslEngine] which invokes our custom [createOpenSslContext].
*/
private fun loadOpenSslEngine(alloc: ByteBufAllocator?): SSLEngine {
val context = try {
// We copied all this code just so we could replace the SSLSupport.createNettyContext method call with our own one.
createOpenSslContext()
} catch (e: Exception) {
throw IllegalStateException("Unable to create NodeNettyAcceptor", e)
}
return Subject.doAs<SSLEngine>(null, PrivilegedExceptionAction {
context.newEngine(alloc)
})
}
/**
* Copy of [NettyAcceptor.loadJdkSslEngine] which invokes our custom [createJdkSSLContext].
*/
private fun loadJdkSslEngine(): SSLEngine {
val context = try {
// We copied all this code just so we could replace the SSLHelper.createContext method call with our own one.
createJdkSSLContext()
} catch (e: Exception) {
throw IllegalStateException("Unable to create NodeNettyAcceptor", e)
}
return Subject.doAs<SSLEngine>(null, PrivilegedExceptionAction {
context.createSSLEngine()
})
}
/**
* Create an [SSLContext] using the [TrustManagerFactory] provided on the [ArtemisTcpTransport.TRUST_MANAGER_FACTORY_NAME] config.
*/
private fun createJdkSSLContext(): SSLContext {
return createAndInitSslContext(
createKeyManagerFactory(),
configuration[ArtemisTcpTransport.TRUST_MANAGER_FACTORY_NAME] as TrustManagerFactory?
)
}
/**
* Create an [SslContext] using the the [TrustManagerFactory] provided on the [ArtemisTcpTransport.TRUST_MANAGER_FACTORY_NAME] config.
*/
private fun createOpenSslContext(): SslContext {
return SslContextBuilder
.forServer(createKeyManagerFactory())
.sslProvider(SslProvider.OPENSSL)
.trustManager(configuration[ArtemisTcpTransport.TRUST_MANAGER_FACTORY_NAME] as TrustManagerFactory?)
.build()
}
private fun createKeyManagerFactory(): KeyManagerFactory {
return keyManagerFactory(CertificateStore.fromFile(Paths.get(keyStorePath), keyStorePassword, keyStorePassword, false))
}
// Replicate the fields which are private in NettyAcceptor
private val sslProvider = ConfigurationHelper.getStringProperty(TransportConstants.SSL_PROVIDER, TransportConstants.DEFAULT_SSL_PROVIDER, configuration)
private val needClientAuth = ConfigurationHelper.getBooleanProperty(TransportConstants.NEED_CLIENT_AUTH_PROP_NAME, TransportConstants.DEFAULT_NEED_CLIENT_AUTH, configuration)
private val enabledCipherSuites = ConfigurationHelper.getStringProperty(TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME, TransportConstants.DEFAULT_ENABLED_CIPHER_SUITES, configuration)
private val enabledProtocols = ConfigurationHelper.getStringProperty(TransportConstants.ENABLED_PROTOCOLS_PROP_NAME, TransportConstants.DEFAULT_ENABLED_PROTOCOLS, configuration)
private val keyStorePath = ConfigurationHelper.getStringProperty(TransportConstants.KEYSTORE_PATH_PROP_NAME, TransportConstants.DEFAULT_KEYSTORE_PATH, configuration)
private val keyStoreProvider = ConfigurationHelper.getStringProperty(TransportConstants.KEYSTORE_PROVIDER_PROP_NAME, TransportConstants.DEFAULT_KEYSTORE_PROVIDER, configuration)
private val keyStorePassword = ConfigurationHelper.getPasswordProperty(TransportConstants.KEYSTORE_PASSWORD_PROP_NAME, TransportConstants.DEFAULT_KEYSTORE_PASSWORD, configuration, ActiveMQDefaultConfiguration.getPropMaskPassword(), ActiveMQDefaultConfiguration.getPropPasswordCodec())
} }

View File

@ -0,0 +1,128 @@
package net.corda.node.services.messaging
import io.netty.handler.ssl.SslContext
import io.netty.handler.ssl.SslContextBuilder
import io.netty.handler.ssl.SslProvider
import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.TRUST_MANAGER_FACTORY_NAME
import net.corda.nodeapi.internal.protonwrapper.netty.createAndInitSslContext
import org.apache.activemq.artemis.core.remoting.impl.ssl.DefaultOpenSSLContextFactory
import org.apache.activemq.artemis.core.remoting.impl.ssl.DefaultSSLContextFactory
import org.apache.activemq.artemis.spi.core.remoting.ssl.SSLContextConfig
import org.apache.activemq.artemis.utils.ClassloadingUtil
import org.apache.commons.io.IOUtils
import java.io.File
import java.io.InputStream
import java.net.MalformedURLException
import java.net.URL
import java.security.AccessController
import java.security.KeyStore
import java.security.PrivilegedAction
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
class NodeSSLContextFactory : DefaultSSLContextFactory() {
override fun getSSLContext(config: SSLContextConfig, additionalOpts: Map<String, Any>): SSLContext {
val trustManagerFactory = additionalOpts[TRUST_MANAGER_FACTORY_NAME] as TrustManagerFactory?
return if (trustManagerFactory != null) {
createAndInitSslContext(loadKeyManagerFactory(config), trustManagerFactory)
} else {
super.getSSLContext(config, additionalOpts)
}
}
override fun getPriority(): Int {
// We make sure this factory is the one that's chosen, so any sufficiently large value will do.
return 15
}
}
class NodeOpenSSLContextFactory : DefaultOpenSSLContextFactory() {
override fun getServerSslContext(config: SSLContextConfig, additionalOpts: Map<String, Any>): SslContext {
val trustManagerFactory = additionalOpts[TRUST_MANAGER_FACTORY_NAME] as TrustManagerFactory?
return if (trustManagerFactory != null) {
SslContextBuilder
.forServer(loadKeyManagerFactory(config))
.sslProvider(SslProvider.OPENSSL)
.trustManager(trustManagerFactory)
.build()
} else {
super.getServerSslContext(config, additionalOpts)
}
}
override fun getPriority(): Int {
// We make sure this factory is the one that's chosen, so any sufficiently large value will do.
return 15
}
}
private fun loadKeyManagerFactory(config: SSLContextConfig): KeyManagerFactory {
val keyStore = loadKeystore(config.keystoreProvider, config.keystoreType, config.keystorePath, config.keystorePassword)
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
keyManagerFactory.init(keyStore, config.keystorePassword?.toCharArray())
return keyManagerFactory
}
/**
* This is a copy of [SSLSupport.loadKeystore] so that we can use the [NodeSSLContextFactory] and
* maintain keystore loading with the correct [keystoreProvider].
*/
private fun loadKeystore(
keystoreProvider: String?,
keystoreType: String,
keystorePath: String?,
keystorePassword: String?
) : KeyStore {
val keyStore = keystoreProvider?.let { KeyStore.getInstance(keystoreType, it) } ?: KeyStore.getInstance(keystoreType)
var inputStream : InputStream? = null
try {
if (keystorePath != null && keystorePath.isNotEmpty()) {
val keystoreURL = validateStoreURL(keystorePath)
inputStream = keystoreURL.openStream()
}
keyStore.load(inputStream, keystorePassword?.toCharArray())
} finally {
inputStream?.closeQuietly()
}
return keyStore
}
/**
* This is a copy of [SSLSupport.validateStoreURL] so we can have a full copy of
* [SSLSupport.loadKeystore].
*/
private fun validateStoreURL(storePath: String): URL {
return try {
URL(storePath)
} catch (e: MalformedURLException) {
val file = File(storePath)
if (file.exists() && file.isFile) {
file.toURI().toURL()
} else {
findResource(storePath)
}
}
}
/**
* This is a copy of [SSLSupport.findResource] so we can have a full copy of
* [SSLSupport.validateStoreURL] and.
*/
private fun findResource(resourceName: String): URL {
return AccessController.doPrivileged(PrivilegedAction {
ClassloadingUtil.findResource(resourceName)
})
}
/**
* This is an inline function for [InputStream] so it can be closed and
* ignore an exception.
*/
private fun InputStream?.closeQuietly() = IOUtils.closeQuietly(this)

View File

@ -24,6 +24,10 @@ class P2PMessageDeduplicator(cacheFactory: NamedCacheFactory, private val databa
private val beingProcessedMessages = ConcurrentHashMap<DeduplicationId, MessageMeta>() private val beingProcessedMessages = ConcurrentHashMap<DeduplicationId, MessageMeta>()
private val processedMessages = createProcessedMessages(cacheFactory) private val processedMessages = createProcessedMessages(cacheFactory)
enum class Outcome {
NEW, DUPLICATE, IN_FLIGHT
}
private fun createProcessedMessages(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap<DeduplicationId, MessageMeta, ProcessedMessage, String> { private fun createProcessedMessages(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap<DeduplicationId, MessageMeta, ProcessedMessage, String> {
return AppendOnlyPersistentMap( return AppendOnlyPersistentMap(
cacheFactory = cacheFactory, cacheFactory = cacheFactory,
@ -48,15 +52,17 @@ class P2PMessageDeduplicator(cacheFactory: NamedCacheFactory, private val databa
private fun senderHash(senderKey: SenderKey) = SecureHash.sha256(senderKey.peer.toString() + senderKey.isSessionInit.toString() + senderKey.senderUUID).toString() private fun senderHash(senderKey: SenderKey) = SecureHash.sha256(senderKey.peer.toString() + senderKey.isSessionInit.toString() + senderKey.senderUUID).toString()
/** /**
* @return true if we have seen this message before. * @return IN_FLIGHT if this message is currently being processed by the state machine, otherwise indicate if DUPLICATE or NEW.
*/ */
fun isDuplicate(msg: ReceivedMessage): Boolean { fun checkDuplicate(msg: ReceivedMessage): Outcome {
if (beingProcessedMessages.containsKey(msg.uniqueMessageId)) { if (beingProcessedMessages.containsKey(msg.uniqueMessageId)) {
return true return Outcome.IN_FLIGHT
} }
return isDuplicateInDatabase(msg) return booleanToEnum(isDuplicateInDatabase(msg))
} }
private fun booleanToEnum(isDuplicate: Boolean): Outcome = if (isDuplicate) Outcome.DUPLICATE else Outcome.NEW
/** /**
* Called the first time we encounter [deduplicationId]. * Called the first time we encounter [deduplicationId].
*/ */

View File

@ -18,7 +18,15 @@ import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.serialization.internal.nodeSerializationEnv
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.utilities.* import net.corda.core.utilities.ByteSequence
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.minutes
import net.corda.core.utilities.seconds
import net.corda.core.utilities.trace
import net.corda.node.VersionInfo import net.corda.node.VersionInfo
import net.corda.node.internal.LifecycleSupport import net.corda.node.internal.LifecycleSupport
import net.corda.node.internal.artemis.ReactiveArtemisConsumer import net.corda.node.internal.artemis.ReactiveArtemisConsumer
@ -31,12 +39,15 @@ import net.corda.node.services.statemachine.SenderDeduplicationId
import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor
import net.corda.node.utilities.errorAndTerminate import net.corda.node.utilities.errorAndTerminate
import net.corda.nodeapi.internal.ArtemisMessagingComponent import net.corda.nodeapi.internal.ArtemisMessagingComponent
import net.corda.nodeapi.internal.ArtemisMessagingComponent.* import net.corda.nodeapi.internal.ArtemisMessagingComponent.ArtemisAddress
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_CONTROL import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_CONTROL
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_NOTIFY import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_NOTIFY
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.JOURNAL_HEADER_SIZE import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.JOURNAL_HEADER_SIZE
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2PMessagingHeaders import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2PMessagingHeaders
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.NodeAddress
import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress
import net.corda.nodeapi.internal.ArtemisMessagingComponent.ServiceAddress
import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.p2pConnectorTcpTransport import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.p2pConnectorTcpTransport
import net.corda.nodeapi.internal.bridging.BridgeControl import net.corda.nodeapi.internal.bridging.BridgeControl
import net.corda.nodeapi.internal.bridging.BridgeEntry import net.corda.nodeapi.internal.bridging.BridgeEntry
@ -48,15 +59,26 @@ import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration
import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException
import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID
import org.apache.activemq.artemis.api.core.Message.HDR_VALIDATED_USER import org.apache.activemq.artemis.api.core.Message.HDR_VALIDATED_USER
import org.apache.activemq.artemis.api.core.QueueConfiguration
import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.RoutingType
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.client.* import org.apache.activemq.artemis.api.core.client.ActiveMQClient
import org.apache.activemq.artemis.api.core.client.ClientConsumer
import org.apache.activemq.artemis.api.core.client.ClientMessage
import org.apache.activemq.artemis.api.core.client.ClientProducer
import org.apache.activemq.artemis.api.core.client.ClientSession
import org.apache.activemq.artemis.api.core.client.ClientSessionFactory
import org.apache.activemq.artemis.api.core.client.FailoverEventType
import org.apache.activemq.artemis.api.core.client.ServerLocator
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import java.security.PublicKey import java.security.PublicKey
import java.time.Duration
import java.time.Instant import java.time.Instant
import java.util.* import java.util.Collections
import java.util.Timer
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import javax.annotation.concurrent.ThreadSafe import javax.annotation.concurrent.ThreadSafe
@ -72,15 +94,17 @@ import kotlin.concurrent.timer
* executor through into Artemis and from there, back through to senders. * executor through into Artemis and from there, back through to senders.
* *
* An implementation of [CordaRPCOps] can be provided. If given, clients using the CordaMQClient RPC library can * An implementation of [CordaRPCOps] can be provided. If given, clients using the CordaMQClient RPC library can
* invoke methods on the provided implementation. There is more documentation on this in the docsite and the * invoke methods on the provided implementation. There is more documentation on this in the doc-site and the
* CordaRPCClient class. * CordaRPCClient class.
* *
* @param config The configuration of the node, which is used for controlling the message redelivery options. * @param config The configuration of the node, which is used for controlling the message redelivery options.
* @param versionInfo All messages from the node carry the version info and received messages are checked against this for compatibility. * @param versionInfo All messages from the node carry the version info and received messages are checked against this for compatibility.
* @param serverAddress The host and port of the Artemis broker. * @param serverAddress The host and port of the Artemis broker.
* @param nodeExecutor The received messages are marshalled onto the server executor to prevent Netty buffers leaking during fiber suspends. * @param nodeExecutor The received messages are marshalled onto the server executor to prevent Netty buffers leaking during fiber suspends.
* @param database The nodes database, which is used to deduplicate messages. * @param database The node's database, which is used to deduplicate messages.
* @param terminateOnConnectionError whether the process should be terminated forcibly if connection with the broker fails.
*/ */
@Suppress("LongParameterList")
@ThreadSafe @ThreadSafe
class P2PMessagingClient(val config: NodeConfiguration, class P2PMessagingClient(val config: NodeConfiguration,
private val versionInfo: VersionInfo, private val versionInfo: VersionInfo,
@ -94,7 +118,9 @@ class P2PMessagingClient(val config: NodeConfiguration,
private val isDrainingModeOn: () -> Boolean, private val isDrainingModeOn: () -> Boolean,
private val drainingModeWasChangedEvents: Observable<Pair<Boolean, Boolean>>, private val drainingModeWasChangedEvents: Observable<Pair<Boolean, Boolean>>,
private val threadPoolName: String = "P2PClient", private val threadPoolName: String = "P2PClient",
private val stateHelper: ServiceStateHelper = ServiceStateHelper(log) private val stateHelper: ServiceStateHelper = ServiceStateHelper(log),
private val terminateOnConnectionError: Boolean = true,
private val timeoutConfig: TimeoutConfig = TimeoutConfig.default()
) : SingletonSerializeAsToken(), MessagingService, AddressToArtemisQueueResolver, ServiceStateSupport by stateHelper { ) : SingletonSerializeAsToken(), MessagingService, AddressToArtemisQueueResolver, ServiceStateSupport by stateHelper {
companion object { companion object {
private val log = contextLogger() private val log = contextLogger()
@ -127,6 +153,21 @@ class P2PMessagingClient(val config: NodeConfiguration,
fun sendMessage(address: String, message: ClientMessage) = producer!!.send(address, message) fun sendMessage(address: String, message: ClientMessage) = producer!!.send(address, message)
} }
/**
* @property callTimeout the time a blocking call (e.g. message send) from a client waits for a response until it times out.
* @property serverConnectionTtl the time the server waits for a packet/heartbeat from a client before it announces the connection dead and cleans it up.
* @property clientConnectionTtl the time the client waits for a packet/heartbeat from a client before it announces the connection dead and cleans it up.
*/
data class TimeoutConfig(val callTimeout: Duration, val serverConnectionTtl: Duration, val clientConnectionTtl: Duration) {
companion object {
/**
* Some sensible defaults, aligned with defaults of Artemis
*/
@Suppress("MagicNumber")
fun default() = TimeoutConfig(30.seconds, 60.seconds, 30.seconds)
}
}
/** A registration to handle messages of different types */ /** A registration to handle messages of different types */
data class HandlerRegistration(val topic: String, val callback: Any) : MessageHandlerRegistration data class HandlerRegistration(val topic: String, val callback: Any) : MessageHandlerRegistration
@ -167,15 +208,21 @@ class P2PMessagingClient(val config: NodeConfiguration,
// TODO Add broker CN to config for host verification in case the embedded broker isn't used // TODO Add broker CN to config for host verification in case the embedded broker isn't used
val tcpTransport = p2pConnectorTcpTransport(serverAddress, config.p2pSslOptions, threadPoolName = threadPoolName) val tcpTransport = p2pConnectorTcpTransport(serverAddress, config.p2pSslOptions, threadPoolName = threadPoolName)
locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply { locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply {
connectionTTL = 60000 callTimeout = timeoutConfig.callTimeout.toMillis()
clientFailureCheckPeriod = 30000 connectionTTL = timeoutConfig.serverConnectionTtl.toMillis()
clientFailureCheckPeriod = timeoutConfig.clientConnectionTtl.toMillis()
minLargeMessageSize = maxMessageSize + JOURNAL_HEADER_SIZE minLargeMessageSize = maxMessageSize + JOURNAL_HEADER_SIZE
isUseGlobalPools = nodeSerializationEnv != null isUseGlobalPools = nodeSerializationEnv != null
} }
sessionFactory = locator!!.createSessionFactory().addFailoverListener(::failoverCallback)
sessionFactory = if (terminateOnConnectionError) {
locator!!.createSessionFactory().addFailoverListener(::failoverCallback)
} else {
locator!!.createSessionFactory()
}
// Login using the node username. The broker will authenticate us as its node (as opposed to another peer) // Login using the node username. The broker will authenticate us as its node (as opposed to another peer)
// using our TLS certificate. // using our TLS certificate.
// Note that the acknowledgement of messages is not flushed to the Artermis journal until the default buffer // Note that the acknowledgement of messages is not flushed to the Artemis journal until the default buffer
// size of 1MB is acknowledged. // size of 1MB is acknowledged.
val createNewSession = { sessionFactory!!.createSession(ArtemisMessagingComponent.NODE_P2P_USER, ArtemisMessagingComponent.NODE_P2P_USER, false, true, true, false, ActiveMQClient.DEFAULT_ACK_BATCH_SIZE) } val createNewSession = { sessionFactory!!.createSession(ArtemisMessagingComponent.NODE_P2P_USER, ArtemisMessagingComponent.NODE_P2P_USER, false, true, true, false, ActiveMQClient.DEFAULT_ACK_BATCH_SIZE) }
@ -233,7 +280,8 @@ class P2PMessagingClient(val config: NodeConfiguration,
private fun InnerState.registerBridgeControl(session: ClientSession, inboxes: List<String>) { private fun InnerState.registerBridgeControl(session: ClientSession, inboxes: List<String>) {
val bridgeNotifyQueue = "$BRIDGE_NOTIFY.${myIdentity.toStringShort()}" val bridgeNotifyQueue = "$BRIDGE_NOTIFY.${myIdentity.toStringShort()}"
if (!session.queueQuery(SimpleString(bridgeNotifyQueue)).isExists) { if (!session.queueQuery(SimpleString(bridgeNotifyQueue)).isExists) {
session.createTemporaryQueue(BRIDGE_NOTIFY, RoutingType.MULTICAST, bridgeNotifyQueue) session.createQueue(QueueConfiguration(bridgeNotifyQueue).setAddress(BRIDGE_NOTIFY).setRoutingType(RoutingType.MULTICAST)
.setTemporary(true).setDurable(false))
} }
val bridgeConsumer = session.createConsumer(bridgeNotifyQueue) val bridgeConsumer = session.createConsumer(bridgeNotifyQueue)
bridgeNotifyConsumer = bridgeConsumer bridgeNotifyConsumer = bridgeConsumer
@ -265,8 +313,8 @@ class P2PMessagingClient(val config: NodeConfiguration,
log.info("Updating bridges on network map change: ${change::class.simpleName} ${change.node}") log.info("Updating bridges on network map change: ${change::class.simpleName} ${change.node}")
fun gatherAddresses(node: NodeInfo): Sequence<BridgeEntry> { fun gatherAddresses(node: NodeInfo): Sequence<BridgeEntry> {
return state.locked { return state.locked {
node.legalIdentitiesAndCerts.map { node.legalIdentitiesAndCerts.map { partyAndCertificate ->
val messagingAddress = NodeAddress(it.party.owningKey) val messagingAddress = NodeAddress(partyAndCertificate.party.owningKey)
BridgeEntry(messagingAddress.queueName, node.addresses, node.legalIdentities.map { it.name }, serviceAddress = false) BridgeEntry(messagingAddress.queueName, node.addresses, node.legalIdentities.map { it.name }, serviceAddress = false)
}.filter { producerSession!!.queueQuery(SimpleString(it.queueName)).isExists }.asSequence() }.filter { producerSession!!.queueQuery(SimpleString(it.queueName)).isExists }.asSequence()
} }
@ -404,12 +452,15 @@ class P2PMessagingClient(val config: NodeConfiguration,
internal fun deliver(artemisMessage: ClientMessage) { internal fun deliver(artemisMessage: ClientMessage) {
artemisToCordaMessage(artemisMessage)?.let { cordaMessage -> artemisToCordaMessage(artemisMessage)?.let { cordaMessage ->
if (!deduplicator.isDuplicate(cordaMessage)) { val outcome = deduplicator.checkDuplicate(cordaMessage)
if (outcome == P2PMessageDeduplicator.Outcome.NEW) {
deduplicator.signalMessageProcessStart(cordaMessage) deduplicator.signalMessageProcessStart(cordaMessage)
deliver(cordaMessage, artemisMessage) deliver(cordaMessage, artemisMessage)
} else { } else if (outcome == P2PMessageDeduplicator.Outcome.DUPLICATE) {
log.trace { "Discard duplicate message ${cordaMessage.uniqueMessageId} for ${cordaMessage.topic}" } log.debug { "Acknowledge duplicate message id: ${cordaMessage.uniqueMessageId} senderUUID: ${cordaMessage.senderUUID} senderSeqNo: ${cordaMessage.senderSeqNo} isSessionInit: ${cordaMessage.isSessionInit}" }
messagingExecutor!!.acknowledge(artemisMessage) messagingExecutor!!.acknowledge(artemisMessage)
} else {
log.debug { "Discard in-flight message id: ${cordaMessage.uniqueMessageId} senderUUID: ${cordaMessage.senderUUID} senderSeqNo: ${cordaMessage.senderSeqNo} isSessionInit: ${cordaMessage.isSessionInit}" }
} }
} }
} }
@ -463,8 +514,8 @@ class P2PMessagingClient(val config: NodeConfiguration,
running = false running = false
stateHelper.active = false stateHelper.active = false
networkChangeSubscription?.unsubscribe() networkChangeSubscription?.unsubscribe()
require(p2pConsumer != null, { "stop can't be called twice" }) require(p2pConsumer != null) { "stop can't be called twice" }
require(producer != null, { "stop can't be called twice" }) require(producer != null) { "stop can't be called twice" }
close(p2pConsumer) close(p2pConsumer)
p2pConsumer = null p2pConsumer = null
@ -524,7 +575,7 @@ class P2PMessagingClient(val config: NodeConfiguration,
// If we are sending to ourselves then route the message directly to our P2P queue. // If we are sending to ourselves then route the message directly to our P2P queue.
RemoteInboxAddress(myIdentity).queueName RemoteInboxAddress(myIdentity).queueName
} else { } else {
// Otherwise we send the message to an internal queue for the target residing on our broker. It's then the // Otherwise, we send the message to an internal queue for the target residing on our broker. It's then the
// broker's job to route the message to the target's P2P queue. // broker's job to route the message to the target's P2P queue.
val internalTargetQueue = (address as? ArtemisAddress)?.queueName val internalTargetQueue = (address as? ArtemisAddress)?.queueName
?: throw IllegalArgumentException("Not an Artemis address") ?: throw IllegalArgumentException("Not an Artemis address")
@ -556,9 +607,13 @@ class P2PMessagingClient(val config: NodeConfiguration,
val queueQuery = session.queueQuery(SimpleString(queueName)) val queueQuery = session.queueQuery(SimpleString(queueName))
if (!queueQuery.isExists) { if (!queueQuery.isExists) {
log.info("Create fresh queue $queueName bound on same address") log.info("Create fresh queue $queueName bound on same address")
session.createQueue(queueName, RoutingType.ANYCAST, queueName, null, true, false, session.createQueue(QueueConfiguration(queueName).setRoutingType(RoutingType.ANYCAST).setAddress(queueName)
ActiveMQDefaultConfiguration.getDefaultMaxQueueConsumers(), .setDurable(true).setAutoCreated(false)
ActiveMQDefaultConfiguration.getDefaultPurgeOnNoConsumers(), exclusive, null) .setMaxConsumers(ActiveMQDefaultConfiguration.getDefaultMaxQueueConsumers())
.setPurgeOnNoConsumers(ActiveMQDefaultConfiguration.getDefaultPurgeOnNoConsumers())
.setExclusive(exclusive)
.setLastValue(null)
)
sendBridgeCreateMessage() sendBridgeCreateMessage()
} }
} }
@ -567,7 +622,7 @@ class P2PMessagingClient(val config: NodeConfiguration,
} }
override fun addMessageHandler(topic: String, callback: MessageHandler): MessageHandlerRegistration { override fun addMessageHandler(topic: String, callback: MessageHandler): MessageHandlerRegistration {
require(!topic.isBlank()) { "Topic must not be blank, as the empty topic is a special case." } require(topic.isNotBlank()) { "Topic must not be blank, as the empty topic is a special case." }
handlers.compute(topic) { _, handler -> handlers.compute(topic) { _, handler ->
if (handler != null) { if (handler != null) {
throw IllegalStateException("Cannot add another acking handler for $topic, there is already an acking one") throw IllegalStateException("Cannot add another acking handler for $topic, there is already an acking one")

View File

@ -17,6 +17,7 @@ import net.corda.core.node.services.NetworkMapCache.MapChange
import net.corda.core.node.services.PartyInfo import net.corda.core.node.services.PartyInfo
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.utilities.MAX_HASH_HEX_SIZE
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
@ -34,6 +35,9 @@ import java.security.PublicKey
import java.security.cert.CertPathValidatorException import java.security.cert.CertPathValidatorException
import java.util.* import java.util.*
import javax.annotation.concurrent.ThreadSafe import javax.annotation.concurrent.ThreadSafe
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.Id
import javax.persistence.PersistenceException import javax.persistence.PersistenceException
/** Database-based network map cache. */ /** Database-based network map cache. */
@ -61,6 +65,18 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory,
@Volatile @Volatile
private lateinit var rotatedNotaries: Set<CordaX500Name> private lateinit var rotatedNotaries: Set<CordaX500Name>
@Entity
@javax.persistence.Table(name = "node_named_identities")
data class PersistentPartyToPublicKeyHash(
@Id
@Suppress("MagicNumber") // database column width
@Column(name = "name", length = 128, nullable = false)
var name: String = "",
@Column(name = "pk_hash", length = MAX_HASH_HEX_SIZE, nullable = true)
var publicKeyHash: String? = ""
)
// Notary whitelist may contain multiple identities with the same X.500 name after certificate rotation. // Notary whitelist may contain multiple identities with the same X.500 name after certificate rotation.
// Exclude duplicated entries, which are not present in the network map. // Exclude duplicated entries, which are not present in the network map.
override val notaryIdentities: List<Party> get() = notaries.map { it.identity } override val notaryIdentities: List<Party> get() = notaries.map { it.identity }
@ -294,6 +310,7 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory,
synchronized(_changed) { synchronized(_changed) {
database.transaction { database.transaction {
removeInfoDB(session, node) removeInfoDB(session, node)
archiveNamedIdentity(node)
changePublisher.onNext(MapChange.Removed(node)) changePublisher.onNext(MapChange.Removed(node))
} }
} }
@ -302,6 +319,12 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory,
logger.debug { "Done removing node with info: $node" } logger.debug { "Done removing node with info: $node" }
} }
private fun archiveNamedIdentity(nodeInfo: NodeInfo) {
nodeInfo.legalIdentities.forEach { party ->
identityService.archiveNamedIdentity(party.name.toString(), party.owningKey.toStringShort())
}
}
override val allNodes: List<NodeInfo> override val allNodes: List<NodeInfo>
get() { get() {
return database.transaction { return database.transaction {
@ -428,7 +451,10 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory,
database.transaction { database.transaction {
val result = getAllNodeInfos(session) val result = getAllNodeInfos(session)
logger.debug { "Number of node infos to be cleared: ${result.size}" } logger.debug { "Number of node infos to be cleared: ${result.size}" }
for (nodeInfo in result) session.remove(nodeInfo) for (nodeInfo in result) {
session.remove(nodeInfo)
archiveNamedIdentity(nodeInfo.toNodeInfo())
}
} }
} }

View File

@ -101,7 +101,9 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory:
// Rough estimate for the average of a public key and the transaction metadata - hard to get exact figures here, // Rough estimate for the average of a public key and the transaction metadata - hard to get exact figures here,
// as public keys can vary in size a lot, and if someone else is holding a reference to the key, it won't add // as public keys can vary in size a lot, and if someone else is holding a reference to the key, it won't add
// to the memory pressure at all here. // to the memory pressure at all here.
private const val transactionSignatureOverheadEstimate = 1024 private const val TRANSACTION_SIGNATURE_OVERHEAD_BYTES = 1024
private const val TXCACHEVALUE_OVERHEAD_BYTES = 80
private const val SECUREHASH_OVERHEAD_BYTES = 24
private val logger = contextLogger() private val logger = contextLogger()
@ -134,13 +136,13 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory:
) )
}, },
persistentEntityClass = DBTransaction::class.java, persistentEntityClass = DBTransaction::class.java,
weighingFunc = { hash, tx -> hash.size + weighTx(tx) } weighingFunc = { hash, tx -> SECUREHASH_OVERHEAD_BYTES + hash.size + weighTx(tx) }
) )
} }
private fun weighTx(tx: AppendOnlyPersistentMapBase.Transactional<TxCacheValue>): Int { private fun weighTx(actTx: TxCacheValue?): Int {
val actTx = tx.peekableValue ?: return 0 if (actTx == null) return 0
return actTx.sigs.sumBy { it.size + transactionSignatureOverheadEstimate } + actTx.txBits.size return TXCACHEVALUE_OVERHEAD_BYTES + actTx.sigs.sumBy { it.size + TRANSACTION_SIGNATURE_OVERHEAD_BYTES } + actTx.txBits.size
} }
private val log = contextLogger() private val log = contextLogger()

View File

@ -88,6 +88,9 @@ class NodeAttachmentService @JvmOverloads constructor(
while (true) { while (true) {
val cursor = jar.nextJarEntry ?: break val cursor = jar.nextJarEntry ?: break
// Security check to stop directory traversal from filename entry
require(!(cursor.name.contains("../"))) { "Bad character in ${cursor.name}" }
require(!(cursor.name.contains("..\\"))) { "Bad character in ${cursor.name}" }
if (manifestHasEntries && !allManifestEntries!!.remove(cursor.name)) extraFilesNotFoundInEntries.add(cursor) if (manifestHasEntries && !allManifestEntries!!.remove(cursor.name)) extraFilesNotFoundInEntries.add(cursor)
val entryPath = Paths.get(cursor.name) val entryPath = Paths.get(cursor.name)
// Security check to stop zips trying to escape their rightful place. // Security check to stop zips trying to escape their rightful place.

View File

@ -8,6 +8,7 @@ import net.corda.node.internal.artemis.*
import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.NODE_SECURITY_CONFIG import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.NODE_SECURITY_CONFIG
import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.RPC_SECURITY_CONFIG import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.RPC_SECURITY_CONFIG
import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.internal.security.RPCSecurityManager
import net.corda.node.utilities.artemis.startSynchronously
import net.corda.nodeapi.BrokerRpcSslOptions import net.corda.nodeapi.BrokerRpcSslOptions
import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.config.MutualSslConfiguration
import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
@ -51,20 +52,19 @@ class ArtemisRpcBroker internal constructor(
} }
} }
@Suppress("TooGenericExceptionCaught")
override fun start() { override fun start() {
logger.debug { "Artemis RPC broker is starting for: $addresses" } logger.debug { "Artemis RPC broker is starting for: $addresses" }
try { try {
server.start() server.startSynchronously()
} catch (e: IOException) { } catch (e: Throwable) {
logger.error("Unable to start message broker", e) logger.error("Unable to start message broker", e)
if (e.isBindingError()) { if (e.isBindingError()) {
throw AddressBindingException(adminAddressOptional?.let { setOf(it, addresses.primary) } ?: setOf(addresses.primary)) throw AddressBindingException(adminAddressOptional?.let { setOf(it, addresses.primary) } ?: setOf(addresses.primary))
} else { } else {
logger.error("Unexpected error starting message broker", e)
throw e throw e
} }
} catch (th: Throwable) {
logger.error("Unexpected error starting message broker", th)
throw th
} }
logger.debug("Artemis RPC broker is started.") logger.debug("Artemis RPC broker is started.")
} }
@ -90,7 +90,6 @@ class ArtemisRpcBroker internal constructor(
val serverSecurityManager = createArtemisSecurityManager(serverConfiguration.loginListener) val serverSecurityManager = createArtemisSecurityManager(serverConfiguration.loginListener)
return ActiveMQServerImpl(serverConfiguration, serverSecurityManager).apply { return ActiveMQServerImpl(serverConfiguration, serverSecurityManager).apply {
registerActivationFailureListener { exception -> throw exception }
registerPostQueueDeletionCallback { address, qName -> logger.debug("Queue deleted: $qName for $address") } registerPostQueueDeletionCallback { address, qName -> logger.debug("Queue deleted: $qName for $address") }
} }
} }

View File

@ -12,8 +12,8 @@ import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcAcceptorTcpTr
import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcInternalAcceptorTcpTransport import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcInternalAcceptorTcpTransport
import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.config.MutualSslConfiguration
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration
import org.apache.activemq.artemis.api.core.QueueConfiguration
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.core.config.CoreQueueConfiguration
import org.apache.activemq.artemis.core.security.Role import org.apache.activemq.artemis.core.security.Role
import org.apache.activemq.artemis.core.settings.impl.AddressFullMessagePolicy import org.apache.activemq.artemis.core.settings.impl.AddressFullMessagePolicy
import org.apache.activemq.artemis.core.settings.impl.AddressSettings import org.apache.activemq.artemis.core.settings.impl.AddressSettings
@ -37,14 +37,14 @@ internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int,
} }
acceptorConfigurations = acceptorConfigurationsSet acceptorConfigurations = acceptorConfigurationsSet
queueConfigurations = queueConfigurations() queueConfigs = queueConfigurations()
managementNotificationAddress = SimpleString(ArtemisMessagingComponent.NOTIFICATIONS_ADDRESS) managementNotificationAddress = SimpleString(ArtemisMessagingComponent.NOTIFICATIONS_ADDRESS)
addressesSettings = mapOf( addressesSettings = mapOf(
"${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.#" to AddressSettings().apply { "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.#" to AddressSettings().apply {
maxSizeBytes = 5L * maxMessageSize maxSizeBytes = 5L * maxMessageSize
addressFullMessagePolicy = AddressFullMessagePolicy.PAGE addressFullMessagePolicy = AddressFullMessagePolicy.PAGE
pageSizeBytes = 1L * maxMessageSize pageSizeBytes = maxMessageSize
} }
) )
@ -76,7 +76,11 @@ internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int,
securityRoles["${ArtemisMessagingComponent.INTERNAL_PREFIX}#"] = setOf(nodeInternalRole) securityRoles["${ArtemisMessagingComponent.INTERNAL_PREFIX}#"] = setOf(nodeInternalRole)
securityRoles[RPCApi.RPC_SERVER_QUEUE_NAME] = setOf(nodeInternalRole, restrictedRole(BrokerJaasLoginModule.RPC_ROLE, send = true)) securityRoles[RPCApi.RPC_SERVER_QUEUE_NAME] = setOf(nodeInternalRole, restrictedRole(BrokerJaasLoginModule.RPC_ROLE, send = true))
securitySettingPlugins.add(rolesAdderOnLogin) securitySettingPlugins.add(rolesAdderOnLogin)
securityInvalidationInterval = ArtemisMessagingComponent.SECURITY_INVALIDATION_INTERVAL
// Effectively disable security cache as permissions might change dynamically when e.g. DB is updated
authenticationCacheSize = 0
authorizationCacheSize = 0
securityInvalidationInterval = 0
} }
private fun enableJmx() { private fun enableJmx() {
@ -85,19 +89,19 @@ internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int,
} }
private fun initialiseSettings(maxMessageSize: Int, journalBufferTimeout: Int?) { private fun initialiseSettings(maxMessageSize: Int, journalBufferTimeout: Int?) {
// Enable built in message deduplication. Note we still have to do our own as the delayed commits // Enable built-in message deduplication. Note we still have to do our own as the delayed commits
// and our own definition of commit mean that the built in deduplication cannot remove all duplicates. // and our own definition of commit means that the built-in deduplication cannot remove all the duplicates.
idCacheSize = 2000 // Artemis Default duplicate cache size i.e. a guess idCacheSize = 2000 // Artemis Default duplicate cache size i.e. a guess
isPersistIDCache = true isPersistIDCache = true
isPopulateValidatedUser = true isPopulateValidatedUser = true
journalBufferSize_NIO = maxMessageSize // Artemis default is 490KiB - required to address IllegalArgumentException (when Artemis uses Java NIO): Record is too large to store. journalBufferSize_NIO = maxMessageSize // Artemis default is 490 KB - required to address IllegalArgumentException (when Artemis uses Java NIO): Record is too large to store.
journalBufferTimeout_NIO = journalBufferTimeout ?: ActiveMQDefaultConfiguration.getDefaultJournalBufferTimeoutNio() journalBufferTimeout_NIO = journalBufferTimeout ?: ActiveMQDefaultConfiguration.getDefaultJournalBufferTimeoutNio()
journalBufferSize_AIO = maxMessageSize // Required to address IllegalArgumentException (when Artemis uses Linux Async IO): Record is too large to store. journalBufferSize_AIO = maxMessageSize // Required to address IllegalArgumentException (when Artemis uses Linux Async IO): Record is too large to store.
journalBufferTimeout_AIO = journalBufferTimeout ?: ActiveMQDefaultConfiguration.getDefaultJournalBufferTimeoutAio() journalBufferTimeout_AIO = journalBufferTimeout ?: ActiveMQDefaultConfiguration.getDefaultJournalBufferTimeoutAio()
journalFileSize = maxMessageSize // The size of each journal file in bytes. Artemis default is 10MiB. journalFileSize = maxMessageSize // The size of each journal file in bytes. Artemis default is 10 MB.
} }
private fun queueConfigurations(): List<CoreQueueConfiguration> { private fun queueConfigurations(): List<QueueConfiguration> {
return listOf( return listOf(
queueConfiguration(RPCApi.RPC_SERVER_QUEUE_NAME, durable = false), queueConfiguration(RPCApi.RPC_SERVER_QUEUE_NAME, durable = false),
queueConfiguration( queueConfiguration(
@ -122,15 +126,8 @@ internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int,
pagingDirectory = (baseDirectory / "paging").toString() pagingDirectory = (baseDirectory / "paging").toString()
} }
private fun queueConfiguration(name: String, address: String = name, filter: String? = null, durable: Boolean): CoreQueueConfiguration { private fun queueConfiguration(name: String, address: String = name, filter: String? = null, durable: Boolean): QueueConfiguration {
val configuration = CoreQueueConfiguration() return QueueConfiguration(name).setAddress(address).setFilterString(filter).setDurable(durable)
configuration.name = name
configuration.address = address
configuration.filterString = filter
configuration.isDurable = durable
return configuration
} }
private fun restrictedRole(name: String, send: Boolean = false, consume: Boolean = false, createDurableQueue: Boolean = false, private fun restrictedRole(name: String, send: Boolean = false, consume: Boolean = false, createDurableQueue: Boolean = false,

View File

@ -14,6 +14,7 @@ import net.corda.node.services.events.NodeSchedulerService
import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.identity.PersistentIdentityService
import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.node.services.keys.BasicHSMKeyManagementService
import net.corda.node.services.messaging.P2PMessageDeduplicator import net.corda.node.services.messaging.P2PMessageDeduplicator
import net.corda.node.services.network.PersistentNetworkMapCache
import net.corda.node.services.persistence.DBCheckpointStorage import net.corda.node.services.persistence.DBCheckpointStorage
import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.DBTransactionStorage
import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.persistence.NodeAttachmentService
@ -49,7 +50,8 @@ class NodeSchemaService(private val extraSchemas: Set<MappedSchema> = emptySet()
PersistentIdentityService.PersistentHashToPublicKey::class.java, PersistentIdentityService.PersistentHashToPublicKey::class.java,
ContractUpgradeServiceImpl.DBContractUpgrade::class.java, ContractUpgradeServiceImpl.DBContractUpgrade::class.java,
DBNetworkParametersStorage.PersistentNetworkParameters::class.java, DBNetworkParametersStorage.PersistentNetworkParameters::class.java,
PublicKeyHashToExternalId::class.java PublicKeyHashToExternalId::class.java,
PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java
)) { )) {
override val migrationResource = "node-core.changelog-master" override val migrationResource = "node-core.changelog-master"
} }

View File

@ -11,6 +11,7 @@ import net.corda.core.flows.StateMachineRunId
import net.corda.core.flows.UnexpectedFlowEndException import net.corda.core.flows.UnexpectedFlowEndException
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.DeclaredField import net.corda.core.internal.DeclaredField
import net.corda.core.internal.ResolveTransactionsFlow
import net.corda.core.internal.ThreadBox import net.corda.core.internal.ThreadBox
import net.corda.core.internal.TimedFlow import net.corda.core.internal.TimedFlow
import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.VisibleForTesting
@ -21,6 +22,7 @@ import net.corda.core.utilities.debug
import net.corda.core.utilities.minutes import net.corda.core.utilities.minutes
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import net.corda.node.services.FinalityHandler import net.corda.node.services.FinalityHandler
import net.corda.node.services.statemachine.transitions.StartedFlowTransition
import org.hibernate.exception.ConstraintViolationException import org.hibernate.exception.ConstraintViolationException
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import java.io.Closeable import java.io.Closeable
@ -29,10 +31,9 @@ import java.sql.SQLTransientConnectionException
import java.time.Clock import java.time.Clock
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
import java.util.* import java.util.Timer
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import javax.persistence.PersistenceException import javax.persistence.PersistenceException
import kotlin.collections.HashMap
import kotlin.concurrent.timerTask import kotlin.concurrent.timerTask
import kotlin.math.pow import kotlin.math.pow
@ -103,17 +104,6 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging,
* Flows should be removed from [flowsInHospital] when they have completed a successful transition. * Flows should be removed from [flowsInHospital] when they have completed a successful transition.
*/ */
private val flowsInHospital = ConcurrentHashMap<StateMachineRunId, FlowFiber>() private val flowsInHospital = ConcurrentHashMap<StateMachineRunId, FlowFiber>()
/**
* Returns true if the flow is currently being treated in the hospital.
* The differs to flows with a medical history (which can accessed via [StaffedFlowHospital.contains]).
*/
@VisibleForTesting
internal fun flowInHospital(runId: StateMachineRunId): Boolean {
// The .keys avoids https://youtrack.jetbrains.com/issue/KT-18053
return runId in flowsInHospital.keys
}
private val mutex = ThreadBox(object { private val mutex = ThreadBox(object {
/** /**
* Contains medical history of every flow (a patient) that has entered the hospital. A flow can leave the hospital, * Contains medical history of every flow (a patient) that has entered the hospital. A flow can leave the hospital,
@ -347,7 +337,7 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging,
} }
} }
operator fun contains(flowId: StateMachineRunId) = mutex.locked { flowId in flowPatients } operator fun contains(flowId: StateMachineRunId) = flowId in flowsInHospital.keys
override fun close() { override fun close() {
hospitalJobTimer.cancel() hospitalJobTimer.cancel()
@ -485,14 +475,23 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging,
"the flow by re-starting the node. State machine state: $currentState", newError) "the flow by re-starting the node. State machine state: $currentState", newError)
Diagnosis.OVERNIGHT_OBSERVATION Diagnosis.OVERNIGHT_OBSERVATION
} else if (isFromReceiveFinalityFlow(newError)) { } else if (isFromReceiveFinalityFlow(newError)) {
if (isErrorPropagatedFromCounterparty(newError) && isErrorThrownDuringReceiveFinality(newError)) { when {
isErrorPropagatedFromCounterparty(newError) && isErrorThrownDuringReceiveTransactionFlow(newError) -> {
// no need to keep around the flow, since notarisation has already failed at the counterparty. // no need to keep around the flow, since notarisation has already failed at the counterparty.
Diagnosis.NOT_MY_SPECIALTY Diagnosis.NOT_MY_SPECIALTY
} else { }
log.warn("Flow ${flowFiber.id} failed to be finalised. Manual intervention may be required before retrying " + isEndSessionErrorThrownDuringReceiveTransactionFlow(newError) -> {
"the flow by re-starting the node. State machine state: $currentState", newError) // Typically occurs if the initiating flow catches a notary exception and ends their flow successfully.
Diagnosis.NOT_MY_SPECIALTY
}
else -> {
log.warn(
"Flow ${flowFiber.id} failed to be finalised. Manual intervention may be required before retrying " +
"the flow by re-starting the node. State machine state: $currentState", newError
)
Diagnosis.OVERNIGHT_OBSERVATION Diagnosis.OVERNIGHT_OBSERVATION
} }
}
} else { } else {
Diagnosis.NOT_MY_SPECIALTY Diagnosis.NOT_MY_SPECIALTY
} }
@ -523,13 +522,26 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging,
* This is because in the latter case, the transaction might have already been finalised and deleting the flow * This is because in the latter case, the transaction might have already been finalised and deleting the flow
* would introduce risk for inconsistency between nodes. * would introduce risk for inconsistency between nodes.
*/ */
private fun isErrorThrownDuringReceiveFinality(error: Throwable): Boolean { private fun isErrorThrownDuringReceiveTransactionFlow(error: Throwable): Boolean {
val strippedStacktrace = error.stackTrace val strippedStacktrace = error.stackTrace
.filterNot { it?.className?.contains("counter-flow exception from peer") ?: false } .filterNot { it?.className?.contains("counter-flow exception from peer") ?: false }
.filterNot { it?.className?.startsWith("net.corda.node.services.statemachine.") ?: false } .filterNot { it?.className?.startsWith("net.corda.node.services.statemachine.") ?: false }
return strippedStacktrace.isNotEmpty() return strippedStacktrace.isNotEmpty()
&& strippedStacktrace.first().className.startsWith(ReceiveTransactionFlow::class.qualifiedName!!) && strippedStacktrace.first().className.startsWith(ReceiveTransactionFlow::class.qualifiedName!!)
} }
/**
* Checks if an end session error exception was thrown and that it did so within [ReceiveTransactionFlow].
*
* The check for [ReceiveTransactionFlow] is important to ensure that the session didn't end within [ResolveTransactionsFlow] which
* implies that it has been receiving the transaction's dependencies and therefore ending before receiving the whole transaction
* is incorrect behaviour.
*/
private fun isEndSessionErrorThrownDuringReceiveTransactionFlow(error: Throwable): Boolean {
return error is UnexpectedFlowEndException
&& error.message?.contains(StartedFlowTransition.UNEXPECTED_SESSION_END_MESSAGE) == true
&& isErrorThrownDuringReceiveTransactionFlow(error)
}
} }
/** /**

View File

@ -28,6 +28,7 @@ class StartedFlowTransition(
companion object { companion object {
private val logger: Logger = contextLogger() private val logger: Logger = contextLogger()
const val UNEXPECTED_SESSION_END_MESSAGE = "Received session end message instead of a data session message. Mismatched send and receive?"
} }
override fun transition(): TransitionResult { override fun transition(): TransitionResult {
@ -253,7 +254,7 @@ class StartedFlowTransition(
newSessionMessages[sessionId] = sessionState.copy(receivedMessages = messages.subList(1, messages.size).toArrayList()) newSessionMessages[sessionId] = sessionState.copy(receivedMessages = messages.subList(1, messages.size).toArrayList())
// at this point, we've already checked for errors and session ends, so it's guaranteed that the first message will be a data message. // at this point, we've already checked for errors and session ends, so it's guaranteed that the first message will be a data message.
resultMessages[sessionId] = if (messages[0] is EndSessionMessage) { resultMessages[sessionId] = if (messages[0] is EndSessionMessage) {
throw UnexpectedFlowEndException("Received session end message instead of a data session message. Mismatched send and receive?") throw UnexpectedFlowEndException(UNEXPECTED_SESSION_END_MESSAGE)
} else { } else {
(messages[0] as DataSessionMessage).payload (messages[0] as DataSessionMessage).payload
} }

View File

@ -272,7 +272,7 @@ class HibernateAttachmentQueryCriteriaParser<T,R>(override val criteriaBuilder:
class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractState>, class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractState>,
val contractStateTypeMappings: Map<String, Set<String>>, val contractStateTypeMappings: Map<String, Set<String>>,
override val criteriaBuilder: CriteriaBuilder, override val criteriaBuilder: CriteriaBuilder,
val criteriaQuery: CriteriaQuery<Tuple>, val criteriaQuery: CriteriaQuery<*>,
val vaultStates: Root<VaultSchemaV1.VaultStates>) : AbstractQueryCriteriaParser<QueryCriteria, IQueryCriteriaParser, Sort>(), IQueryCriteriaParser { val vaultStates: Root<VaultSchemaV1.VaultStates>) : AbstractQueryCriteriaParser<QueryCriteria, IQueryCriteriaParser, Sort>(), IQueryCriteriaParser {
private companion object { private companion object {
private val log = contextLogger() private val log = contextLogger()

View File

@ -35,7 +35,6 @@ import net.corda.core.node.services.vault.PageSpecification
import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.node.services.vault.Sort import net.corda.core.node.services.vault.Sort
import net.corda.core.node.services.vault.SortAttribute import net.corda.core.node.services.vault.SortAttribute
import net.corda.core.node.services.vault.builder
import net.corda.core.observable.internal.OnResilientSubscribe import net.corda.core.observable.internal.OnResilientSubscribe
import net.corda.core.schemas.PersistentStateRef import net.corda.core.schemas.PersistentStateRef
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
@ -69,17 +68,21 @@ import java.security.PublicKey
import java.sql.SQLException import java.sql.SQLException
import java.time.Clock import java.time.Clock
import java.time.Instant import java.time.Instant
import java.util.Arrays import java.util.*
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.CopyOnWriteArraySet
import java.util.stream.Stream import java.util.stream.Stream
import javax.persistence.PersistenceException import javax.persistence.PersistenceException
import javax.persistence.Tuple import javax.persistence.Tuple
import javax.persistence.criteria.CriteriaBuilder import javax.persistence.criteria.CriteriaBuilder
import javax.persistence.criteria.CriteriaQuery
import javax.persistence.criteria.CriteriaUpdate import javax.persistence.criteria.CriteriaUpdate
import javax.persistence.criteria.Predicate import javax.persistence.criteria.Predicate
import javax.persistence.criteria.Root import javax.persistence.criteria.Root
import kotlin.collections.ArrayList
import kotlin.collections.LinkedHashSet
import kotlin.collections.component1
import kotlin.collections.component2
/** /**
* The vault service handles storage, retrieval and querying of states. * The vault service handles storage, retrieval and querying of states.
@ -706,7 +709,8 @@ class NodeVaultService(
paging: PageSpecification, paging: PageSpecification,
sorting: Sort, sorting: Sort,
contractStateType: Class<out T>): Vault.Page<T> { contractStateType: Class<out T>): Vault.Page<T> {
val (query, stateTypes) = createQuery(criteria, contractStateType, sorting) val (criteriaQuery, criteriaParser) = buildCriteriaQuery<Tuple>(criteria, contractStateType, sorting)
val query = getSession().createQuery(criteriaQuery)
query.setResultWindow(paging) query.setResultWindow(paging)
val statesMetadata: MutableList<Vault.StateMetadata> = mutableListOf() val statesMetadata: MutableList<Vault.StateMetadata> = mutableListOf()
@ -736,7 +740,7 @@ class NodeVaultService(
else -> queryTotalStateCount(criteria, contractStateType) else -> queryTotalStateCount(criteria, contractStateType)
} }
return Vault.Page(states, statesMetadata, totalStatesAvailable, stateTypes, otherResults) return Vault.Page(states, statesMetadata, totalStatesAvailable, criteriaParser.stateTypes, otherResults)
} }
private fun <R> Query<R>.resultStream(paging: PageSpecification): Stream<R> { private fun <R> Query<R>.resultStream(paging: PageSpecification): Stream<R> {
@ -765,19 +769,17 @@ class NodeVaultService(
} }
} }
private fun <T : ContractState> queryTotalStateCount(baseCriteria: QueryCriteria, contractStateType: Class<out T>): Long { private fun <T : ContractState> queryTotalStateCount(criteria: QueryCriteria, contractStateType: Class<out T>): Long {
val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } val (criteriaQuery, criteriaParser) = buildCriteriaQuery<Long>(criteria, contractStateType, null)
val countCriteria = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.ALL) criteriaQuery.select(criteriaBuilder.count(criteriaParser.vaultStates))
val criteria = baseCriteria.and(countCriteria) val query = getSession().createQuery(criteriaQuery)
val (query) = createQuery(criteria, contractStateType, null) return query.singleResult
val results = query.resultList
return results.last().toArray().last() as Long
} }
private fun <T : ContractState> createQuery(criteria: QueryCriteria, private inline fun <reified T> buildCriteriaQuery(criteria: QueryCriteria,
contractStateType: Class<out T>, contractStateType: Class<out ContractState>,
sorting: Sort?): Pair<Query<Tuple>, Vault.StateStatus> { sorting: Sort?): Pair<CriteriaQuery<T>, HibernateQueryCriteriaParser> {
val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java) val criteriaQuery = criteriaBuilder.createQuery(T::class.java)
val criteriaParser = HibernateQueryCriteriaParser( val criteriaParser = HibernateQueryCriteriaParser(
contractStateType, contractStateType,
contractStateTypeMappings, contractStateTypeMappings,
@ -786,8 +788,7 @@ class NodeVaultService(
criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) criteriaQuery.from(VaultSchemaV1.VaultStates::class.java)
) )
criteriaParser.parse(criteria, sorting) criteriaParser.parse(criteria, sorting)
val query = getSession().createQuery(criteriaQuery) return Pair(criteriaQuery, criteriaParser)
return Pair(query, criteriaParser.stateTypes)
} }
/** /**

View File

@ -32,8 +32,10 @@ abstract class AppendOnlyPersistentMapBase<K, V, E, out EK>(
private val log = contextLogger() private val log = contextLogger()
} }
protected class PendingKeyValue(val transactions: MutableSet<DatabaseTransaction>, val estimatedSize: Int)
protected abstract val cache: LoadingCache<K, Transactional<V>> protected abstract val cache: LoadingCache<K, Transactional<V>>
protected val pendingKeys = ConcurrentHashMap<K, MutableSet<DatabaseTransaction>>() protected val pendingKeys = ConcurrentHashMap<K, PendingKeyValue>()
/** /**
* Returns the value associated with the key, first loading that value from the storage if necessary. * Returns the value associated with the key, first loading that value from the storage if necessary.
@ -85,7 +87,8 @@ abstract class AppendOnlyPersistentMapBase<K, V, E, out EK>(
// for cases where the value passed to set differs from that in the cache, but an update function has decided that this // for cases where the value passed to set differs from that in the cache, but an update function has decided that this
// differing value should not be written to the database. // differing value should not be written to the database.
if (wasWritten) { if (wasWritten) {
Transactional.InFlight(this, key, _readerValueLoader = { loadValue(key) }).apply { alsoWrite(value) } Transactional.InFlight(this, key, weight = weight(key, value), _readerValueLoader = { loadValue(key) })
.apply { alsoWrite(value) }
} else { } else {
oldValueInCache oldValueInCache
} }
@ -120,7 +123,8 @@ abstract class AppendOnlyPersistentMapBase<K, V, E, out EK>(
Transactional.Committed(oldValue) Transactional.Committed(oldValue)
} else { } else {
// Some database transactions, including us, writing, with readers seeing whatever is in the database and writers seeing the (in memory) value. // Some database transactions, including us, writing, with readers seeing whatever is in the database and writers seeing the (in memory) value.
Transactional.InFlight(this, key, _readerValueLoader = { loadValue(key) }).apply { alsoWrite(value) } Transactional.InFlight(this, key, weight = weight(key, value), _readerValueLoader = { loadValue(key) })
.apply { alsoWrite(value) }
} }
} }
@ -214,11 +218,12 @@ abstract class AppendOnlyPersistentMapBase<K, V, E, out EK>(
protected fun transactionalLoadValue(key: K): Transactional<V> { protected fun transactionalLoadValue(key: K): Transactional<V> {
// This gets called if a value is read and the cache has no Transactional for this key yet. // This gets called if a value is read and the cache has no Transactional for this key yet.
return if (anyoneWriting(key)) { val estimatedSize = anyoneWriting(key)
return if (estimatedSize != -1) {
// If someone is writing (but not us) // If someone is writing (but not us)
// For those not writing, they need to re-load the value from the database (which their database transaction MIGHT see). // For those not writing, they need to re-load the value from the database (which their database transaction MIGHT see).
// For those writing, they need to re-load the value from the database (which their database transaction CAN see). // For those writing, they need to re-load the value from the database (which their database transaction CAN see).
Transactional.InFlight(this, key, { loadValue(key) }, { loadValue(key)!! }) Transactional.InFlight(this, key, estimatedSize, { loadValue(key) }, { loadValue(key)!! })
} else { } else {
// If no one is writing, then the value may or may not exist in the database. // If no one is writing, then the value may or may not exist in the database.
Transactional.Unknown(this, key) { loadValue(key) } Transactional.Unknown(this, key) { loadValue(key) }
@ -240,21 +245,24 @@ abstract class AppendOnlyPersistentMapBase<K, V, E, out EK>(
} }
// Helpers to know if transaction(s) are currently writing the given key. // Helpers to know if transaction(s) are currently writing the given key.
private fun weAreWriting(key: K): Boolean = pendingKeys[key]?.contains(contextTransaction) ?: false private fun weAreWriting(key: K): Boolean = pendingKeys[key]?.transactions?.contains(contextTransaction) ?: false
private fun anyoneWriting(key: K): Boolean = pendingKeys[key]?.isNotEmpty() ?: false private fun anyoneWriting(key: K): Int = pendingKeys[key]?.estimatedSize ?: -1
protected open fun weight(key: K, value: V): Int = 1
// Indicate this database transaction is a writer of this key. // Indicate this database transaction is a writer of this key.
private fun addPendingKey(key: K, databaseTransaction: DatabaseTransaction): Boolean { private fun addPendingKey(key: K, databaseTransaction: DatabaseTransaction, estimatedSize: Int): Boolean {
var added = true var added = true
pendingKeys.compute(key) { _, oldSet -> pendingKeys.compute(key) { _, value: PendingKeyValue? ->
val oldSet = value?.transactions
if (oldSet == null) { if (oldSet == null) {
val newSet = HashSet<DatabaseTransaction>(0) val newSet = HashSet<DatabaseTransaction>(0)
newSet += databaseTransaction newSet += databaseTransaction
newSet PendingKeyValue(newSet, estimatedSize)
} else { } else {
added = oldSet.add(databaseTransaction) added = oldSet.add(databaseTransaction)
oldSet value
} }
} }
return added return added
@ -262,12 +270,13 @@ abstract class AppendOnlyPersistentMapBase<K, V, E, out EK>(
// Remove this database transaction as a writer of this key, because the transaction committed or rolled back. // Remove this database transaction as a writer of this key, because the transaction committed or rolled back.
private fun removePendingKey(key: K, databaseTransaction: DatabaseTransaction) { private fun removePendingKey(key: K, databaseTransaction: DatabaseTransaction) {
pendingKeys.compute(key) { _, oldSet -> pendingKeys.compute(key) { _, value: PendingKeyValue? ->
val oldSet = value?.transactions
if (oldSet == null) { if (oldSet == null) {
oldSet null
} else { } else {
oldSet -= databaseTransaction oldSet -= databaseTransaction
if (oldSet.size == 0) null else oldSet if (oldSet.size == 0) null else value
} }
} }
} }
@ -278,10 +287,12 @@ abstract class AppendOnlyPersistentMapBase<K, V, E, out EK>(
* There are 3 states. Globally missing, globally visible, and being written in a transaction somewhere now or in * There are 3 states. Globally missing, globally visible, and being written in a transaction somewhere now or in
* the past (and it rolled back). * the past (and it rolled back).
*/ */
@Suppress("MagicNumber")
sealed class Transactional<T> { sealed class Transactional<T> {
abstract val value: T abstract val value: T
abstract val isPresent: Boolean abstract val isPresent: Boolean
abstract val peekableValue: T? abstract val peekableValue: T?
abstract val shallowSize: Int
fun orElse(alt: T?) = if (isPresent) value else alt fun orElse(alt: T?) = if (isPresent) value else alt
@ -291,6 +302,8 @@ abstract class AppendOnlyPersistentMapBase<K, V, E, out EK>(
get() = true get() = true
override val peekableValue: T? override val peekableValue: T?
get() = value get() = value
override val shallowSize: Int
get() = 48
} }
// No one can see it. // No one can see it.
@ -301,6 +314,8 @@ abstract class AppendOnlyPersistentMapBase<K, V, E, out EK>(
get() = false get() = false
override val peekableValue: T? override val peekableValue: T?
get() = null get() = null
override val shallowSize: Int
get() = 16
} }
// No one is writing, but we haven't looked in the database yet. This can only be when there are no writers. // No one is writing, but we haven't looked in the database yet. This can only be when there are no writers.
@ -323,12 +338,15 @@ abstract class AppendOnlyPersistentMapBase<K, V, E, out EK>(
} }
val isResolved: Boolean get() = valueWithoutIsolationDelegate.isInitialized() val isResolved: Boolean get() = valueWithoutIsolationDelegate.isInitialized()
override val peekableValue: T? get() = if (isResolved && isPresent) value else null override val peekableValue: T? get() = if (isResolved && isPresent) value else null
override val shallowSize: Int
get() = 128
} }
// Written in a transaction (uncommitted) somewhere, but there's a small window when this might be seen after commit, // Written in a transaction (uncommitted) somewhere, but there's a small window when this might be seen after commit,
// hence the committed flag. // hence the committed flag.
class InFlight<K, T>(private val map: AppendOnlyPersistentMapBase<K, T, *, *>, class InFlight<K, T>(private val map: AppendOnlyPersistentMapBase<K, T, *, *>,
private val key: K, private val key: K,
val weight: Int,
private val _readerValueLoader: () -> T?, private val _readerValueLoader: () -> T?,
private val _writerValueLoader: () -> T = { throw IllegalAccessException("No value loader provided") }) : Transactional<T>() { private val _writerValueLoader: () -> T = { throw IllegalAccessException("No value loader provided") }) : Transactional<T>() {
@ -352,7 +370,7 @@ abstract class AppendOnlyPersistentMapBase<K, V, E, out EK>(
val tx = contextTransaction val tx = contextTransaction
val strongKey = key val strongKey = key
val strongMap = map val strongMap = map
if (map.addPendingKey(key, tx)) { if (map.addPendingKey(key, tx, weight)) {
// If the transaction commits, update cache to make globally visible if we're first for this key, // If the transaction commits, update cache to make globally visible if we're first for this key,
// and then stop saying the transaction is writing the key. // and then stop saying the transaction is writing the key.
tx.onCommit { tx.onCommit {
@ -414,6 +432,9 @@ abstract class AppendOnlyPersistentMapBase<K, V, E, out EK>(
// The value from the perspective of the eviction algorithm of the cache. i.e. we want to reveal memory footprint to it etc. // The value from the perspective of the eviction algorithm of the cache. i.e. we want to reveal memory footprint to it etc.
override val peekableValue: T? override val peekableValue: T?
get() = if (writerValueLoader.get() != _writerValueLoader) writerValueLoader.get()() else if (readerValueLoader.get() != _readerValueLoader) readerValueLoader.get()() else null get() = if (writerValueLoader.get() != _writerValueLoader) writerValueLoader.get()() else if (readerValueLoader.get() != _readerValueLoader) readerValueLoader.get()() else null
override val shallowSize: Int
get() = 256
} }
} }
} }
@ -445,15 +466,24 @@ class WeightBasedAppendOnlyPersistentMap<K, V, E, out EK>(
fromPersistentEntity: (E) -> Pair<K, V>, fromPersistentEntity: (E) -> Pair<K, V>,
toPersistentEntity: (key: K, value: V) -> E, toPersistentEntity: (key: K, value: V) -> E,
persistentEntityClass: Class<E>, persistentEntityClass: Class<E>,
weighingFunc: (K, Transactional<V>) -> Int private val weighingFunc: (K, V?) -> Int
) : AppendOnlyPersistentMapBase<K, V, E, EK>( ) : AppendOnlyPersistentMapBase<K, V, E, EK>(
toPersistentEntityKey, toPersistentEntityKey,
fromPersistentEntity, fromPersistentEntity,
toPersistentEntity, toPersistentEntity,
persistentEntityClass) { persistentEntityClass) {
override fun weight(key: K, value: V): Int = weighingFunc(key, value)
override val cache = NonInvalidatingWeightBasedCache( override val cache = NonInvalidatingWeightBasedCache(
cacheFactory = cacheFactory, cacheFactory = cacheFactory,
name = name, name = name,
weigher = Weigher { key, value -> weighingFunc(key, value) }, weigher = Weigher { key, value: Transactional<V> ->
value.shallowSize + if (value is Transactional.InFlight<*, *>) {
value.weight * 2
} else {
weighingFunc(key, value.peekableValue)
}
},
loadFunction = { key: K -> transactionalLoadValue(key) }) loadFunction = { key: K -> transactionalLoadValue(key) })
} }

View File

@ -73,7 +73,7 @@ class InfrequentlyMutatedCache<K : Any, V : Any>(name: String, cacheFactory: Nam
backingCache.invalidateAll() backingCache.invalidateAll()
} }
private fun invalidate(key: K, value: Wrapper.Invalidated<V>): Wrapper.Invalidated<V> { private fun invalidate(key: K, value: Wrapper.Invalidated<V>): Wrapper.Invalidated<V>? {
val tx = contextTransactionOrNull val tx = contextTransactionOrNull
value.invalidators.incrementAndGet() value.invalidators.incrementAndGet()
currentlyInvalid[key] = value currentlyInvalid[key] = value
@ -81,7 +81,10 @@ class InfrequentlyMutatedCache<K : Any, V : Any>(name: String, cacheFactory: Nam
// When we close, we can't start using caching again until all simultaneously open transactions are closed. // When we close, we can't start using caching again until all simultaneously open transactions are closed.
tx.onClose { tx.database.onAllOpenTransactionsClosed { decrementInvalidators(key, value) } } tx.onClose { tx.database.onAllOpenTransactionsClosed { decrementInvalidators(key, value) } }
} else { } else {
decrementInvalidators(key, value) if (value.invalidators.decrementAndGet() == 0) {
currentlyInvalid.remove(key)
return null
}
} }
return value return value
} }

View File

@ -0,0 +1,22 @@
package net.corda.node.utilities.artemis
import net.corda.core.utilities.getOrThrow
import org.apache.activemq.artemis.core.server.ActivateCallback
import org.apache.activemq.artemis.core.server.ActiveMQServer
import java.util.concurrent.CompletableFuture
fun ActiveMQServer.startSynchronously() {
val startupFuture = CompletableFuture<Unit>()
registerActivateCallback(object: ActivateCallback {
override fun activationComplete() {
startupFuture.complete(Unit)
}
})
registerActivationFailureListener {
startupFuture.completeExceptionally(it)
}
start()
startupFuture.getOrThrow()
}

View File

@ -0,0 +1 @@
net.corda.node.services.messaging.NodeOpenSSLContextFactory

View File

@ -0,0 +1 @@
net.corda.node.services.messaging.NodeSSLContextFactory

View File

@ -27,6 +27,7 @@
<include file="migration/node-core.changelog-v14-data.xml"/> <include file="migration/node-core.changelog-v14-data.xml"/>
<include file="migration/node-core.changelog-v16.xml"/> <include file="migration/node-core.changelog-v16.xml"/>
<include file="migration/node-core.changelog-v20.xml"/> <include file="migration/node-core.changelog-v20.xml"/>
<include file="migration/node-core.changelog-v22.xml"/>
<!-- This must run after node-core.changelog-init.xml, to prevent database columns being created twice. --> <!-- This must run after node-core.changelog-init.xml, to prevent database columns being created twice. -->
<include file="migration/vault-schema.changelog-v9.xml"/> <include file="migration/vault-schema.changelog-v9.xml"/>

View File

@ -0,0 +1,13 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
<changeSet author="R3.Corda" id="add_node_named_identities_table_back_in">
<createTable tableName="node_named_identities">
<column name="name" type="NVARCHAR(128)">
<constraints nullable="false"/>
</column>
<column name="pk_hash" type="NVARCHAR(130)"/>
</createTable>
</changeSet>
</databaseChangeLog>

View File

@ -1,24 +1,34 @@
package net.corda.node.internal.artemis package net.corda.node.internal.artemis
import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.doThrow
import com.nhaarman.mockito_kotlin.whenever import com.nhaarman.mockito_kotlin.whenever
import net.corda.coretesting.internal.rigorousMock import net.corda.coretesting.internal.rigorousMock
import net.corda.nodeapi.internal.ArtemisMessagingComponent import net.corda.nodeapi.internal.ArtemisMessagingComponent
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME import net.corda.testing.core.BOB_NAME
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
import org.apache.activemq.artemis.api.core.Message
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.core.client.impl.ClientMessageImpl import org.apache.activemq.artemis.core.client.impl.ClientMessageImpl
import org.apache.activemq.artemis.core.server.ServerSession import org.apache.activemq.artemis.core.server.ServerSession
import org.apache.activemq.artemis.protocol.amqp.broker.AMQPMessage import org.apache.activemq.artemis.protocol.amqp.broker.AMQPMessage
import org.apache.activemq.artemis.protocol.amqp.converter.AMQPConverter import org.apache.activemq.artemis.protocol.amqp.broker.AMQPStandardMessage
import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions
import org.junit.Test import org.junit.Test
class UserValidationPluginTest { class UserValidationPluginTest {
private val plugin = UserValidationPlugin() private val plugin = UserValidationPlugin()
private val coreMessage = ClientMessageImpl(0, false, 0, System.currentTimeMillis(), 4.toByte(), 1024) private val coreMessage = ClientMessageImpl(0, false, 0, System.currentTimeMillis(),
private val amqpMessage get() = AMQPConverter.getInstance().fromCore(coreMessage) 4.toByte(), 1024)
private val amqpMessage: AMQPMessage
get() {
return rigorousMock<AMQPMessage>().also {
doReturn(coreMessage.validatedUserID).whenever(it).getStringProperty(Message.HDR_VALIDATED_USER)
}
}
private val session = rigorousMock<ServerSession>().also { private val session = rigorousMock<ServerSession>().also {
doReturn(ArtemisMessagingComponent.PEER_USER).whenever(it).username doReturn(ArtemisMessagingComponent.PEER_USER).whenever(it).username
doReturn(ALICE_NAME.toString()).whenever(it).validatedUser doReturn(ALICE_NAME.toString()).whenever(it).validatedUser
@ -31,16 +41,17 @@ class UserValidationPluginTest {
@Test(timeout = 300_000) @Test(timeout = 300_000)
fun `accept AMQP message with user`() { fun `accept AMQP message with user`() {
coreMessage.putStringProperty("_AMQ_VALIDATED_USER", ALICE_NAME.toString()) coreMessage.validatedUserID = ALICE_NAME.toString()
plugin.beforeSend(session, rigorousMock(), amqpMessage, direct = false, noAutoCreateQueue = false) plugin.beforeSend(session, rigorousMock(), amqpMessage, direct = false, noAutoCreateQueue = false)
} }
@Test(timeout = 300_000) @Test(timeout = 300_000)
fun `reject AMQP message with different user`() { fun `reject AMQP message with different user`() {
coreMessage.putStringProperty("_AMQ_VALIDATED_USER", BOB_NAME.toString()) coreMessage.validatedUserID = BOB_NAME.toString()
val localAmqpMessage = amqpMessage
Assertions.assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy { Assertions.assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
plugin.beforeSend(session, rigorousMock(), amqpMessage, direct = false, noAutoCreateQueue = false) plugin.beforeSend(session, rigorousMock(), localAmqpMessage, direct = false, noAutoCreateQueue = false)
}.withMessageContaining("_AMQ_VALIDATED_USER") }.withMessageContaining(Message.HDR_VALIDATED_USER.toString())
} }
@Test(timeout = 300_000) @Test(timeout = 300_000)
@ -49,7 +60,7 @@ class UserValidationPluginTest {
doReturn(ArtemisMessagingComponent.NODE_P2P_USER).whenever(it).username doReturn(ArtemisMessagingComponent.NODE_P2P_USER).whenever(it).username
doReturn(ALICE_NAME.toString()).whenever(it).validatedUser doReturn(ALICE_NAME.toString()).whenever(it).validatedUser
} }
coreMessage.putStringProperty("_AMQ_VALIDATED_USER", BOB_NAME.toString()) coreMessage.validatedUserID = BOB_NAME.toString()
plugin.beforeSend(internalSession, rigorousMock(), amqpMessage, direct = false, noAutoCreateQueue = false) plugin.beforeSend(internalSession, rigorousMock(), amqpMessage, direct = false, noAutoCreateQueue = false)
} }
@ -62,11 +73,8 @@ class UserValidationPluginTest {
@Test(timeout = 300_000) @Test(timeout = 300_000)
fun `reject message with exception`() { fun `reject message with exception`() {
coreMessage.putStringProperty("_AMQ_VALIDATED_USER", BOB_NAME.toString()) val messageWithException = rigorousMock<AMQPMessage>().also {
val messageWithException = object : AMQPMessage(0, amqpMessage.buffer.array(), null) { doThrow(IllegalStateException("My exception")).whenever(it).getStringProperty(any<SimpleString>())
override fun getStringProperty(key: SimpleString?): String {
throw IllegalStateException("My exception")
}
} }
// Artemis swallows all exceptions except ActiveMQException, so making sure that proper exception is thrown // Artemis swallows all exceptions except ActiveMQException, so making sure that proper exception is thrown
Assertions.assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy { Assertions.assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
@ -76,9 +84,8 @@ class UserValidationPluginTest {
@Test(timeout = 300_000) @Test(timeout = 300_000)
fun `reject message with security exception`() { fun `reject message with security exception`() {
coreMessage.putStringProperty("_AMQ_VALIDATED_USER", BOB_NAME.toString()) val messageWithException = object : AMQPStandardMessage(0, ByteArray(0), null) {
val messageWithException = object : AMQPMessage(0, amqpMessage.buffer.array(), null) { override fun getApplicationPropertiesMap(createIfAbsent: Boolean): MutableMap<String, Any> {
override fun getStringProperty(key: SimpleString?): String {
throw ActiveMQSecurityException("My security exception") throw ActiveMQSecurityException("My security exception")
} }
} }

View File

@ -50,7 +50,7 @@ object IdentityTestSchemaV1 : MappedSchema(
@Column(name = "name", length = 128, nullable = false) @Column(name = "name", length = 128, nullable = false)
var name: String = "", var name: String = "",
@Column(name = "pk_hash", length = MAX_HASH_HEX_SIZE, nullable = false) @Column(name = "pk_hash", length = MAX_HASH_HEX_SIZE, nullable = true)
var publicKeyHash: String = "" var publicKeyHash: String = ""
) )

View File

@ -10,10 +10,10 @@ import net.corda.core.internal.div
import net.corda.core.internal.toPath import net.corda.core.internal.toPath
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import net.corda.node.services.config.shell.SSHDConfiguration
import net.corda.nodeapi.internal.config.getBooleanCaseInsensitive import net.corda.nodeapi.internal.config.getBooleanCaseInsensitive
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import net.corda.tools.shell.SSHDConfiguration
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals

View File

@ -1,16 +1,22 @@
package net.corda.node.services.network package net.corda.node.services.network
import net.corda.core.crypto.toStringShort import net.corda.core.crypto.toStringShort
import net.corda.core.identity.CordaX500Name
import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkMapCache
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.CHARLIE_NAME
import net.corda.testing.core.singleIdentity import net.corda.testing.core.singleIdentity
import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.InternalMockNodeParameters import net.corda.testing.node.internal.InternalMockNodeParameters
import net.corda.testing.node.internal.TestStartedNode
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.After import org.junit.After
import org.junit.Assert
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import java.math.BigInteger import java.math.BigInteger
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -18,6 +24,7 @@ import kotlin.test.assertNotNull
import kotlin.test.assertNull import kotlin.test.assertNull
class NetworkMapCacheTest { class NetworkMapCacheTest {
private val TestStartedNode.party get() = info.legalIdentities.first()
private val mockNet = InternalMockNetwork() private val mockNet = InternalMockNetwork()
@After @After
@ -25,6 +32,153 @@ class NetworkMapCacheTest {
mockNet.stopNodes() mockNet.stopNodes()
} }
@Test(timeout=300_000)
fun `unknown Party object gets recorded as null entry in node_named_identities table`() {
val bobNode = mockNet.createPartyNode(BOB_NAME)
assertEquals(null, bobNode.services.identityService.wellKnownPartyFromX500Name(CHARLIE_NAME))
bobNode.database.transaction {
val cb = session.criteriaBuilder
val query = cb.createQuery(PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java)
val root = query.from(PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java)
val matchPublicKey = cb.isNull(root.get<String>("publicKeyHash"))
val matchName = cb.equal(root.get<String>("name"), CHARLIE_NAME.toString())
query.select(root).where(cb.and(matchName, matchPublicKey))
val resultList = session.createQuery(query).resultList
assertEquals(1, resultList.size)
}
}
@Test(timeout=300_000)
fun `check Party object can still be retrieved when not in node_named_identities table`() {
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
val bobNode = mockNet.createPartyNode(BOB_NAME)
val bobCache: NetworkMapCache = bobNode.services.networkMapCache
val bobCacheInternal = bobCache as NetworkMapCacheInternal
assertNotNull(bobCacheInternal)
bobCache.removeNode(aliceNode.info)
val alicePubKeyHash = aliceNode.info.legalIdentities[0].owningKey.toStringShort()
// Remove node adds an entry to the PersistentPartyToPublicKeyHash, so for this test delete this entry.
removeNodeFromNodeNamedIdentitiesTable(bobNode, alicePubKeyHash)
assertEquals(aliceNode.party, bobNode.services.identityService.wellKnownPartyFromX500Name(ALICE_NAME))
assertEquals(1, queryNodeNamedIdentities(bobNode, ALICE_NAME, alicePubKeyHash).size)
}
private fun removeNodeFromNodeNamedIdentitiesTable(node: TestStartedNode, publicKeyHashToRemove: String) {
// Remove node adds an entry to the PersistentPartyToPublicKeyHash, so for this test delete this entry.
node.database.transaction {
val deleteQuery = session.criteriaBuilder.createCriteriaDelete(PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java)
val queryRoot = deleteQuery.from(PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java)
deleteQuery.where(session.criteriaBuilder.equal(queryRoot.get<String>("publicKeyHash"), publicKeyHashToRemove))
session.createQuery(deleteQuery).executeUpdate()
}
}
private fun queryNodeNamedIdentities(node: TestStartedNode, party: CordaX500Name, publicKeyHash: String): List<PersistentNetworkMapCache.PersistentPartyToPublicKeyHash> {
return node.database.transaction {
val cb = session.criteriaBuilder
val query = cb.createQuery(PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java)
val root = query.from(PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java)
val matchPublicKeyHash = cb.equal(root.get<String>("publicKeyHash"), publicKeyHash)
val matchName = cb.equal(root.get<String>("name"), party.toString())
query.select(root).where(cb.and(matchName, matchPublicKeyHash))
session.createQuery(query).resultList
}
}
@Test(timeout=300_000)
fun `check removed node is inserted into node_name_identities table and then its Party object can be retrieved`() {
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
val bobNode = mockNet.createPartyNode(BOB_NAME)
val bobCache: NetworkMapCache = bobNode.services.networkMapCache
val bobCacheInternal = bobCache as NetworkMapCacheInternal
assertNotNull(bobCacheInternal)
val aliceParty1 = bobNode.services.identityService.wellKnownPartyFromX500Name(ALICE_NAME)
println("alicePart1 = $aliceParty1")
bobCache.removeNode(aliceNode.info)
val alicePubKeyHash = aliceNode.info.legalIdentities[0].owningKey.toStringShort()
assertEquals(1, queryNodeNamedIdentities(bobNode, ALICE_NAME, alicePubKeyHash).size)
assertEquals(aliceNode.party, bobNode.services.identityService.wellKnownPartyFromX500Name(ALICE_NAME))
}
@Test(timeout=300_000)
fun `check two removed nodes are both archived and then both Party objects are retrievable`() {
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
val bobNode = mockNet.createPartyNode(BOB_NAME)
val charlieNode = mockNet.createPartyNode(CHARLIE_NAME)
val bobCache: NetworkMapCache = bobNode.services.networkMapCache
val bobCacheInternal = bobCache as NetworkMapCacheInternal
assertNotNull(bobCacheInternal)
bobCache.removeNode(aliceNode.info)
bobCache.removeNode(charlieNode.info)
val alicePubKeyHash = aliceNode.info.legalIdentities[0].owningKey.toStringShort()
val charliePubKeyHash = charlieNode.info.legalIdentities[0].owningKey.toStringShort()
assertEquals(1, queryNodeNamedIdentities(bobNode, ALICE_NAME, alicePubKeyHash).size)
assertEquals(1, queryNodeNamedIdentities(bobNode, CHARLIE_NAME, charliePubKeyHash).size)
assertEquals(aliceNode.party, bobNode.services.identityService.wellKnownPartyFromX500Name(ALICE_NAME))
assertEquals(charlieNode.party, bobNode.services.identityService.wellKnownPartyFromX500Name(CHARLIE_NAME))
}
@Test(timeout=300_000)
fun `check latest identity returned according to certificate after identity mock rotatated`() {
val aliceNode1 = mockNet.createPartyNode(ALICE_NAME)
val bobNode = mockNet.createPartyNode(BOB_NAME)
val bobCache: NetworkMapCache = bobNode.services.networkMapCache
val alicePubKeyHash1 = aliceNode1.info.legalIdentities[0].owningKey.toStringShort()
val bobCacheInternal = bobCache as NetworkMapCacheInternal
assertNotNull(bobCacheInternal)
bobCache.removeNode(aliceNode1.info)
// Remove node adds an entry to the PersistentPartyToPublicKeyHash, so for this test delete this entry.
removeNodeFromNodeNamedIdentitiesTable(bobNode, alicePubKeyHash1)
val aliceNode2 = mockNet.createPartyNode(ALICE_NAME)
val alicePubKeyHash2 = aliceNode2.info.legalIdentities[0].owningKey.toStringShort()
bobCache.removeNode(aliceNode2.info)
// Remove node adds an entry to the PersistentPartyToPublicKeyHash, so for this test delete this entry.
removeNodeFromNodeNamedIdentitiesTable(bobNode, alicePubKeyHash2)
val retrievedParty = bobNode.services.identityService.wellKnownPartyFromX500Name(ALICE_NAME)
// For both identity certificates the valid from date is the start of the day, so either could be returned.
assertTrue(aliceNode2.party == retrievedParty || aliceNode1.party == retrievedParty)
}
@Test(timeout=300_000)
fun `latest identity is archived after identity rotated`() {
var aliceNode = mockNet.createPartyNode(ALICE_NAME)
val bobNode = mockNet.createPartyNode(BOB_NAME)
val bobCache: NetworkMapCache = bobNode.services.networkMapCache
val bobCacheInternal = bobCache as NetworkMapCacheInternal
assertNotNull(bobCacheInternal)
bobCache.removeNode(aliceNode.info)
fun checkArchivedIdentity(bobNode: TestStartedNode, aliceNode: TestStartedNode) {
val alicePubKeyHash = aliceNode.info.legalIdentities[0].owningKey.toStringShort()
bobNode.database.transaction {
val hashToIdentityStatement = database.dataSource.connection.prepareStatement("SELECT name, pk_hash FROM node_named_identities WHERE pk_hash=?")
hashToIdentityStatement.setString(1, alicePubKeyHash)
val aliceResultSet = hashToIdentityStatement.executeQuery()
Assert.assertTrue(aliceResultSet.next())
Assert.assertEquals(ALICE_NAME.toString(), aliceResultSet.getString("name"))
Assert.assertEquals(alicePubKeyHash.toString(), aliceResultSet.getString("pk_hash"))
Assert.assertFalse(aliceResultSet.next())
}
}
checkArchivedIdentity(bobNode, aliceNode)
aliceNode.dispose()
aliceNode = mockNet.createPartyNode(ALICE_NAME)
bobCache.removeNode(aliceNode.info)
checkArchivedIdentity(bobNode, aliceNode)
}
@Test(timeout=300_000) @Test(timeout=300_000)
fun `key collision`() { fun `key collision`() {
val entropy = BigInteger.valueOf(24012017L) val entropy = BigInteger.valueOf(24012017L)

View File

@ -46,14 +46,19 @@ import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Ignore import org.junit.Ignore
import org.junit.Test import org.junit.Test
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.net.URL import java.net.URL
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.nio.file.FileAlreadyExistsException import java.nio.file.FileAlreadyExistsException
import java.nio.file.FileSystem import java.nio.file.FileSystem
import java.nio.file.Path import java.nio.file.Path
import java.util.* import java.util.*
import java.util.jar.JarEntry
import java.util.jar.JarInputStream import java.util.jar.JarInputStream
import java.util.jar.JarOutputStream
import java.util.jar.Manifest
import kotlin.streams.toList import kotlin.streams.toList
import kotlin.test.* import kotlin.test.*
@ -788,6 +793,32 @@ class NodeAttachmentServiceTest {
} }
} }
@Test(timeout=300_000)
fun `attachments containing jar entries whose names expose malicious directory traversal are prevented`() {
fun createJarWithJarEntryTraversalAttack(jarEntryName: String): InputStream {
val byteArrayOutputStream = ByteArrayOutputStream()
JarOutputStream(byteArrayOutputStream, Manifest()).apply {
putNextEntry(JarEntry(jarEntryName))
write("some-text".toByteArray())
closeEntry()
close()
}
return ByteArrayInputStream(byteArrayOutputStream.toByteArray())
}
val traversalAttackJarWin = createJarWithJarEntryTraversalAttack("..\\attack")
val traversalAttackJarUnix = createJarWithJarEntryTraversalAttack("../attack")
assertFailsWith(IllegalArgumentException::class) {
NodeAttachmentService.checkIsAValidJAR(traversalAttackJarWin)
}
assertFailsWith(IllegalArgumentException::class) {
NodeAttachmentService.checkIsAValidJAR(traversalAttackJarUnix)
}
}
@Test(timeout=300_000) @Test(timeout=300_000)
fun `attachments can be queried by providing a intersection of signers using an EQUAL statement - EQUAL containing a single public key`() { fun `attachments can be queried by providing a intersection of signers using an EQUAL statement - EQUAL containing a single public key`() {
SelfCleaningDir().use { file -> SelfCleaningDir().use { file ->

View File

@ -22,7 +22,7 @@ class VaultQueryExceptionsTests : VaultQueryParties by rule {
@ClassRule @ClassRule
@JvmField @JvmField
val rule = object : VaultQueryTestRule() { val rule = object : VaultQueryTestRule(persistentServices = false) {
override val cordappPackages = listOf( override val cordappPackages = listOf(
"net.corda.testing.contracts", "net.corda.testing.contracts",
"net.corda.finance.contracts", "net.corda.finance.contracts",

View File

@ -4,6 +4,7 @@ import com.nhaarman.mockito_kotlin.mock
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.packageName import net.corda.core.internal.packageName
@ -37,6 +38,7 @@ import net.corda.testing.internal.configureDatabase
import net.corda.testing.internal.vault.* import net.corda.testing.internal.vault.*
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndPersistentServices
import net.corda.testing.node.makeTestIdentityService import net.corda.testing.node.makeTestIdentityService
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatCode import org.assertj.core.api.Assertions.assertThatCode
@ -102,7 +104,7 @@ interface VaultQueryParties {
val cordappPackages: List<String> val cordappPackages: List<String>
} }
open class VaultQueryTestRule : ExternalResource(), VaultQueryParties { open class VaultQueryTestRule(private val persistentServices: Boolean) : ExternalResource(), VaultQueryParties {
override val alice = TestIdentity(ALICE_NAME, 70) override val alice = TestIdentity(ALICE_NAME, 70)
override val bankOfCorda = TestIdentity(BOC_NAME) override val bankOfCorda = TestIdentity(BOC_NAME)
override val bigCorp = TestIdentity(CordaX500Name("BigCorporation", "New York", "US")) override val bigCorp = TestIdentity(CordaX500Name("BigCorporation", "New York", "US"))
@ -135,12 +137,22 @@ open class VaultQueryTestRule : ExternalResource(), VaultQueryParties {
override fun before() { override fun before() {
// register additional identities val databaseAndServices = if (persistentServices) {
val databaseAndServices = makeTestDatabaseAndMockServices( makeTestDatabaseAndPersistentServices(
cordappPackages,
megaCorp,
moreKeys = setOf(DUMMY_NOTARY_KEY),
moreIdentities = setOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, dummyCashIssuer.identity, dummyNotary.identity)
)
} else {
@Suppress("SpreadOperator")
makeTestDatabaseAndMockServices(
cordappPackages, cordappPackages,
makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, dummyCashIssuer.identity, dummyNotary.identity), makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, dummyCashIssuer.identity, dummyNotary.identity),
megaCorp, megaCorp,
moreKeys = *arrayOf(DUMMY_NOTARY_KEY)) moreKeys = *arrayOf(DUMMY_NOTARY_KEY)
)
}
database = databaseAndServices.first database = databaseAndServices.first
services = databaseAndServices.second services = databaseAndServices.second
vaultFiller = VaultFiller(services, dummyNotary) vaultFiller = VaultFiller(services, dummyNotary)
@ -2832,9 +2844,8 @@ abstract class VaultQueryTestsBase : VaultQueryParties {
} }
class VaultQueryTests : VaultQueryTestsBase(), VaultQueryParties by delegate { class VaultQueryTests : VaultQueryTestsBase(), VaultQueryParties by delegate {
companion object { companion object {
val delegate = VaultQueryTestRule() val delegate = VaultQueryTestRule(persistentServices = false)
} }
@Rule @Rule
@ -3138,3 +3149,33 @@ class VaultQueryTests : VaultQueryTestsBase(), VaultQueryParties by delegate {
} }
} }
} }
class PersistentServicesVaultQueryTests : VaultQueryParties by delegate {
companion object {
val delegate = VaultQueryTestRule(persistentServices = true)
@ClassRule
@JvmField
val testSerialization = SerializationEnvironmentRule()
}
@Rule
@JvmField
val vaultQueryTestRule = delegate
@Test(timeout = 300_000)
fun `query on externalId which maps to multiple keys`() {
val externalId = UUID.randomUUID()
val page = database.transaction {
val keys = Array(2) { services.keyManagementService.freshKey(externalId) }
vaultFiller.fillWithDummyState(participants = keys.map(::AnonymousParty))
services.vaultService.queryBy<ContractState>(
VaultQueryCriteria(externalIds = listOf(externalId)),
paging = PageSpecification(DEFAULT_PAGE_NUM, 10)
)
}
assertThat(page.states).hasSize(1)
assertThat(page.totalStatesAvailable).isEqualTo(1)
}
}

View File

@ -25,6 +25,11 @@ class InfrequentlyMutatedCacheTest {
database.close() database.close()
} }
@Test(timeout = 300_000)
fun `invalidate outside transaction should not hang`() {
cache.invalidate("Fred")
}
@Test(timeout=300_000) @Test(timeout=300_000)
fun `get from empty cache returns result of loader`() { fun `get from empty cache returns result of loader`() {
database.transaction { database.transaction {

Some files were not shown because too many files have changed in this diff Show More