mirror of
https://github.com/corda/corda.git
synced 2024-12-19 21:17:58 +00:00
Merge branch 'release/os/4.6' into dan/4.6-into-checkpoint-feature-branch-2020-05-05
# Conflicts: # node/src/main/kotlin/net/corda/node/services/statemachine/Event.kt # node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt # node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt # node/src/main/kotlin/net/corda/node/services/statemachine/transitions/TopLevelTransition.kt
This commit is contained in:
commit
4ccd0fd3df
79
.ci/dev/compatibility/JenkinsfileJDK11Azul
Normal file
79
.ci/dev/compatibility/JenkinsfileJDK11Azul
Normal file
@ -0,0 +1,79 @@
|
||||
@Library('corda-shared-build-pipeline-steps')
|
||||
import static com.r3.build.BuildControl.killAllExistingBuildsForJob
|
||||
|
||||
killAllExistingBuildsForJob(env.JOB_NAME, env.BUILD_NUMBER.toInteger())
|
||||
|
||||
pipeline {
|
||||
agent { label 'k8s' }
|
||||
options {
|
||||
timestamps()
|
||||
buildDiscarder(logRotator(daysToKeepStr: '7', artifactDaysToKeepStr: '7'))
|
||||
timeout(time: 3, unit: 'HOURS')
|
||||
}
|
||||
|
||||
environment {
|
||||
DOCKER_TAG_TO_USE = "${env.GIT_COMMIT.subSequence(0, 8)}"
|
||||
EXECUTOR_NUMBER = "${env.EXECUTOR_NUMBER}"
|
||||
BUILD_ID = "${env.BUILD_ID}-${env.JOB_NAME}"
|
||||
ARTIFACTORY_CREDENTIALS = credentials('artifactory-credentials')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Corda Pull Request - Generate Build Image') {
|
||||
steps {
|
||||
withCredentials([string(credentialsId: 'container_reg_passwd', variable: 'DOCKER_PUSH_PWD')]) {
|
||||
sh "./gradlew " +
|
||||
"-Dkubenetize=true " +
|
||||
"-Ddocker.push.password=\"\${DOCKER_PUSH_PWD}\" " +
|
||||
"-Ddocker.work.dir=\"/tmp/\${EXECUTOR_NUMBER}\" " +
|
||||
"-Ddocker.build.tag=\"\${DOCKER_TAG_TO_USE}\" " +
|
||||
"-Ddocker.buildbase.tag=11latest " +
|
||||
"-Ddocker.dockerfile=DockerfileJDK11Azul" +
|
||||
" clean pushBuildImage --stacktrace"
|
||||
}
|
||||
sh "kubectl auth can-i get pods"
|
||||
}
|
||||
}
|
||||
|
||||
stage('Testing phase') {
|
||||
parallel {
|
||||
stage('Regression Test') {
|
||||
steps {
|
||||
sh "./gradlew " +
|
||||
"-DbuildId=\"\${BUILD_ID}\" " +
|
||||
"-Dkubenetize=true " +
|
||||
"-Ddocker.run.tag=\"\${DOCKER_TAG_TO_USE}\" " +
|
||||
"-Dartifactory.username=\"\${ARTIFACTORY_CREDENTIALS_USR}\" " +
|
||||
"-Dartifactory.password=\"\${ARTIFACTORY_CREDENTIALS_PSW}\" " +
|
||||
"-Dgit.branch=\"\${GIT_BRANCH}\" " +
|
||||
"-Dgit.target.branch=\"\${GIT_BRANCH}\" " +
|
||||
" parallelRegressionTest --stacktrace"
|
||||
}
|
||||
}
|
||||
stage('Slow Integration Test') {
|
||||
steps {
|
||||
sh "./gradlew " +
|
||||
"-DbuildId=\"\${BUILD_ID}\" " +
|
||||
"-Dkubenetize=true " +
|
||||
"-Ddocker.run.tag=\"\${DOCKER_TAG_TO_USE}\" " +
|
||||
"-Dartifactory.username=\"\${ARTIFACTORY_CREDENTIALS_USR}\" " +
|
||||
"-Dartifactory.password=\"\${ARTIFACTORY_CREDENTIALS_PSW}\" " +
|
||||
"-Dgit.branch=\"\${GIT_BRANCH}\" " +
|
||||
"-Dgit.target.branch=\"\${GIT_BRANCH}\" " +
|
||||
" allParallelSlowIntegrationTest --stacktrace"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
always {
|
||||
archiveArtifacts artifacts: '**/pod-logs/**/*.log', fingerprint: false
|
||||
junit '**/build/test-results-xml/**/*.xml'
|
||||
}
|
||||
cleanup {
|
||||
deleteDir() /* clean up our workspace */
|
||||
}
|
||||
}
|
||||
}
|
2
.ci/dev/integration/Jenkinsfile
vendored
2
.ci/dev/integration/Jenkinsfile
vendored
@ -5,7 +5,7 @@ import static com.r3.build.BuildControl.killAllExistingBuildsForJob
|
||||
killAllExistingBuildsForJob(env.JOB_NAME, env.BUILD_NUMBER.toInteger())
|
||||
|
||||
pipeline {
|
||||
agent { label 'local-k8s' }
|
||||
agent { label 'k8s' }
|
||||
options {
|
||||
timestamps()
|
||||
timeout(time: 3, unit: 'HOURS')
|
||||
|
2
.ci/dev/nightly-regression/Jenkinsfile
vendored
2
.ci/dev/nightly-regression/Jenkinsfile
vendored
@ -4,7 +4,7 @@ import static com.r3.build.BuildControl.killAllExistingBuildsForJob
|
||||
killAllExistingBuildsForJob(env.JOB_NAME, env.BUILD_NUMBER.toInteger())
|
||||
|
||||
pipeline {
|
||||
agent { label 'local-k8s' }
|
||||
agent { label 'k8s' }
|
||||
options {
|
||||
timestamps()
|
||||
overrideIndexTriggers(false)
|
||||
|
2
.ci/dev/on-demand-tests/Jenkinsfile
vendored
2
.ci/dev/on-demand-tests/Jenkinsfile
vendored
@ -3,4 +3,4 @@ import static com.r3.build.BuildControl.killAllExistingBuildsForJob
|
||||
|
||||
killAllExistingBuildsForJob(env.JOB_NAME, env.BUILD_NUMBER.toInteger())
|
||||
|
||||
onDemandTestPipeline('local-k8s', '.ci/dev/on-demand-tests/commentMappings.yml')
|
||||
onDemandTestPipeline('k8s', '.ci/dev/on-demand-tests/commentMappings.yml')
|
||||
|
32
.ci/dev/regression/Jenkinsfile
vendored
32
.ci/dev/regression/Jenkinsfile
vendored
@ -4,7 +4,7 @@ import static com.r3.build.BuildControl.killAllExistingBuildsForJob
|
||||
killAllExistingBuildsForJob(env.JOB_NAME, env.BUILD_NUMBER.toInteger())
|
||||
|
||||
pipeline {
|
||||
agent { label 'local-k8s' }
|
||||
agent { label 'k8s' }
|
||||
options {
|
||||
timestamps()
|
||||
buildDiscarder(logRotator(daysToKeepStr: '7', artifactDaysToKeepStr: '7'))
|
||||
@ -61,6 +61,36 @@ pipeline {
|
||||
" allParallelSlowIntegrationTest --stacktrace"
|
||||
}
|
||||
}
|
||||
|
||||
stage('Generate sonarqube report') {
|
||||
steps {
|
||||
script {
|
||||
try {
|
||||
// running this step here is the only way to not majorly affect the distributed test plugin,
|
||||
// as now that neither returns build files nor runs jacoco reports
|
||||
sh "./gradlew --no-daemon build jacocoRootReport --stacktrace"
|
||||
withSonarQubeEnv('sq01') {
|
||||
sh "./gradlew --no-daemon sonarqube -x test --stacktrace"
|
||||
}
|
||||
timeout(time: 3, unit: 'MINUTES') {
|
||||
script {
|
||||
try {
|
||||
def qg = waitForQualityGate();
|
||||
if (qg.status != 'OK') {
|
||||
error "Pipeline aborted due to quality gate failure: ${qg.status}"
|
||||
}
|
||||
} catch (org.jenkinsci.plugins.workflow.steps.FlowInterruptedException e) {
|
||||
println('No sonarqube webhook response within timeout. Please check the webhook configuration in sonarqube.')
|
||||
// continue the pipeline
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
println('Error while trying to execute sonarqube analysis, will be skipped.')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
2
.ci/dev/smoke/Jenkinsfile
vendored
2
.ci/dev/smoke/Jenkinsfile
vendored
@ -4,7 +4,7 @@ import static com.r3.build.BuildControl.killAllExistingBuildsForJob
|
||||
killAllExistingBuildsForJob(env.JOB_NAME, env.BUILD_NUMBER.toInteger())
|
||||
|
||||
pipeline {
|
||||
agent { label 'local-k8s' }
|
||||
agent { label 'k8s' }
|
||||
options {
|
||||
timestamps()
|
||||
overrideIndexTriggers(false)
|
||||
|
2
.ci/dev/unit/Jenkinsfile
vendored
2
.ci/dev/unit/Jenkinsfile
vendored
@ -5,7 +5,7 @@ import static com.r3.build.BuildControl.killAllExistingBuildsForJob
|
||||
killAllExistingBuildsForJob(env.JOB_NAME, env.BUILD_NUMBER.toInteger())
|
||||
|
||||
pipeline {
|
||||
agent { label 'local-k8s' }
|
||||
agent { label 'k8s' }
|
||||
options {
|
||||
timestamps()
|
||||
timeout(time: 3, unit: 'HOURS')
|
||||
|
19
.github/workflows/jira_assign_issue.yml
vendored
Normal file
19
.github/workflows/jira_assign_issue.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
name: Sync assigned jira issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '15 * * * *'
|
||||
|
||||
jobs:
|
||||
sync_assigned:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Assign
|
||||
uses: corda/jira-sync-assigned-action@master
|
||||
with:
|
||||
jiraBaseUrl: ${{ secrets.JIRA_BASE_URL }}
|
||||
jiraEmail: ${{ secrets.JIRA_USER_EMAIL }}
|
||||
jiraToken: ${{ secrets.JIRA_API_TOKEN }}
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
owner: corda
|
||||
repository: corda
|
20
.github/workflows/jira_close_issue.yml
vendored
Normal file
20
.github/workflows/jira_close_issue.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
name: Sync closed jira issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 * * * *'
|
||||
|
||||
jobs:
|
||||
sync_closed:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Close
|
||||
uses: corda/jira-sync-closed-action@master
|
||||
with:
|
||||
project: CORDA
|
||||
jiraBaseUrl: https://r3-cev.atlassian.net
|
||||
jiraEmail: ${{ secrets.JIRA_USER_EMAIL }}
|
||||
jiraToken: ${{ secrets.JIRA_API_TOKEN }}
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
owner: corda
|
||||
repository: corda
|
36
.github/workflows/jira_create_issue.yml
vendored
Normal file
36
.github/workflows/jira_create_issue.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
name: Create jira issue from github issue
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
update_jira:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Jira Create issue
|
||||
id: create
|
||||
uses: corda/jira-create-issue-action@master
|
||||
with:
|
||||
jiraBaseUrl: https://r3-cev.atlassian.net
|
||||
project: CORDA
|
||||
issuetype: Bug
|
||||
summary: ${{ github.event.issue.title }}
|
||||
labels: community
|
||||
jiraEmail: ${{ secrets.JIRA_USER_EMAIL }}
|
||||
jiraToken: ${{ secrets.JIRA_API_TOKEN }}
|
||||
description: |
|
||||
${{ github.event.issue.body }}
|
||||
|
||||
Created by github action.
|
||||
|
||||
- name: Create comment
|
||||
uses: peter-evans/create-or-update-comment@v1
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
Automatically created Jira issue: ${{ steps.create.outputs.issue }}
|
||||
reaction-type: '+1'
|
2
Jenkinsfile
vendored
2
Jenkinsfile
vendored
@ -5,7 +5,7 @@ import static com.r3.build.BuildControl.killAllExistingBuildsForJob
|
||||
killAllExistingBuildsForJob(env.JOB_NAME, env.BUILD_NUMBER.toInteger())
|
||||
|
||||
pipeline {
|
||||
agent { label 'local-k8s' }
|
||||
agent { label 'k8s' }
|
||||
options {
|
||||
timestamps()
|
||||
timeout(time: 3, unit: 'HOURS')
|
||||
|
@ -2,7 +2,7 @@
|
||||
<img src="https://www.corda.net/wp-content/themes/corda/assets/images/crda-logo-big.svg" alt="Corda" width="500">
|
||||
</p>
|
||||
|
||||
<a href="https://ci-master.corda.r3cev.com/viewType.html?buildTypeId=Corda_CordaBuild&tab=buildTypeStatusDiv&guest=1"><img src="https://ci.corda.r3cev.com/app/rest/builds/buildType:Corda_CordaBuild/statusIcon"/></a> [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
|
||||
<a href="https://ci-master.corda.r3cev.com/viewType.html?buildTypeId=Corda_Build_ActiveReleaseBranches_BuildOsRelease45&tab=buildTypeStatusDiv&guest=1"><img src="https://ci.corda.r3cev.com/app/rest/builds/buildType:Corda_Build_ActiveReleaseBranches_BuildOsRelease45/statusIcon"/></a> [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
|
||||
|
||||
# Corda
|
||||
|
||||
|
38
build.gradle
38
build.gradle
@ -79,8 +79,8 @@ buildscript {
|
||||
ext.djvm_version = constants.getProperty("djvmVersion")
|
||||
ext.deterministic_rt_version = constants.getProperty('deterministicRtVersion')
|
||||
ext.okhttp_version = '3.14.2'
|
||||
ext.netty_version = '4.1.29.Final'
|
||||
ext.tcnative_version = '2.0.14.Final'
|
||||
ext.netty_version = '4.1.46.Final'
|
||||
ext.tcnative_version = '2.0.29.Final'
|
||||
ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion")
|
||||
ext.fileupload_version = '1.4'
|
||||
ext.kryo_version = '4.0.2'
|
||||
@ -189,6 +189,7 @@ buildscript {
|
||||
classpath group: "com.r3.testing", name: "gradle-distributed-testing-plugin", version: "1.2-LOCAL-K8S-SHARED-CACHE-SNAPSHOT", changing: true
|
||||
classpath group: "com.r3.dependx", name: "gradle-dependx", version: "0.1.13", changing: true
|
||||
classpath "com.bmuschko:gradle-docker-plugin:5.0.0"
|
||||
classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.8"
|
||||
}
|
||||
}
|
||||
|
||||
@ -241,6 +242,7 @@ allprojects {
|
||||
apply plugin: 'jacoco'
|
||||
apply plugin: 'org.owasp.dependencycheck'
|
||||
apply plugin: 'kotlin-allopen'
|
||||
apply plugin: 'org.sonarqube'
|
||||
|
||||
allOpen {
|
||||
annotations(
|
||||
@ -400,6 +402,16 @@ allprojects {
|
||||
}
|
||||
}
|
||||
}
|
||||
sonarqube {
|
||||
properties {
|
||||
property "sonar.projectName", "Corda"
|
||||
property "sonar.projectKey", "corda"
|
||||
property 'sonar.tests', '**/src/test/**,**/src/smoke-test/**,**/src/integration-test/**,**/src/integration-test-slow/**'
|
||||
property 'sonar.coverage.jacoco.xmlReportPaths', "${rootDir.path}/build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml"
|
||||
property 'detekt.sonar.kotlin.baseline.path', "${rootDir.path}/detekt-baseline.xml"
|
||||
property 'detekt.sonar.kotlin.config.path', "${rootDir.path}/detekt-config.yml"
|
||||
}
|
||||
}
|
||||
|
||||
// Check that we are running on a Java 8 JDK. The source/targetCompatibility values above aren't sufficient to
|
||||
// guarantee this because those are properties checked by the Java plugin, but we're using Kotlin.
|
||||
@ -459,6 +471,28 @@ task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) {
|
||||
it.exists()
|
||||
})
|
||||
}
|
||||
afterEvaluate {
|
||||
classDirectories = files(classDirectories.files.collect {
|
||||
fileTree(dir: it,
|
||||
// these exclusions are necessary because jacoco gets confused by same class names
|
||||
// which occur due to deterministic versions of non deterministic classes
|
||||
exclude: ['**/net/corda/core/crypto/SHA256DigestSupplier**',
|
||||
'**/net/corda/core/crypto/DelegatingSecureRandomService',
|
||||
'**/net/corda/core/internal/ThreadLocalToggleField**',
|
||||
'**/net/corda/core/internal/InheritableThreadLocalToggleField**',
|
||||
'**/net/corda/core/internal/ToggleField**',
|
||||
'net/corda/core/internal/rules/StateContractValidationEnforcementRule**',
|
||||
'net/corda/core/internal/SimpleToggleField**',
|
||||
'net/corda/core/serialization/SerializationFactory**',
|
||||
'net/corda/serialization/internal/amqp/AMQPStreams**',
|
||||
'net/corda/serialization/internal/amqp/AMQPSerializerFactories**',
|
||||
'net/corda/serialization/internal/amqp/AMQPSerializationThreadContext**',
|
||||
'net/corda/serialization/internal/ByteBufferStreams**',
|
||||
'net/corda/serialization/internal/model/DefaultCacheProvider**',
|
||||
'net/corda/serialization/internal/DefaultWhitelist**'
|
||||
])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('detekt', JavaExec) {
|
||||
|
@ -1,7 +1,6 @@
|
||||
package net.corda.client.rpc
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.esotericsoftware.kryo.KryoException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.messaging.startFlow
|
||||
|
@ -14,13 +14,13 @@ import net.corda.core.utilities.Try
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.millis
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.coretesting.internal.testThreadFactory
|
||||
import net.corda.node.services.rpc.RPCServerConfiguration
|
||||
import net.corda.nodeapi.RPCApi
|
||||
import net.corda.testing.common.internal.eventually
|
||||
import net.corda.testing.common.internal.succeeds
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.driver.internal.incrementalPortAllocation
|
||||
import net.corda.coretesting.internal.testThreadFactory
|
||||
import net.corda.testing.node.internal.RPCDriverDSL
|
||||
import net.corda.testing.node.internal.RpcBrokerHandle
|
||||
import net.corda.testing.node.internal.RpcServerHandle
|
||||
@ -75,7 +75,7 @@ class RPCStabilityTests {
|
||||
values.poll()
|
||||
}
|
||||
val first = values.peek()
|
||||
if (values.size == 5 && values.all { it.keys.size == first.keys.size }) {
|
||||
if (values.size == 5 && values.all { it.keys == first.keys }) {
|
||||
first
|
||||
} else {
|
||||
null
|
||||
|
@ -1,7 +1,6 @@
|
||||
package net.corda.common.configuration.parsing.internal
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import net.corda.common.validation.internal.Validated
|
||||
import net.corda.common.validation.internal.Validated.Companion.invalid
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
|
@ -16,6 +16,8 @@ dependencies {
|
||||
compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version"
|
||||
|
||||
testCompile project(":test-utils")
|
||||
testCompile "com.nhaarman:mockito-kotlin:$mockito_kotlin_version"
|
||||
testCompile "org.mockito:mockito-core:$mockito_version"
|
||||
}
|
||||
|
||||
|
||||
|
@ -9,4 +9,4 @@ package net.corda.common.logging
|
||||
* (originally added to source control for ease of use)
|
||||
*/
|
||||
|
||||
internal const val CURRENT_MAJOR_RELEASE = "4.5-SNAPSHOT"
|
||||
internal const val CURRENT_MAJOR_RELEASE = "4.6-SNAPSHOT"
|
||||
|
@ -0,0 +1,31 @@
|
||||
package net.corda.common.logging.errorReporting
|
||||
|
||||
import net.corda.common.logging.CordaVersion
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Provides information specific to Corda to the error reporting library.
|
||||
*
|
||||
* The primary use of this is to provide the URL to the docs site where the error information is hosted.
|
||||
*/
|
||||
class CordaErrorContextProvider : ErrorContextProvider {
|
||||
|
||||
companion object {
|
||||
private const val BASE_URL = "https://docs.corda.net/docs"
|
||||
private const val OS_PAGES = "corda-os"
|
||||
private const val ENTERPRISE_PAGES = "corda-enterprise"
|
||||
private const val ERROR_CODE_PAGE = "error-codes.html"
|
||||
}
|
||||
|
||||
override fun getURL(locale: Locale): String {
|
||||
val versionNumber = CordaVersion.releaseVersion
|
||||
|
||||
// This slightly strange block here allows the code to be merged across to Enterprise with no changes.
|
||||
val productVersion = if (CordaVersion.platformEditionCode == "OS") {
|
||||
OS_PAGES
|
||||
} else {
|
||||
ENTERPRISE_PAGES
|
||||
}
|
||||
return "$BASE_URL/$productVersion/$versionNumber/$ERROR_CODE_PAGE"
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package net.corda.common.logging.errorReporting
|
||||
|
||||
/**
|
||||
* A type representing an error condition.
|
||||
*
|
||||
* Error codes should be used in situations where an error is expected and information can be provided back to the user about what they've
|
||||
* done wrong. Each error code should have a resource bundle defined for it, which contains set of properties that define the error string
|
||||
* in different languages. See the resource bundles in common/logging/src/main/resources/errorReporting for more details.
|
||||
*/
|
||||
interface ErrorCode<CODES> where CODES: ErrorCodes, CODES: Enum<CODES> {
|
||||
|
||||
/**
|
||||
* The error code.
|
||||
*
|
||||
* Error codes are used to indicate what sort of error occurred. A unique code should be returned for each possible
|
||||
* error condition that could be reported within the defined namespace. The code should very briefly describe what has gone wrong, e.g.
|
||||
* "failed-to-store" or "connection-unavailable".
|
||||
*/
|
||||
val code: CODES
|
||||
|
||||
/**
|
||||
* Parameters to pass to the string template when reporting this error. The corresponding template that defines the error string in the
|
||||
* resource bundle must be expecting this list of parameters. Parameters should be in the order required by the message template - for
|
||||
* example, if the message template is "This error has argument {0} and argument {1}", the first element of this list will be placed
|
||||
* into {0}.
|
||||
*/
|
||||
val parameters: List<Any>
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package net.corda.common.logging.errorReporting
|
||||
|
||||
/**
|
||||
* A collection of error codes.
|
||||
*
|
||||
* Types implementing this are required to be enum classes to be used in an error.
|
||||
*/
|
||||
interface ErrorCodes {
|
||||
/**
|
||||
* The namespace of this collection of errors.
|
||||
*
|
||||
* These are used to partition errors into categories, e.g. "database" or "cordapp". Namespaces should be unique, which can be enforced
|
||||
* by using enum elements.
|
||||
*/
|
||||
val namespace: String
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package net.corda.common.logging.errorReporting
|
||||
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Provide context around reported errors by supplying product specific information.
|
||||
*/
|
||||
interface ErrorContextProvider {
|
||||
|
||||
/**
|
||||
* Get the URL to the docs site where the error codes are hosted.
|
||||
*
|
||||
* Note that the correct docs site link is likely to depend on the following:
|
||||
* - The locale of the error message
|
||||
* - The product the error was reported from
|
||||
* - The version of the product the error was reported from
|
||||
*
|
||||
* The returned URL must be the link the to the error code table in the documentation.
|
||||
*
|
||||
* @param locale The locale of the link
|
||||
* @return The URL of the docs site, to be printed in the logs
|
||||
*/
|
||||
fun getURL(locale: Locale) : String
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package net.corda.common.logging.errorReporting
|
||||
|
||||
import org.slf4j.Logger
|
||||
|
||||
/**
|
||||
* Reports error conditions to the logs, using localised error messages.
|
||||
*/
|
||||
internal interface ErrorReporter {
|
||||
/**
|
||||
* Report a particular error condition
|
||||
*
|
||||
* @param error The error to report
|
||||
* @param logger The logger to use when reporting this error
|
||||
*/
|
||||
fun report(error: ErrorCode<*>, logger: Logger)
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package net.corda.common.logging.errorReporting
|
||||
|
||||
import org.slf4j.Logger
|
||||
import java.text.MessageFormat
|
||||
import java.util.*
|
||||
|
||||
internal const val ERROR_INFO_RESOURCE = "ErrorInfo"
|
||||
internal const val ERROR_CODE_MESSAGE = "errorCodeMessage"
|
||||
internal const val ERROR_CODE_URL = "errorCodeUrl"
|
||||
|
||||
internal class ErrorReporterImpl(private val resourceLocation: String,
|
||||
private val locale: Locale,
|
||||
private val errorContextProvider: ErrorContextProvider) : ErrorReporter {
|
||||
|
||||
private fun fetchAndFormat(resource: String, property: String, params: Array<out Any>) : String {
|
||||
val bundle = ResourceBundle.getBundle(resource, locale)
|
||||
val template = bundle.getString(property)
|
||||
val formatter = MessageFormat(template, locale)
|
||||
return formatter.format(params)
|
||||
}
|
||||
|
||||
// Returns the string appended to all reported errors, indicating the error code and the URL to go to.
|
||||
// e.g. [Code: my-error-code, For further information, please go to https://docs.corda.net/corda-os/4.5/error-codes.html]
|
||||
private fun getErrorInfo(error: ErrorCode<*>) : String {
|
||||
val resource = "$resourceLocation/$ERROR_INFO_RESOURCE"
|
||||
val codeMessage = fetchAndFormat(resource, ERROR_CODE_MESSAGE, arrayOf(error.formatCode()))
|
||||
val urlMessage = fetchAndFormat(resource, ERROR_CODE_URL, arrayOf(errorContextProvider.getURL(locale)))
|
||||
return "[$codeMessage, $urlMessage]"
|
||||
}
|
||||
|
||||
override fun report(error: ErrorCode<*>, logger: Logger) {
|
||||
val errorResource = ErrorResource.fromErrorCode(error, resourceLocation, locale)
|
||||
val message = "${errorResource.getErrorMessage(error.parameters.toTypedArray())} ${getErrorInfo(error)}"
|
||||
logger.error(message)
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
package net.corda.common.logging.errorReporting
|
||||
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Entry point into the Error Reporting framework.
|
||||
*
|
||||
* This creates the error reporter used to report errors. The `initialiseReporting` method should be called to build a reporter before any
|
||||
* errors are reported.
|
||||
*/
|
||||
class ErrorReporting private constructor(private val localeString: String,
|
||||
private val resourceLocation: String,
|
||||
private val contextProvider: ErrorContextProvider?) {
|
||||
|
||||
constructor() : this(DEFAULT_LOCALE, DEFAULT_LOCATION, null)
|
||||
|
||||
private companion object {
|
||||
private const val DEFAULT_LOCALE = "en-US"
|
||||
private const val DEFAULT_LOCATION = "."
|
||||
|
||||
private var errorReporter: ErrorReporter? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the locale to use when reporting errors
|
||||
*
|
||||
* @param locale The locale tag to use when reporting errors, e.g. en-US
|
||||
*/
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun withLocale(locale: String) : ErrorReporting {
|
||||
// Currently, if anything other than the default is used this is entirely untested. As a result, an exception is thrown here to
|
||||
// indicate that this functionality is not ready at this point in time.
|
||||
throw LocaleSettingUnsupportedException()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the location of the resource bundles containing the error codes.
|
||||
*
|
||||
* @param location The location within the JAR of the resource bundle
|
||||
*/
|
||||
fun usingResourcesAt(location: String) : ErrorReporting {
|
||||
return ErrorReporting(localeString, location, contextProvider)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the context provider to supply project-specific information about the errors.
|
||||
*
|
||||
* @param contextProvider The context provider to use with error reporting
|
||||
*/
|
||||
fun withContextProvider(contextProvider: ErrorContextProvider) : ErrorReporting {
|
||||
return ErrorReporting(localeString, resourceLocation, contextProvider)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the reporting of errors.
|
||||
*/
|
||||
fun initialiseReporting() {
|
||||
if (contextProvider == null) {
|
||||
throw NoContextProviderSuppliedException()
|
||||
}
|
||||
if (errorReporter != null) {
|
||||
throw DoubleInitializationException()
|
||||
}
|
||||
errorReporter = ErrorReporterImpl(resourceLocation, Locale.forLanguageTag(localeString), contextProvider)
|
||||
}
|
||||
|
||||
internal fun getReporter() : ErrorReporter {
|
||||
return errorReporter ?: throw ReportingUninitializedException()
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package net.corda.common.logging.errorReporting
|
||||
|
||||
abstract class ErrorReportingException(message: String, cause: Throwable? = null) : Exception(message, cause)
|
||||
|
||||
/**
|
||||
* Occurs when reporting is requested before the error reporting code has been initialized
|
||||
*/
|
||||
class ReportingUninitializedException : ErrorReportingException("Error reporting is uninitialized")
|
||||
|
||||
/**
|
||||
* Occurs when no error context provider is supplied while initializing error reporting
|
||||
*/
|
||||
class NoContextProviderSuppliedException
|
||||
: ErrorReportingException("No error context provider was supplied when initializing error reporting")
|
||||
|
||||
/**
|
||||
* Occurs if the error reporting framework has been initialized twice
|
||||
*/
|
||||
class DoubleInitializationException : ErrorReportingException("Error reporting has previously been initialized")
|
||||
|
||||
/**
|
||||
* Occurs if a locale is set while initializing the error reporting framework.
|
||||
*
|
||||
* This is done as locale support has not yet been properly designed, and so using anything other than the default is untested.
|
||||
*/
|
||||
class LocaleSettingUnsupportedException :
|
||||
ErrorReportingException("Setting a locale other than the default is not supported in the first release")
|
@ -0,0 +1,18 @@
|
||||
package net.corda.common.logging.errorReporting
|
||||
|
||||
import org.slf4j.Logger
|
||||
|
||||
/**
|
||||
* Report errors that have occurred.
|
||||
*
|
||||
* Doing this allows the error reporting framework to find the corresponding resources for the error and pick the correct locale.
|
||||
*
|
||||
* @param error The error that has occurred.
|
||||
*/
|
||||
fun Logger.report(error: ErrorCode<*>) = ErrorReporting().getReporter().report(error, this)
|
||||
|
||||
internal fun ErrorCode<*>.formatCode() : String {
|
||||
val namespaceString = this.code.namespace.toLowerCase().replace("_", "-")
|
||||
val codeString = this.code.toString().toLowerCase().replace("_", "-")
|
||||
return "$namespaceString-$codeString"
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package net.corda.common.logging.errorReporting
|
||||
|
||||
import net.corda.common.logging.errorReporting.ResourceBundleProperties.ACTIONS_TO_FIX
|
||||
import net.corda.common.logging.errorReporting.ResourceBundleProperties.ALIASES
|
||||
import net.corda.common.logging.errorReporting.ResourceBundleProperties.MESSAGE_TEMPLATE
|
||||
import net.corda.common.logging.errorReporting.ResourceBundleProperties.SHORT_DESCRIPTION
|
||||
import java.text.MessageFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A representation of a single error resource file.
|
||||
*
|
||||
* This handles selecting the right properties from the resource bundle and formatting the error message.
|
||||
*/
|
||||
class ErrorResource private constructor(private val bundle: ResourceBundle,
|
||||
private val locale: Locale) {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Construct an error resource from a provided code.
|
||||
*
|
||||
* @param errorCode The code to get the resource bundle for
|
||||
* @param resourceLocation The location in the JAR of the error code resource bundles
|
||||
* @param locale The locale to use for this resource
|
||||
*/
|
||||
fun fromErrorCode(errorCode: ErrorCode<*>, resourceLocation: String, locale: Locale) : ErrorResource {
|
||||
val resource = "$resourceLocation/${errorCode.formatCode()}"
|
||||
val bundle = ResourceBundle.getBundle(resource, locale)
|
||||
return ErrorResource(bundle, locale)
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an error resource using resources loaded in a given classloader
|
||||
*
|
||||
* @param resource The resource bundle to load
|
||||
* @param classLoader The classloader used to load the resource bundles
|
||||
* @param locale The locale to use for this resource
|
||||
*/
|
||||
fun fromLoader(resource: String, classLoader: ClassLoader, locale: Locale) : ErrorResource {
|
||||
val bundle = ResourceBundle.getBundle(resource, locale, classLoader)
|
||||
return ErrorResource(bundle, locale)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getProperty(propertyName: String) : String = bundle.getString(propertyName)
|
||||
|
||||
private fun formatTemplate(template: String, args: Array<Any>) : String {
|
||||
val formatter = MessageFormat(template, locale)
|
||||
return formatter.format(args)
|
||||
}
|
||||
|
||||
fun getErrorMessage(args: Array<Any>): String {
|
||||
val template = getProperty(MESSAGE_TEMPLATE)
|
||||
return formatTemplate(template, args)
|
||||
}
|
||||
|
||||
val shortDescription: String = getProperty(SHORT_DESCRIPTION)
|
||||
val actionsToFix: String = getProperty(ACTIONS_TO_FIX)
|
||||
val aliases: String = getProperty(ALIASES)
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package net.corda.common.logging.errorReporting
|
||||
|
||||
/**
|
||||
* Namespaces for errors within the node.
|
||||
*/
|
||||
enum class NodeNamespaces {
|
||||
DATABASE,
|
||||
CORDAPP
|
||||
}
|
||||
|
||||
/**
|
||||
* Errors related to database connectivity
|
||||
*/
|
||||
enum class NodeDatabaseErrors : ErrorCodes {
|
||||
COULD_NOT_CONNECT,
|
||||
MISSING_DRIVER,
|
||||
FAILED_STARTUP,
|
||||
PASSWORD_REQUIRED_FOR_H2;
|
||||
|
||||
override val namespace = NodeNamespaces.DATABASE.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Errors related to loading of Cordapps
|
||||
*/
|
||||
enum class CordappErrors : ErrorCodes {
|
||||
DUPLICATE_CORDAPPS_INSTALLED,
|
||||
MULTIPLE_CORDAPPS_FOR_FLOW,
|
||||
MISSING_VERSION_ATTRIBUTE,
|
||||
INVALID_VERSION_IDENTIFIER;
|
||||
|
||||
override val namespace = NodeNamespaces.CORDAPP.toString()
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package net.corda.common.logging.errorReporting
|
||||
|
||||
/**
|
||||
* Constants defining the properties available in the resource bundle files.
|
||||
*/
|
||||
object ResourceBundleProperties {
|
||||
const val MESSAGE_TEMPLATE = "errorTemplate"
|
||||
const val SHORT_DESCRIPTION = "shortDescription"
|
||||
const val ACTIONS_TO_FIX = "actionsToFix"
|
||||
const val ALIASES = "aliases"
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
errorCodeMessage = Error Code: {0}
|
||||
errorCodeUrl = For further information, please go to {0}
|
@ -0,0 +1,2 @@
|
||||
errorCodeMessage = Error Code: {0}
|
||||
errorCodeUrl = For further information, please go to {0}
|
@ -0,0 +1,4 @@
|
||||
errorTemplate = The CorDapp (name: {0}, file: {1}) is installed multiple times on the node. The following files correspond to the exact same content: {2}
|
||||
shortDescription = A CorDapp has been installed multiple times on the same node.
|
||||
actionsToFix = Investigate the logs to determine the files with duplicate content, and remove one of them from the cordapps directory.
|
||||
aliases = iw8d4e
|
@ -0,0 +1,3 @@
|
||||
errorTemplate = The CorDapp (name: {0}, file: {1}) is installed multiple times on the node. The following files correspond to the exact same content: {2}
|
||||
shortDescription = A CorDapp has been installed multiple times on the same node.
|
||||
actionsToFix = Investigate the logs to determine the files with duplicate content, and remove one of them from the cordapps directory.
|
@ -0,0 +1,4 @@
|
||||
errorTemplate = Version identifier ({0}) for attribute {1} must be a whole number starting from 1.
|
||||
shortDescription = A version attribute was specified in the CorDapp manifest with an invalid value. The value must be a whole number, and it must be greater than or equal to 1.
|
||||
actionsToFix = Investigate the logs to find the invalid attribute, and change the attribute value to be valid (a whole number greater than or equal to 1).
|
||||
aliases =
|
@ -0,0 +1,4 @@
|
||||
errorTemplate = Version identifier ({0}) for attribute {1} must be a whole number starting from 1.
|
||||
shortDescription = A version attribute was specified in the CorDapp manifest with an invalid value. The value must be a whole number, and it must be greater than or equal to 1.
|
||||
actionsToFix = Investigate the logs to find the invalid attribute, and change the attribute value to be valid (a whole number greater than or equal to 1).
|
||||
aliases =
|
@ -0,0 +1,4 @@
|
||||
errorTemplate = Target versionId attribute {0} not specified. Please specify a whole number starting from 1.
|
||||
shortDescription = A required version attribute was not specified in the manifest of the CorDapp JAR.
|
||||
actionsToFix = Investigate the logs to find out which version attribute has not been specified, and add that version attribute to the CorDapp manifest.
|
||||
aliases =
|
@ -0,0 +1,3 @@
|
||||
errorTemplate = Target versionId attribute {0} not specified. Please specify a whole number starting from 1.
|
||||
shortDescription = A required version attribute was not specified in the manifest of the CorDapp JAR.
|
||||
actionsToFix = Investigate the logs to find out which version attribute has not been specified, and add that version attribute to the CorDapp manifest.
|
@ -0,0 +1,4 @@
|
||||
errorTemplate = There are multiple CorDapp JARs on the classpath for the flow {0}: [{1}]
|
||||
shortDescription = Multiple CorDapp JARs on the classpath define the same flow class. As a result, the platform will not know which version of the flow to start when the flow is invoked.
|
||||
actionsToFix = Investigate the logs to find out which CorDapp JARs define the same flow classes. The developers of these apps will need to resolve the clash.
|
||||
aliases =
|
@ -0,0 +1,4 @@
|
||||
errorTemplate = There are multiple CorDapp JARs on the classpath for the flow {0}: [{1}]
|
||||
shortDescription = Multiple CorDapp JARs on the classpath define the same flow class. As a result, the platform will not know which version of the flow to start when the flow is invoked.
|
||||
actionsToFix = Investigate the logs to find out which CorDapp JARs define the same flow classes. The developers of these apps will need to resolve the clash.
|
||||
aliases =
|
@ -0,0 +1,4 @@
|
||||
errorTemplate = Could not connect to the database. Please check your JDBC connection URL, or the connectivity to the database.
|
||||
shortDescription = The node failed to connect to the database on node startup, preventing the node from starting correctly.
|
||||
actionsToFix = This happens either because the database connection has been misconfigured or the database is unreachable. Check that the JDBC URL is configured correctly in your node.conf. If this is correctly configured, then check your database connection.
|
||||
aliases =
|
@ -0,0 +1,3 @@
|
||||
errorTemplate = Could not connect to the database. Please check your JDBC connection URL, or the connectivity to the database.
|
||||
shortDescription = The node failed to connect to the database on node startup, preventing the node from starting correctly.
|
||||
actionsToFix = This happens either because the database connection has been misconfigured or the database is unreachable. Check that the JDBC URL is configured correctly in your node.conf. If this is correctly configured, then check your database connection.
|
@ -0,0 +1,4 @@
|
||||
errorTemplate = Failed to create the datasource. See the logs for further information and the cause.
|
||||
shortDescription = The datasource could not be created for unknown reasons.
|
||||
actionsToFix = The logs in the logs directory should contain more information on what went wrong.
|
||||
aliases =
|
@ -0,0 +1,3 @@
|
||||
errorTemplate = Failed to create the datasource. See the logs for further information and the cause.
|
||||
shortDescription = The datasource could not be created for unknown reasons.
|
||||
actionsToFix = The logs in the logs directory should contain more information on what went wrong.
|
@ -0,0 +1,4 @@
|
||||
errorTemplate = Could not find the database driver class. Please add it to the 'drivers' folder.
|
||||
shortDescription = The node could not find the driver in the 'drivers' directory.
|
||||
actionsToFix = Please ensure that the correct database driver has been placed in the 'drivers' folder. The driver must contain the driver main class specified in 'node.conf'.
|
||||
aliases =
|
@ -0,0 +1,3 @@
|
||||
errorTemplate = Could not find the database driver class. Please add it to the 'drivers' folder.
|
||||
shortDescription = The node could not find the driver in the 'drivers' directory.
|
||||
actionsToFix = Please ensure that the correct database driver has been placed in the 'drivers' folder. The driver must contain the driver main class specified in 'node.conf'.
|
@ -0,0 +1,4 @@
|
||||
errorTemplate = Database password is required for H2 server listening on {0}
|
||||
shortDescription = A password is required to access the H2 server the node is trying to access, and this password is missing.
|
||||
actionsToFix = Add the required password to the 'datasource.password' configuration in 'node.conf'.
|
||||
aliases =
|
@ -0,0 +1,3 @@
|
||||
errorTemplate = Database password is required for H2 server listening on {0}
|
||||
shortDescription = A password is required to access the H2 server the node is trying to access, and this password is missing.
|
||||
actionsToFix = Add the required password to the 'datasource.password' configuration in 'node.conf'.
|
@ -0,0 +1,20 @@
|
||||
package net.corda.commmon.logging.errorReporting
|
||||
|
||||
import net.corda.common.logging.CordaVersion
|
||||
import net.corda.common.logging.errorReporting.CordaErrorContextProvider
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class CordaErrorContextProviderTest {
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `check that correct URL is returned from context provider`() {
|
||||
val context = CordaErrorContextProvider()
|
||||
val expectedURL = "https://docs.corda.net/docs/corda-os/${CordaVersion.releaseVersion}/error-codes.html"
|
||||
// In this first release, there is only one localisation and the URL structure for future localisations is currently unknown. As
|
||||
// a result, the same URL is expected for all locales.
|
||||
assertEquals(expectedURL, context.getURL(Locale.getDefault()))
|
||||
assertEquals(expectedURL, context.getURL(Locale.forLanguageTag("es-ES")))
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package net.corda.commmon.logging.errorReporting
|
||||
|
||||
import net.corda.common.logging.errorReporting.CordappErrors
|
||||
|
||||
class CordappErrorsTest : ErrorCodeTest<CordappErrors>(CordappErrors::class.java, true) {
|
||||
override val dataForCodes = mapOf(
|
||||
CordappErrors.MISSING_VERSION_ATTRIBUTE to listOf("test-attribute"),
|
||||
CordappErrors.INVALID_VERSION_IDENTIFIER to listOf(-1, "test-attribute"),
|
||||
CordappErrors.MULTIPLE_CORDAPPS_FOR_FLOW to listOf("MyTestFlow", "Jar 1, Jar 2"),
|
||||
CordappErrors.DUPLICATE_CORDAPPS_INSTALLED to listOf("TestCordapp", "testapp.jar", "testapp2.jar")
|
||||
)
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package net.corda.commmon.logging.errorReporting
|
||||
|
||||
import net.corda.common.logging.errorReporting.NodeDatabaseErrors
|
||||
import java.net.InetAddress
|
||||
|
||||
class DatabaseErrorsTest : ErrorCodeTest<NodeDatabaseErrors>(NodeDatabaseErrors::class.java) {
|
||||
override val dataForCodes = mapOf(
|
||||
NodeDatabaseErrors.COULD_NOT_CONNECT to listOf<Any>(),
|
||||
NodeDatabaseErrors.FAILED_STARTUP to listOf(),
|
||||
NodeDatabaseErrors.MISSING_DRIVER to listOf(),
|
||||
NodeDatabaseErrors.PASSWORD_REQUIRED_FOR_H2 to listOf(InetAddress.getLocalHost())
|
||||
)
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package net.corda.commmon.logging.errorReporting
|
||||
|
||||
import junit.framework.TestCase.assertFalse
|
||||
import net.corda.common.logging.errorReporting.ErrorCode
|
||||
import net.corda.common.logging.errorReporting.ErrorCodes
|
||||
import net.corda.common.logging.errorReporting.ErrorResource
|
||||
import net.corda.common.logging.errorReporting.ResourceBundleProperties
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Utility for testing that error code resource files behave as expected.
|
||||
*
|
||||
* This allows for testing that error messages are printed correctly if they are provided the correct parameters. The test will fail if any
|
||||
* of the parameters of the template are not filled in.
|
||||
*
|
||||
* To use, override the `dataForCodes` with a map from an error code enum value to a list of parameters the message template takes. If any
|
||||
* are missed, the test will fail.
|
||||
*
|
||||
* `printProperties`, if set to true, will print the properties out the resource files, with the error message filled in. This allows the
|
||||
* message to be inspected.
|
||||
*/
|
||||
abstract class ErrorCodeTest<T>(private val clazz: Class<T>,
|
||||
private val printProperties: Boolean = false) where T: Enum<T>, T: ErrorCodes {
|
||||
|
||||
abstract val dataForCodes: Map<T, List<Any>>
|
||||
|
||||
private class TestError<T>(override val code: T,
|
||||
override val parameters: List<Any>) : ErrorCode<T> where T: Enum<T>, T: ErrorCodes
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `test error codes`() {
|
||||
for ((code, params) in dataForCodes) {
|
||||
val error = TestError(code, params)
|
||||
val resource = ErrorResource.fromErrorCode(error, "error-codes", Locale.forLanguageTag("en-US"))
|
||||
val message = resource.getErrorMessage(error.parameters.toTypedArray())
|
||||
assertFalse(
|
||||
"The error message reported for code $code contains missing parameters",
|
||||
message.contains("\\{.*}".toRegex())
|
||||
)
|
||||
val otherProperties = Triple(resource.shortDescription, resource.actionsToFix, resource.aliases)
|
||||
if (printProperties) {
|
||||
println("Data for $code")
|
||||
println("Error Message = $message")
|
||||
println("${ResourceBundleProperties.SHORT_DESCRIPTION} = ${otherProperties.first}")
|
||||
println("${ResourceBundleProperties.ACTIONS_TO_FIX} = ${otherProperties.second}")
|
||||
println("${ResourceBundleProperties.ALIASES} = ${otherProperties.third}")
|
||||
println("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `ensure all error codes tested`() {
|
||||
val expected = clazz.enumConstants.toSet()
|
||||
val actual = dataForCodes.keys.toSet()
|
||||
val missing = expected - actual
|
||||
assertTrue(missing.isEmpty(), "The following codes have not been tested: $missing")
|
||||
}
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
package net.corda.commmon.logging.errorReporting
|
||||
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import net.corda.common.logging.errorReporting.ErrorCode
|
||||
import net.corda.common.logging.errorReporting.ErrorCodes
|
||||
import net.corda.common.logging.errorReporting.ErrorContextProvider
|
||||
import net.corda.common.logging.errorReporting.ErrorReporterImpl
|
||||
import org.junit.After
|
||||
import org.junit.Test
|
||||
import org.mockito.ArgumentMatchers.anyString
|
||||
import org.mockito.Mockito
|
||||
import org.slf4j.Logger
|
||||
import java.text.DateFormat
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
class ErrorReporterImplTest {
|
||||
|
||||
companion object {
|
||||
private const val TEST_URL = "test-url"
|
||||
}
|
||||
|
||||
private val logs: MutableList<Any> = mutableListOf()
|
||||
|
||||
private val loggerMock = Mockito.mock(Logger::class.java).also {
|
||||
Mockito.`when`(it.error(anyString())).then { logs.addAll(it.arguments) }
|
||||
}
|
||||
|
||||
private val contextProvider: ErrorContextProvider = object : ErrorContextProvider {
|
||||
override fun getURL(locale: Locale): String {
|
||||
return "$TEST_URL/${locale.toLanguageTag()}"
|
||||
}
|
||||
}
|
||||
|
||||
private enum class TestNamespaces {
|
||||
TEST
|
||||
}
|
||||
|
||||
private enum class TestErrors : ErrorCodes {
|
||||
CASE1,
|
||||
CASE2,
|
||||
CASE_3;
|
||||
|
||||
override val namespace = TestNamespaces.TEST.toString()
|
||||
}
|
||||
|
||||
private val TEST_ERROR_1 = object : ErrorCode<TestErrors> {
|
||||
override val code = TestErrors.CASE1
|
||||
override val parameters = listOf<Any>()
|
||||
}
|
||||
|
||||
private class TestError2(currentDate: Date) : ErrorCode<TestErrors> {
|
||||
override val code = TestErrors.CASE2
|
||||
override val parameters: List<Any> = listOf("foo", 1, currentDate)
|
||||
}
|
||||
|
||||
private val TEST_ERROR_3 = object : ErrorCode<TestErrors> {
|
||||
override val code = TestErrors.CASE_3
|
||||
override val parameters = listOf<Any>()
|
||||
}
|
||||
|
||||
private fun createReporterImpl(localeTag: String?) : ErrorReporterImpl {
|
||||
val locale = if (localeTag != null) Locale.forLanguageTag(localeTag) else Locale.getDefault()
|
||||
return ErrorReporterImpl("errorReporting", locale, contextProvider)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
logs.clear()
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `error codes logged correctly`() {
|
||||
val error = TEST_ERROR_1
|
||||
val testReporter = createReporterImpl("en-US")
|
||||
testReporter.report(error, loggerMock)
|
||||
assertEquals(listOf("This is a test message [Code: test-case1, URL: $TEST_URL/en-US]"), logs)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_00)
|
||||
fun `error code with parameters correctly reported`() {
|
||||
val currentDate = Date.from(Instant.now())
|
||||
val error = TestError2(currentDate)
|
||||
val testReporter = createReporterImpl("en-US")
|
||||
testReporter.report(error, loggerMock)
|
||||
val format = DateFormat.getDateInstance(DateFormat.LONG, Locale.forLanguageTag("en-US"))
|
||||
assertEquals(listOf("This is the second case with string foo, number 1, date ${format.format(currentDate)} [Code: test-case2, URL: $TEST_URL/en-US]"), logs)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `locale used with no corresponding resource falls back to default`() {
|
||||
val error = TEST_ERROR_1
|
||||
val testReporter = createReporterImpl("fr-FR")
|
||||
testReporter.report(error, loggerMock)
|
||||
assertEquals(listOf("This is a test message [Code: test-case1, URL: $TEST_URL/fr-FR]"), logs)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `locale with corresponding resource causes correct error to be printed`() {
|
||||
val error = TEST_ERROR_1
|
||||
val testReporter = createReporterImpl("ga-IE")
|
||||
testReporter.report(error, loggerMock)
|
||||
assertEquals(listOf("Is teachtaireacht earráide é seo [Code: test-case1, URL: $TEST_URL/ga-IE]"), logs)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `locale with missing properties falls back to default properties`() {
|
||||
val error = TEST_ERROR_1
|
||||
val testReporter = createReporterImpl("es-ES")
|
||||
testReporter.report(error, loggerMock)
|
||||
assertEquals(listOf("This is a test message [Code: test-case1, URL: $TEST_URL/es-ES]"), logs)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `error code with underscore in translated to resource file successfully`() {
|
||||
val error = TEST_ERROR_3
|
||||
val testReporter = createReporterImpl("en-US")
|
||||
testReporter.report(error, loggerMock)
|
||||
assertEquals(listOf("This is the third test message [Code: test-case-3, URL: $TEST_URL/en-US]"), logs)
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
errorCodeMessage = Code: {0}
|
||||
errorCodeUrl = URL: {0}
|
@ -0,0 +1,2 @@
|
||||
errorCodeMessage = Code: {0}
|
||||
errorCodeUrl = URL: {0}
|
@ -0,0 +1,4 @@
|
||||
errorTemplate = This is the third test message
|
||||
shortDescription = Test description
|
||||
actionsToFix = Actions
|
||||
aliases =
|
@ -0,0 +1,4 @@
|
||||
errorTemplate = This is the third test message
|
||||
shortDescription = Test description
|
||||
actionsToFix = Actions
|
||||
aliases =
|
@ -0,0 +1,4 @@
|
||||
errorTemplate = This is a test message
|
||||
shortDescription = Test description
|
||||
actionsToFix = Actions
|
||||
aliases = foo, bar
|
@ -0,0 +1,3 @@
|
||||
errorTemplate = This is a test message
|
||||
shortDescription = Test description
|
||||
actionsToFix = Actions
|
@ -0,0 +1 @@
|
||||
shortDescription = Descripción de la prueba
|
@ -0,0 +1,3 @@
|
||||
errorTemplate = Is teachtaireacht earráide é seo
|
||||
shortDescription = Teachtaireacht tástála
|
||||
actionsToFix = Roinnt gníomhartha
|
@ -0,0 +1,4 @@
|
||||
errorTemplate = This is the second case with string {0}, number {1, number, integer}, date {2, date, long}
|
||||
shortDescription = Test description
|
||||
actionsToFix = Actions
|
||||
aliases = ""
|
@ -0,0 +1,4 @@
|
||||
errorTemplate = This is the second case with string {0}, number {1, number, integer}, date {2, date, long}
|
||||
shortDescription = Test description
|
||||
actionsToFix = Actions
|
||||
aliases =
|
@ -0,0 +1,4 @@
|
||||
errorTemplate = Seo an dara cás le sreang {0}, uimhir {1, uimhir, slánuimhir}, dáta {2, dáta, fada}
|
||||
shortDescription = An dara tuairisc
|
||||
actionsToFix = Roinnt gníomhartha eile
|
||||
aliases =
|
@ -2,7 +2,7 @@
|
||||
# because some versions here need to be matched by app authors in
|
||||
# their own projects. So don't get fancy with syntax!
|
||||
|
||||
cordaVersion=4.5
|
||||
cordaVersion=4.6
|
||||
versionSuffix=SNAPSHOT
|
||||
gradlePluginsVersion=5.0.8
|
||||
kotlinVersion=1.2.71
|
||||
@ -11,7 +11,7 @@ java8MinUpdateVersion=171
|
||||
# When incrementing platformVersion make sure to update #
|
||||
# net.corda.core.internal.CordaUtilsKt.PLATFORM_VERSION as well. #
|
||||
# ***************************************************************#
|
||||
platformVersion=6
|
||||
platformVersion=7
|
||||
guavaVersion=28.0-jre
|
||||
# Quasar version to use with Java 8:
|
||||
quasarVersion=0.7.12_r3
|
||||
@ -21,7 +21,7 @@ quasarVersion11=0.8.0_r3
|
||||
jdkClassifier11=jdk11
|
||||
proguardVersion=6.1.1
|
||||
bouncycastleVersion=1.60
|
||||
classgraphVersion=4.8.58
|
||||
classgraphVersion=4.8.71
|
||||
disruptorVersion=3.4.2
|
||||
typesafeConfigVersion=1.3.4
|
||||
jsr305Version=3.0.2
|
||||
@ -30,7 +30,7 @@ snakeYamlVersion=1.19
|
||||
caffeineVersion=2.7.0
|
||||
metricsVersion=4.1.0
|
||||
metricsNewRelicVersion=1.1.1
|
||||
djvmVersion=1.0
|
||||
djvmVersion=1.1-RC02
|
||||
deterministicRtVersion=1.0-RC02
|
||||
openSourceBranch=https://github.com/corda/corda/blob/release/os/4.4
|
||||
openSourceSamplesBranch=https://github.com/corda/samples/blob/release-V4
|
||||
|
@ -3,6 +3,7 @@ package net.corda.core.internal
|
||||
/**
|
||||
* Stubbing out non-deterministic method.
|
||||
*/
|
||||
fun <T: Any> createInstancesOfClassesImplementing(@Suppress("UNUSED_PARAMETER") classloader: ClassLoader, @Suppress("UNUSED_PARAMETER") clazz: Class<T>): Set<T> {
|
||||
fun <T: Any> createInstancesOfClassesImplementing(@Suppress("UNUSED_PARAMETER") classloader: ClassLoader, @Suppress("UNUSED_PARAMETER") clazz: Class<T>,
|
||||
@Suppress("UNUSED_PARAMETER") classVersionRange: IntRange? = null): Set<T> {
|
||||
return emptySet()
|
||||
}
|
@ -324,4 +324,17 @@ class TransactionVerificationExceptionSerialisationTests {
|
||||
assertEquals(exception.cause?.message, exception2.cause?.message)
|
||||
assertEquals(exception.txId, exception2.txId)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun unsupportedClassVersionErrorTest() {
|
||||
val cause = UnsupportedClassVersionError("wobble")
|
||||
val exception = TransactionVerificationException.UnsupportedClassVersionError(txid, cause.message!!, cause)
|
||||
val exception2 = DeserializationInput(factory).deserialize(
|
||||
SerializationOutput(factory).serialize(exception, context),
|
||||
context)
|
||||
|
||||
assertEquals(exception.message, exception2.message)
|
||||
assertEquals("java.lang.UnsupportedClassVersionError: ${exception.cause?.message}", exception2.cause?.message)
|
||||
assertEquals(exception.txId, exception2.txId)
|
||||
}
|
||||
}
|
@ -11,7 +11,6 @@ import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.minutes
|
||||
import net.corda.node.services.statemachine.StaffedFlowHospital
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.contracts.DummyState
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
@ -22,7 +21,6 @@ import net.corda.testing.driver.driver
|
||||
import net.corda.testing.node.internal.cordappsForPackages
|
||||
import org.junit.Test
|
||||
import java.sql.SQLTransientConnectionException
|
||||
import java.util.concurrent.Semaphore
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
|
@ -0,0 +1,364 @@
|
||||
package net.corda.coretests.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
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.KilledFlowException
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.flows.UnexpectedFlowEndException
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.minutes
|
||||
import net.corda.core.utilities.seconds
|
||||
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.singleIdentity
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.driver
|
||||
import org.apache.logging.log4j.Level
|
||||
import org.apache.logging.log4j.core.config.Configurator
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.Semaphore
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class FlowIsKilledTest {
|
||||
|
||||
private companion object {
|
||||
const val EXCEPTION_MESSAGE = "Goodbye, cruel world!"
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
Configurator.setLevel("net.corda.node.services.statemachine", Level.DEBUG)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `manually handle the isKilled check`() {
|
||||
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
|
||||
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
|
||||
alice.rpc.let { rpc ->
|
||||
val handle = rpc.startFlow(::AFlowThatWantsToDie)
|
||||
AFlowThatWantsToDie.lockA.acquire()
|
||||
rpc.killFlow(handle.id)
|
||||
AFlowThatWantsToDie.lockB.release()
|
||||
assertThatExceptionOfType(KilledFlowException::class.java)
|
||||
.isThrownBy { handle.returnValue.getOrThrow(1.minutes) }
|
||||
.withMessage(EXCEPTION_MESSAGE)
|
||||
assertEquals(11, AFlowThatWantsToDie.position)
|
||||
val checkpoints = rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
|
||||
assertEquals(1, checkpoints)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `manually handled killed flows propagate error to counter parties`() {
|
||||
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
|
||||
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
|
||||
val bob = startNode(providedName = BOB_NAME).getOrThrow()
|
||||
val charlie = startNode(providedName = CHARLIE_NAME).getOrThrow()
|
||||
alice.rpc.let { rpc ->
|
||||
val handle = rpc.startFlow(
|
||||
::AFlowThatWantsToDieAndKillsItsFriends,
|
||||
listOf(bob.nodeInfo.singleIdentity(), charlie.nodeInfo.singleIdentity())
|
||||
)
|
||||
AFlowThatWantsToDieAndKillsItsFriends.lockA.acquire()
|
||||
AFlowThatWantsToDieAndKillsItsFriendsResponder.locks.forEach { it.value.acquire() }
|
||||
rpc.killFlow(handle.id)
|
||||
AFlowThatWantsToDieAndKillsItsFriends.lockB.release()
|
||||
assertThatExceptionOfType(KilledFlowException::class.java)
|
||||
.isThrownBy { handle.returnValue.getOrThrow(1.minutes) }
|
||||
.withMessage(EXCEPTION_MESSAGE)
|
||||
AFlowThatWantsToDieAndKillsItsFriendsResponder.locks.forEach { it.value.acquire() }
|
||||
assertEquals(11, AFlowThatWantsToDieAndKillsItsFriends.position)
|
||||
assertTrue(AFlowThatWantsToDieAndKillsItsFriendsResponder.receivedKilledExceptions[BOB_NAME]!!)
|
||||
assertTrue(AFlowThatWantsToDieAndKillsItsFriendsResponder.receivedKilledExceptions[CHARLIE_NAME]!!)
|
||||
val aliceCheckpoints = alice.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
|
||||
assertEquals(1, aliceCheckpoints)
|
||||
val bobCheckpoints = bob.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
|
||||
assertEquals(1, bobCheckpoints)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `a manually killed initiated flow will propagate the killed error to the initiator and its counter parties`() {
|
||||
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
|
||||
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
|
||||
val bob = startNode(providedName = BOB_NAME).getOrThrow()
|
||||
val handle = alice.rpc.startFlow(
|
||||
::AFlowThatGetsMurderedByItsFriend,
|
||||
bob.nodeInfo.singleIdentity()
|
||||
)
|
||||
|
||||
AFlowThatGetsMurderedByItsFriendResponder.lockA.acquire()
|
||||
|
||||
val initiatedFlowId = AFlowThatGetsMurderedByItsFriendResponder.flowId!!
|
||||
|
||||
bob.rpc.killFlow(initiatedFlowId)
|
||||
|
||||
AFlowThatGetsMurderedByItsFriendResponder.lockB.release()
|
||||
|
||||
assertFailsWith<UnexpectedFlowEndException> {
|
||||
handle.returnValue.getOrThrow(1.minutes)
|
||||
}
|
||||
assertTrue(AFlowThatGetsMurderedByItsFriend.receivedKilledException)
|
||||
assertEquals(11, AFlowThatGetsMurderedByItsFriendResponder.position)
|
||||
val aliceCheckpoints = alice.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
|
||||
assertEquals(1, aliceCheckpoints)
|
||||
val bobCheckpoints = bob.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
|
||||
assertEquals(1, bobCheckpoints)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `manually handle killed flows using checkForIsNotKilled`() {
|
||||
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
|
||||
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
|
||||
alice.rpc.let { rpc ->
|
||||
val handle = rpc.startFlow(::AFlowThatChecksIfItWantsToDie)
|
||||
AFlowThatChecksIfItWantsToDie.lockA.acquire()
|
||||
rpc.killFlow(handle.id)
|
||||
AFlowThatChecksIfItWantsToDie.lockB.release()
|
||||
assertThatExceptionOfType(KilledFlowException::class.java)
|
||||
.isThrownBy { handle.returnValue.getOrThrow(1.minutes) }
|
||||
.withMessageNotContaining(EXCEPTION_MESSAGE)
|
||||
assertEquals(11, AFlowThatChecksIfItWantsToDie.position)
|
||||
val checkpoints = rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
|
||||
assertEquals(1, checkpoints)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `manually handle killed flows using checkForIsNotKilled with lazy message`() {
|
||||
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
|
||||
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
|
||||
alice.rpc.let { rpc ->
|
||||
val handle = rpc.startFlow(::AFlowThatChecksIfItWantsToDieAndLeavesANote)
|
||||
AFlowThatChecksIfItWantsToDieAndLeavesANote.lockA.acquire()
|
||||
rpc.killFlow(handle.id)
|
||||
AFlowThatChecksIfItWantsToDieAndLeavesANote.lockB.release()
|
||||
assertThatExceptionOfType(KilledFlowException::class.java)
|
||||
.isThrownBy { handle.returnValue.getOrThrow(1.minutes) }
|
||||
.withMessage(EXCEPTION_MESSAGE)
|
||||
assertEquals(11, AFlowThatChecksIfItWantsToDieAndLeavesANote.position)
|
||||
val checkpoints = rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
|
||||
assertEquals(1, checkpoints)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
class AFlowThatWantsToDie : FlowLogic<Unit>() {
|
||||
|
||||
companion object {
|
||||
val lockA = Semaphore(0)
|
||||
val lockB = Semaphore(0)
|
||||
|
||||
var position = 0
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
for (i in 0..100) {
|
||||
position = i
|
||||
logger.info("i = $i")
|
||||
if (isKilled) {
|
||||
throw KilledFlowException(runId, EXCEPTION_MESSAGE)
|
||||
}
|
||||
|
||||
if (i == 10) {
|
||||
lockA.release()
|
||||
lockB.acquire()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class AFlowThatWantsToDieAndKillsItsFriends(private val parties: List<Party>) : FlowLogic<Unit>() {
|
||||
|
||||
companion object {
|
||||
val lockA = Semaphore(0)
|
||||
val lockB = Semaphore(0)
|
||||
var position = 0
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val sessionOne = initiateFlow(parties[0])
|
||||
val sessionTwo = initiateFlow(parties[1])
|
||||
// trigger sessions with 2 counter parties
|
||||
sessionOne.sendAndReceive<String>("what is up")
|
||||
sessionOne.send("what is up 2")
|
||||
sessionTwo.sendAndReceive<String>("what is up")
|
||||
sessionTwo.send("what is up 2")
|
||||
for (i in 0..100) {
|
||||
position = i
|
||||
logger.info("i = $i")
|
||||
if (isKilled) {
|
||||
throw KilledFlowException(runId, EXCEPTION_MESSAGE)
|
||||
}
|
||||
|
||||
if (i == 10) {
|
||||
lockA.release()
|
||||
lockB.acquire()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(AFlowThatWantsToDieAndKillsItsFriends::class)
|
||||
class AFlowThatWantsToDieAndKillsItsFriendsResponder(private val session: FlowSession) : FlowLogic<Unit>() {
|
||||
|
||||
companion object {
|
||||
val locks = mapOf(
|
||||
BOB_NAME to Semaphore(0),
|
||||
CHARLIE_NAME to Semaphore(0)
|
||||
)
|
||||
var receivedKilledExceptions = mutableMapOf(
|
||||
BOB_NAME to false,
|
||||
CHARLIE_NAME to false
|
||||
)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
session.receive<String>()
|
||||
session.send("hi")
|
||||
session.receive<String>()
|
||||
locks[ourIdentity.name]!!.release()
|
||||
try {
|
||||
session.receive<String>()
|
||||
} catch (e: UnexpectedFlowEndException) {
|
||||
receivedKilledExceptions[ourIdentity.name] = true
|
||||
locks[ourIdentity.name]!!.release()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class AFlowThatGetsMurderedByItsFriend(private val party: Party) : FlowLogic<Unit>() {
|
||||
|
||||
companion object {
|
||||
var receivedKilledException = false
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val sessionOne = initiateFlow(party)
|
||||
// trigger sessions with 2 counter parties
|
||||
sessionOne.sendAndReceive<String>("what is up")
|
||||
try {
|
||||
sessionOne.receive<String>()
|
||||
} catch (e: UnexpectedFlowEndException) {
|
||||
receivedKilledException = true
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(AFlowThatGetsMurderedByItsFriend::class)
|
||||
class AFlowThatGetsMurderedByItsFriendResponder(private val session: FlowSession) : FlowLogic<Unit>() {
|
||||
|
||||
companion object {
|
||||
val lockA = Semaphore(0)
|
||||
val lockB = Semaphore(0)
|
||||
var flowId: StateMachineRunId? = null
|
||||
var position = 0
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
flowId = runId
|
||||
session.receive<String>()
|
||||
session.send("hi")
|
||||
for (i in 0..100) {
|
||||
position = i
|
||||
if (isKilled) {
|
||||
throw KilledFlowException(runId, EXCEPTION_MESSAGE)
|
||||
}
|
||||
|
||||
if (i == 10) {
|
||||
lockA.release()
|
||||
lockB.acquire()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
class AFlowThatChecksIfItWantsToDie : FlowLogic<Unit>() {
|
||||
|
||||
companion object {
|
||||
val lockA = Semaphore(0)
|
||||
val lockB = Semaphore(0)
|
||||
|
||||
var position = 0
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
for (i in 0..100) {
|
||||
position = i
|
||||
logger.info("i = $i")
|
||||
checkFlowIsNotKilled()
|
||||
|
||||
if (i == 10) {
|
||||
lockA.release()
|
||||
lockB.acquire()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
class AFlowThatChecksIfItWantsToDieAndLeavesANote : FlowLogic<Unit>() {
|
||||
|
||||
companion object {
|
||||
val lockA = Semaphore(0)
|
||||
val lockB = Semaphore(0)
|
||||
|
||||
var position = 0
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
for (i in 0..100) {
|
||||
position = i
|
||||
logger.info("i = $i")
|
||||
checkFlowIsNotKilled { EXCEPTION_MESSAGE }
|
||||
|
||||
if (i == 10) {
|
||||
lockA.release()
|
||||
lockB.acquire()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
class GetNumberOfCheckpointsFlow : FlowLogic<Long>() {
|
||||
override fun call(): Long {
|
||||
return serviceHub.jdbcSession().prepareStatement("select count(*) from node_checkpoints").use { ps ->
|
||||
ps.executeQuery().use { rs ->
|
||||
rs.next()
|
||||
rs.getLong(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
package net.corda.coretests.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
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.StartableByRPC
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.minutes
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.driver
|
||||
import org.apache.logging.log4j.Level
|
||||
import org.apache.logging.log4j.core.config.Configurator
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class FlowSleepTest {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
Configurator.setLevel("net.corda.node.services.statemachine", Level.DEBUG)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `flow can sleep`() {
|
||||
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
|
||||
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
|
||||
val (start, finish) = alice.rpc.startFlow(::SleepyFlow).returnValue.getOrThrow(1.minutes)
|
||||
val difference = Duration.between(start, finish)
|
||||
assertTrue(difference >= 5.seconds)
|
||||
assertTrue(difference < 7.seconds)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `flow can sleep multiple times`() {
|
||||
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
|
||||
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
|
||||
val (start, middle, finish) = alice.rpc.startFlow(::AnotherSleepyFlow).returnValue.getOrThrow(1.minutes)
|
||||
val differenceBetweenStartAndMiddle = Duration.between(start, middle)
|
||||
val differenceBetweenMiddleAndFinish = Duration.between(middle, finish)
|
||||
assertTrue(differenceBetweenStartAndMiddle >= 5.seconds)
|
||||
assertTrue(differenceBetweenStartAndMiddle < 7.seconds)
|
||||
assertTrue(differenceBetweenMiddleAndFinish >= 10.seconds)
|
||||
assertTrue(differenceBetweenMiddleAndFinish < 12.seconds)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `flow can sleep and perform other suspending functions`() {
|
||||
// ensures that events received while the flow is sleeping are not processed
|
||||
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
|
||||
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
|
||||
val bob = startNode(providedName = BOB_NAME).getOrThrow()
|
||||
val (start, finish) = alice.rpc.startFlow(
|
||||
::SleepAndInteractWithPartyFlow,
|
||||
bob.nodeInfo.singleIdentity()
|
||||
).returnValue.getOrThrow(1.minutes)
|
||||
val difference = Duration.between(start, finish)
|
||||
assertTrue(difference >= 5.seconds)
|
||||
assertTrue(difference < 7.seconds)
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
class SleepyFlow : FlowLogic<Pair<Instant, Instant>>() {
|
||||
|
||||
@Suspendable
|
||||
override fun call(): Pair<Instant, Instant> {
|
||||
val start = Instant.now()
|
||||
sleep(5.seconds)
|
||||
return start to Instant.now()
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
class AnotherSleepyFlow : FlowLogic<Triple<Instant, Instant, Instant>>() {
|
||||
|
||||
@Suspendable
|
||||
override fun call(): Triple<Instant, Instant, Instant> {
|
||||
val start = Instant.now()
|
||||
sleep(5.seconds)
|
||||
val middle = Instant.now()
|
||||
sleep(10.seconds)
|
||||
return Triple(start, middle, Instant.now())
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class SleepAndInteractWithPartyFlow(private val party: Party) : FlowLogic<Pair<Instant, Instant>>() {
|
||||
|
||||
@Suspendable
|
||||
override fun call(): Pair<Instant, Instant> {
|
||||
subFlow(PingPongFlow(party))
|
||||
val start = Instant.now()
|
||||
sleep(5.seconds)
|
||||
val finish = Instant.now()
|
||||
val session = initiateFlow(party)
|
||||
session.sendAndReceive<String>("hi")
|
||||
session.sendAndReceive<String>("hi")
|
||||
subFlow(PingPongFlow(party))
|
||||
return start to finish
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(SleepAndInteractWithPartyFlow::class)
|
||||
class SleepAndInteractWithPartyResponder(val session: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
session.receive<String>().unwrap { it }
|
||||
session.send("go away")
|
||||
session.receive<String>().unwrap { it }
|
||||
session.send("go away")
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
class PingPongFlow(val party: Party) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val session = initiateFlow(party)
|
||||
session.sendAndReceive<String>("ping pong").unwrap { it }
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(PingPongFlow::class)
|
||||
class PingPongResponder(val session: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
session.receive<String>().unwrap { it }
|
||||
session.send("I got you bro")
|
||||
}
|
||||
}
|
||||
}
|
@ -29,6 +29,7 @@ import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
|
||||
class TransactionBuilderTest {
|
||||
@Rule
|
||||
@ -150,4 +151,100 @@ class TransactionBuilderTest {
|
||||
|
||||
override val signerKeys: List<PublicKey> get() = parties.map { it.owningKey }
|
||||
}, DummyContract.PROGRAM_ID, signerKeys = parties.map { it.owningKey })
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `list accessors are mutable copies`() {
|
||||
val inputState1 = TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary)
|
||||
val inputStateRef1 = StateRef(SecureHash.randomSHA256(), 0)
|
||||
val referenceState1 = TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary)
|
||||
val referenceStateRef1 = StateRef(SecureHash.randomSHA256(), 1)
|
||||
val builder = TransactionBuilder(notary)
|
||||
.addInputState(StateAndRef(inputState1, inputStateRef1))
|
||||
.addAttachment(SecureHash.allOnesHash)
|
||||
.addOutputState(TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary))
|
||||
.addCommand(DummyCommandData, notary.owningKey)
|
||||
.addReferenceState(StateAndRef(referenceState1, referenceStateRef1).referenced())
|
||||
val inputStateRef2 = StateRef(SecureHash.randomSHA256(), 0)
|
||||
val referenceStateRef2 = StateRef(SecureHash.randomSHA256(), 1)
|
||||
|
||||
// List accessors are mutable.
|
||||
assertThat((builder.inputStates() as ArrayList).also { it.add(inputStateRef2) }).hasSize(2)
|
||||
assertThat((builder.attachments() as ArrayList).also { it.add(SecureHash.zeroHash) }).hasSize(2)
|
||||
assertThat((builder.outputStates() as ArrayList).also { it.add(TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary)) }).hasSize(2)
|
||||
assertThat((builder.commands() as ArrayList).also { it.add(Command(DummyCommandData, notary.owningKey)) }).hasSize(2)
|
||||
assertThat((builder.referenceStates() as ArrayList).also { it.add(referenceStateRef2) }).hasSize(2)
|
||||
|
||||
// List accessors are copies.
|
||||
assertThat(builder.inputStates()).hasSize(1)
|
||||
assertThat(builder.attachments()).hasSize(1)
|
||||
assertThat(builder.outputStates()).hasSize(1)
|
||||
assertThat(builder.commands()).hasSize(1)
|
||||
assertThat(builder.referenceStates()).hasSize(1)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `copy makes copy except lockId`() {
|
||||
val inputState = TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary)
|
||||
val inputStateRef = StateRef(SecureHash.randomSHA256(), 0)
|
||||
val referenceState = TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary)
|
||||
val referenceStateRef = StateRef(SecureHash.randomSHA256(), 1)
|
||||
val timeWindow = TimeWindow.untilOnly(Instant.now())
|
||||
val builder = TransactionBuilder(notary)
|
||||
.addInputState(StateAndRef(inputState, inputStateRef))
|
||||
.addAttachment(SecureHash.allOnesHash)
|
||||
.addOutputState(TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary))
|
||||
.addCommand(DummyCommandData, notary.owningKey)
|
||||
.setTimeWindow(timeWindow)
|
||||
.setPrivacySalt(PrivacySalt())
|
||||
.addReferenceState(StateAndRef(referenceState, referenceStateRef).referenced())
|
||||
val copy = builder.copy()
|
||||
|
||||
assertThat(builder.notary).isEqualTo(copy.notary)
|
||||
assertThat(builder.lockId).isNotEqualTo(copy.lockId)
|
||||
assertThat(builder.inputStates()).isEqualTo(copy.inputStates())
|
||||
assertThat(builder.attachments()).isEqualTo(copy.attachments())
|
||||
assertThat(builder.outputStates()).isEqualTo(copy.outputStates())
|
||||
assertThat(builder.commands()).isEqualTo(copy.commands())
|
||||
// assertThat(builder.timeWindow()).isEqualTo(copy.timeWindow())
|
||||
// assertThat(builder.privacySalt()).isEqualTo(copy.privacySalt())
|
||||
assertThat(builder.referenceStates()).isEqualTo(copy.referenceStates())
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `copy makes deep copy of lists`() {
|
||||
val inputState1 = TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary)
|
||||
val inputStateRef1 = StateRef(SecureHash.randomSHA256(), 0)
|
||||
val referenceState1 = TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary)
|
||||
val referenceStateRef1 = StateRef(SecureHash.randomSHA256(), 1)
|
||||
val builder = TransactionBuilder(notary)
|
||||
.addInputState(StateAndRef(inputState1, inputStateRef1))
|
||||
.addAttachment(SecureHash.allOnesHash)
|
||||
.addOutputState(TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary))
|
||||
.addCommand(DummyCommandData, notary.owningKey)
|
||||
.addReferenceState(StateAndRef(referenceState1, referenceStateRef1).referenced())
|
||||
val inputState2 = TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary)
|
||||
val inputStateRef2 = StateRef(SecureHash.randomSHA256(), 0)
|
||||
val referenceState2 = TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary)
|
||||
val referenceStateRef2 = StateRef(SecureHash.randomSHA256(), 1)
|
||||
val copy = builder.copy()
|
||||
.addInputState(StateAndRef(inputState2, inputStateRef2))
|
||||
.addAttachment(SecureHash.zeroHash)
|
||||
.addOutputState(TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary))
|
||||
.addCommand(DummyCommandData, notary.owningKey)
|
||||
.addReferenceState(StateAndRef(referenceState2, referenceStateRef2).referenced())
|
||||
|
||||
// Lists on the copy are longer
|
||||
assertThat(copy.inputStates()).hasSize(2)
|
||||
assertThat(copy.attachments()).hasSize(2)
|
||||
assertThat(copy.outputStates()).hasSize(2)
|
||||
assertThat(copy.commands()).hasSize(2)
|
||||
assertThat(copy.referenceStates()).hasSize(2)
|
||||
|
||||
// Lists on the original are unchanged
|
||||
assertThat(builder.inputStates()).hasSize(1)
|
||||
assertThat(builder.attachments()).hasSize(1)
|
||||
assertThat(builder.outputStates()).hasSize(1)
|
||||
assertThat(builder.commands()).hasSize(1)
|
||||
assertThat(builder.referenceStates()).hasSize(1)
|
||||
}
|
||||
}
|
||||
|
@ -101,7 +101,8 @@ task copyQuasarJar(type: Copy) {
|
||||
|
||||
jar {
|
||||
finalizedBy(copyQuasarJar)
|
||||
baseName 'corda-core'
|
||||
archiveBaseName = 'corda-core'
|
||||
archiveClassifier = ''
|
||||
}
|
||||
|
||||
configurations {
|
||||
|
@ -49,7 +49,7 @@ interface Attachment : NamedByHash {
|
||||
|
||||
/**
|
||||
* Finds the named file case insensitively and copies it to the output stream.
|
||||
* @throws FileNotFoundException if the given path doesn't exist in the attachment.
|
||||
* @throws [FileNotFoundException] if the given path doesn't exist in the attachment.
|
||||
*/
|
||||
@JvmDefault
|
||||
fun extractFile(path: String, outputTo: OutputStream) = openAsJAR().use { it.extractFile(path, outputTo) }
|
||||
|
@ -337,6 +337,8 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S
|
||||
class InvalidAttachmentException(txId: SecureHash, @Suppress("unused") val attachmentHash: AttachmentId) : TransactionVerificationException(txId,
|
||||
"The attachment $attachmentHash is not a valid ZIP or JAR file.".trimIndent(), null)
|
||||
|
||||
class UnsupportedClassVersionError(txId: SecureHash, message: String, cause: Throwable) : TransactionVerificationException(txId, message, cause)
|
||||
|
||||
// TODO: Make this descend from TransactionVerificationException so that untrusted attachments cause flows to be hospitalized.
|
||||
/** Thrown during classloading upon encountering an untrusted attachment (eg. not in the [TRUSTED_UPLOADERS] list) */
|
||||
@KeepForDJVM
|
||||
|
@ -3,7 +3,6 @@ package net.corda.core.crypto
|
||||
import net.corda.core.CordaOID
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.StubOutForDJVM
|
||||
import net.corda.core.crypto.internal.AliasPrivateKey
|
||||
import net.corda.core.crypto.internal.Instances.withSignature
|
||||
import net.corda.core.crypto.internal.`id-Curve25519ph`
|
||||
@ -108,7 +107,7 @@ object Crypto {
|
||||
AlgorithmIdentifier(X9ObjectIdentifiers.ecdsa_with_SHA256, SECObjectIdentifiers.secp256k1),
|
||||
listOf(AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, SECObjectIdentifiers.secp256k1)),
|
||||
cordaBouncyCastleProvider.name,
|
||||
"ECDSA",
|
||||
"EC",
|
||||
"SHA256withECDSA",
|
||||
ECNamedCurveTable.getParameterSpec("secp256k1"),
|
||||
256,
|
||||
@ -123,7 +122,7 @@ object Crypto {
|
||||
AlgorithmIdentifier(X9ObjectIdentifiers.ecdsa_with_SHA256, SECObjectIdentifiers.secp256r1),
|
||||
listOf(AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, SECObjectIdentifiers.secp256r1)),
|
||||
cordaBouncyCastleProvider.name,
|
||||
"ECDSA",
|
||||
"EC",
|
||||
"SHA256withECDSA",
|
||||
ECNamedCurveTable.getParameterSpec("secp256r1"),
|
||||
256,
|
||||
@ -220,11 +219,12 @@ object Crypto {
|
||||
* Map of supported digital signature schemes associated by [SignatureScheme.schemeNumberID].
|
||||
* SchemeNumberID is the scheme identifier attached to [SignatureMetadata].
|
||||
*/
|
||||
private val signatureSchemeNumberIDMap: Map<Int, SignatureScheme> = Crypto.supportedSignatureSchemes().associateBy { it.schemeNumberID }
|
||||
private val signatureSchemeNumberIDMap: Map<Int, SignatureScheme> = supportedSignatureSchemes().associateBy { it.schemeNumberID }
|
||||
|
||||
@JvmStatic
|
||||
fun supportedSignatureSchemes(): List<SignatureScheme> = ArrayList(signatureSchemeMap.values)
|
||||
|
||||
@DeleteForDJVM
|
||||
@JvmStatic
|
||||
fun findProvider(name: String): Provider {
|
||||
return providerMap[name] ?: throw IllegalArgumentException("Unrecognised provider: $name")
|
||||
@ -303,6 +303,7 @@ object Crypto {
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for this key factory to produce a private key.
|
||||
*/
|
||||
@DeleteForDJVM
|
||||
@JvmStatic
|
||||
fun decodePrivateKey(encodedKey: ByteArray): PrivateKey {
|
||||
val keyInfo = PrivateKeyInfo.getInstance(encodedKey)
|
||||
@ -314,6 +315,7 @@ object Crypto {
|
||||
return keyFactory.generatePrivate(PKCS8EncodedKeySpec(encodedKey))
|
||||
}
|
||||
|
||||
@DeleteForDJVM
|
||||
private fun decodeAliasPrivateKey(keyInfo: PrivateKeyInfo): PrivateKey {
|
||||
val encodable = keyInfo.parsePrivateKey() as DLSequence
|
||||
val derutF8String = encodable.getObjectAt(0)
|
||||
@ -329,6 +331,7 @@ object Crypto {
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for this key factory to produce a private key.
|
||||
*/
|
||||
@DeleteForDJVM
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeySpecException::class)
|
||||
fun decodePrivateKey(schemeCodeName: String, encodedKey: ByteArray): PrivateKey {
|
||||
@ -407,7 +410,7 @@ object Crypto {
|
||||
val keyFactory = keyFactory(signatureScheme)
|
||||
return keyFactory.generatePublic(X509EncodedKeySpec(encodedKey))
|
||||
} catch (ikse: InvalidKeySpecException) {
|
||||
throw throw InvalidKeySpecException("This public key cannot be decoded, please ensure it is X509 encoded and " +
|
||||
throw InvalidKeySpecException("This public key cannot be decoded, please ensure it is X509 encoded and " +
|
||||
"that it corresponds to the input scheme's code name.", ikse)
|
||||
}
|
||||
}
|
||||
@ -499,14 +502,14 @@ object Crypto {
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun doSign(keyPair: KeyPair, signableData: SignableData): TransactionSignature {
|
||||
val sigKey: SignatureScheme = Crypto.findSignatureScheme(keyPair.private)
|
||||
val sigMetaData: SignatureScheme = Crypto.findSignatureScheme(signableData.signatureMetadata.schemeNumberID)
|
||||
val sigKey: SignatureScheme = findSignatureScheme(keyPair.private)
|
||||
val sigMetaData: SignatureScheme = findSignatureScheme(signableData.signatureMetadata.schemeNumberID)
|
||||
// Special handling if the advertised SignatureScheme is CompositeKey.
|
||||
// TODO fix notaries that advertise [CompositeKey] in their signature Metadata. Currently, clustered notary nodes
|
||||
// mention Crypto.COMPOSITE_KEY in their SignatureMetadata, but they are actually signing with a leaf-key
|
||||
// (and if they refer to it as a Composite key, then we lose info about the actual type of their signing key).
|
||||
// In short, their metadata should be the leaf key-type, until we support CompositeKey signatures.
|
||||
require(sigKey == sigMetaData || sigMetaData == Crypto.COMPOSITE_KEY) {
|
||||
require(sigKey == sigMetaData || sigMetaData == COMPOSITE_KEY) {
|
||||
"Metadata schemeCodeName: ${sigMetaData.schemeCodeName} is not aligned with the key type: ${sigKey.schemeCodeName}."
|
||||
}
|
||||
val signatureBytes = doSign(sigKey.schemeCodeName, keyPair.private, signableData.serialize().bytes)
|
||||
@ -601,7 +604,7 @@ object Crypto {
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun doVerify(txId: SecureHash, transactionSignature: TransactionSignature): Boolean {
|
||||
val signableData = SignableData(originalSignedHash(txId, transactionSignature.partialMerkleTree), transactionSignature.signatureMetadata)
|
||||
return Crypto.doVerify(transactionSignature.by, transactionSignature.bytes, signableData.serialize().bytes)
|
||||
return doVerify(transactionSignature.by, transactionSignature.bytes, signableData.serialize().bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -975,15 +978,6 @@ object Crypto {
|
||||
@JvmStatic
|
||||
fun validatePublicKey(key: PublicKey): Boolean = validatePublicKey(findSignatureScheme(key), key)
|
||||
|
||||
// Validate a key, by checking its algorithmic params.
|
||||
private fun validateKey(signatureScheme: SignatureScheme, key: Key): Boolean {
|
||||
return when (key) {
|
||||
is PublicKey -> validatePublicKey(signatureScheme, key)
|
||||
is PrivateKey -> validatePrivateKey(signatureScheme, key)
|
||||
else -> throw IllegalArgumentException("Unsupported key type: ${key::class}")
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a public key satisfies algorithm specs (for ECC: key should lie on the curve and not being point-at-infinity).
|
||||
private fun validatePublicKey(signatureScheme: SignatureScheme, key: PublicKey): Boolean {
|
||||
return when (key) {
|
||||
@ -994,16 +988,6 @@ object Crypto {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a private key satisfies algorithm specs.
|
||||
private fun validatePrivateKey(signatureScheme: SignatureScheme, key: PrivateKey): Boolean {
|
||||
return when (key) {
|
||||
is BCECPrivateKey -> key.parameters == signatureScheme.algSpec
|
||||
is EdDSAPrivateKey -> key.params == signatureScheme.algSpec
|
||||
is BCRSAPrivateKey, is BCSphincs256PrivateKey -> true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size).
|
||||
else -> throw IllegalArgumentException("Unsupported key type: ${key::class}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a public key to a supported implementation.
|
||||
* @param key a public key.
|
||||
@ -1043,6 +1027,7 @@ object Crypto {
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for a supported key factory to produce a private key.
|
||||
*/
|
||||
@DeleteForDJVM
|
||||
@JvmStatic
|
||||
fun toSupportedPrivateKey(key: PrivateKey): PrivateKey {
|
||||
return when (key) {
|
||||
@ -1078,6 +1063,7 @@ object Crypto {
|
||||
* CRL & CSR checks etc.).
|
||||
*/
|
||||
// TODO: perform all cryptographic operations via Crypto.
|
||||
@DeleteForDJVM
|
||||
@JvmStatic
|
||||
fun registerProviders() {
|
||||
providerMap
|
||||
@ -1088,7 +1074,7 @@ object Crypto {
|
||||
setBouncyCastleRNG()
|
||||
}
|
||||
|
||||
@StubOutForDJVM
|
||||
@DeleteForDJVM
|
||||
private fun setBouncyCastleRNG() {
|
||||
CryptoServicesRegistrar.setSecureRandom(newSecureRandom())
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.security.PublicKey
|
||||
|
||||
@KeepForDJVM
|
||||
|
@ -15,8 +15,10 @@ import org.bouncycastle.jcajce.provider.asymmetric.ec.AlgorithmParametersSpi
|
||||
import org.bouncycastle.jcajce.provider.util.AsymmetricKeyInfoConverter
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider
|
||||
import java.security.Provider
|
||||
import java.security.SecureRandom
|
||||
import java.security.Security
|
||||
import java.util.Collections.unmodifiableMap
|
||||
|
||||
val cordaSecurityProvider = CordaSecurityProvider().also {
|
||||
// Among the others, we should register [CordaSecurityProvider] as the first provider, to ensure that when invoking [SecureRandom()]
|
||||
@ -52,8 +54,11 @@ val bouncyCastlePQCProvider = BouncyCastlePQCProvider().apply {
|
||||
// This map is required to defend against users that forcibly call Security.addProvider / Security.removeProvider
|
||||
// that could cause unexpected and suspicious behaviour.
|
||||
// i.e. if someone removes a Provider and then he/she adds a new one with the same name.
|
||||
// The val is private to avoid any harmful state changes.
|
||||
val providerMap = listOf(cordaBouncyCastleProvider, cordaSecurityProvider, bouncyCastlePQCProvider).map { it.name to it }.toMap()
|
||||
// The val is immutable to avoid any harmful state changes.
|
||||
internal val providerMap: Map<String, Provider> = unmodifiableMap(
|
||||
listOf(cordaBouncyCastleProvider, cordaSecurityProvider, bouncyCastlePQCProvider)
|
||||
.associateByTo(LinkedHashMap(), Provider::getName)
|
||||
)
|
||||
|
||||
@DeleteForDJVM
|
||||
fun platformSecureRandomFactory(): SecureRandom = platformSecureRandom() // To minimise diff of CryptoUtils against open-source.
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.CordaInternal
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.identity.Party
|
||||
@ -45,6 +46,13 @@ class FinalityFlow private constructor(val transaction: SignedTransaction,
|
||||
private val sessions: Collection<FlowSession>,
|
||||
private val newApi: Boolean,
|
||||
private val statesToRecord: StatesToRecord = ONLY_RELEVANT) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
@CordaInternal
|
||||
data class ExtraConstructorArgs(val oldParticipants: Collection<Party>, val sessions: Collection<FlowSession>, val newApi: Boolean, val statesToRecord: StatesToRecord)
|
||||
|
||||
@CordaInternal
|
||||
fun getExtraConstructorArgs() = ExtraConstructorArgs(oldParticipants, sessions, newApi, statesToRecord)
|
||||
|
||||
@Deprecated(DEPRECATION_MSG)
|
||||
constructor(transaction: SignedTransaction, extraRecipients: Set<Party>, progressTracker: ProgressTracker) : this(
|
||||
transaction, extraRecipients, progressTracker, emptyList(), false
|
||||
|
@ -1,7 +1,5 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import net.corda.core.internal.ServiceHubCoreInternal
|
||||
import net.corda.core.node.ServiceHub
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
/**
|
||||
|
@ -24,9 +24,6 @@ import net.corda.core.messaging.DataFeed
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
@ -130,6 +127,32 @@ abstract class FlowLogic<out T> {
|
||||
*/
|
||||
val serviceHub: ServiceHub get() = stateMachine.serviceHub
|
||||
|
||||
/**
|
||||
* Returns `true` when the current [FlowLogic] has been killed (has received a command to halt its progress and terminate).
|
||||
*
|
||||
* Check this property in long-running computation loops to exit a flow that has been killed:
|
||||
* ```
|
||||
* while (!isKilled) {
|
||||
* // do some computation
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Ideal usage would include throwing a [KilledFlowException] which will lead to the termination of the flow:
|
||||
* ```
|
||||
* for (item in list) {
|
||||
* if (isKilled) {
|
||||
* throw KilledFlowException(runId)
|
||||
* }
|
||||
* // do some computation
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Note, once the [isKilled] flag is set to `true` the flow may terminate once it reaches the next API function marked with the
|
||||
* @[Suspendable] annotation. Therefore, it is possible to write a flow that does not interact with the [isKilled] flag while still
|
||||
* terminating correctly.
|
||||
*/
|
||||
val isKilled: Boolean get() = stateMachine.isKilled
|
||||
|
||||
/**
|
||||
* Creates a communication session with [destination]. Subsequently you may send/receive using this session object. How the messaging
|
||||
* is routed depends on the [Destination] type, including whether this call does any initial communication.
|
||||
@ -267,7 +290,7 @@ abstract class FlowLogic<out T> {
|
||||
@Suspendable
|
||||
internal fun <R : Any> FlowSession.sendAndReceiveWithRetry(receiveType: Class<R>, payload: Any): UntrustworthyData<R> {
|
||||
val request = FlowIORequest.SendAndReceive(
|
||||
sessionToMessage = mapOf(this to payload.serialize(context = SerializationDefaults.P2P_CONTEXT)),
|
||||
sessionToMessage = stateMachine.serialize(mapOf(this to payload)),
|
||||
shouldRetrySend = true
|
||||
)
|
||||
return stateMachine.suspend(request, maySkipCheckpoint = false)[this]!!.checkPayloadIs(receiveType)
|
||||
@ -350,21 +373,11 @@ abstract class FlowLogic<out T> {
|
||||
@JvmOverloads
|
||||
fun sendAllMap(payloadsPerSession: Map<FlowSession, Any>, maySkipCheckpoint: Boolean = false) {
|
||||
val request = FlowIORequest.Send(
|
||||
sessionToMessage = serializePayloads(payloadsPerSession)
|
||||
sessionToMessage = stateMachine.serialize(payloadsPerSession)
|
||||
)
|
||||
stateMachine.suspend(request, maySkipCheckpoint)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun serializePayloads(payloadsPerSession: Map<FlowSession, Any>): Map<FlowSession, SerializedBytes<Any>> {
|
||||
val cachedSerializedPayloads = mutableMapOf<Any, SerializedBytes<Any>>()
|
||||
|
||||
return payloadsPerSession.mapValues { (_, payload) ->
|
||||
cachedSerializedPayloads[payload] ?: payload.serialize(context = SerializationDefaults.P2P_CONTEXT).also { cachedSerializedPayloads[payload] = it }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Invokes the given subflow. This function returns once the subflow completes successfully with the result
|
||||
* returned by that subflow's [call] method. If the subflow has a progress tracker, it is attached to the
|
||||
@ -583,6 +596,46 @@ abstract class FlowLogic<out T> {
|
||||
val request = FlowIORequest.ExecuteAsyncOperation(flowAsyncOperation)
|
||||
return stateMachine.suspend(request, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that throws a [KilledFlowException] if the current [FlowLogic] has been killed.
|
||||
*
|
||||
* Call this function in long-running computation loops to exit a flow that has been killed:
|
||||
* ```
|
||||
* for (item in list) {
|
||||
* checkFlowIsNotKilled()
|
||||
* // do some computation
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* See the [isKilled] property for more information.
|
||||
*/
|
||||
fun checkFlowIsNotKilled() {
|
||||
if (isKilled) {
|
||||
throw KilledFlowException(runId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that throws a [KilledFlowException] if the current [FlowLogic] has been killed. The provided message is added to the
|
||||
* thrown [KilledFlowException].
|
||||
*
|
||||
* Call this function in long-running computation loops to exit a flow that has been killed:
|
||||
* ```
|
||||
* for (item in list) {
|
||||
* checkFlowIsNotKilled { "The flow $runId was killed while iterating through the list of items" }
|
||||
* // do some computation
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* See the [isKilled] property for more information.
|
||||
*/
|
||||
fun checkFlowIsNotKilled(lazyMessage: () -> Any) {
|
||||
if (isKilled) {
|
||||
val message = lazyMessage()
|
||||
throw KilledFlowException(runId, message.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,14 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import net.corda.core.CordaRuntimeException
|
||||
|
||||
/**
|
||||
* An exception that is thrown when a flow has been killed.
|
||||
*
|
||||
* This exception can be returned and thrown to RPC clients waiting for the result of a flow's future.
|
||||
*
|
||||
* It can also be used in conjunction with [FlowLogic.isKilled] to escape long-running computation loops when a flow has been killed.
|
||||
*/
|
||||
class KilledFlowException(val id: StateMachineRunId, message: String) : CordaRuntimeException(message) {
|
||||
constructor(id: StateMachineRunId) : this(id, "The flow $id was killed")
|
||||
}
|
@ -9,17 +9,20 @@ import net.corda.core.serialization.internal.AttachmentURLStreamHandlerFactory.a
|
||||
* Creates instances of all the classes in the classpath of the provided classloader, which implement the interface of the provided class.
|
||||
* @param classloader the classloader, which will be searched for the classes.
|
||||
* @param clazz the class of the interface, which the classes - to be returned - must implement.
|
||||
* @param classVersionRange if specified an exception is raised if class version is not within the passed range.
|
||||
*
|
||||
* @return instances of the identified classes.
|
||||
* @throws IllegalArgumentException if the classes found do not have proper constructors.
|
||||
* @throws UnsupportedClassVersionError if the class version is not within range.
|
||||
*
|
||||
* Note: In order to be instantiated, the associated classes must:
|
||||
* - be non-abstract
|
||||
* - either be a Kotlin object or have a constructor with no parameters (or only optional ones)
|
||||
*/
|
||||
@StubOutForDJVM
|
||||
fun <T: Any> createInstancesOfClassesImplementing(classloader: ClassLoader, clazz: Class<T>): Set<T> {
|
||||
return getNamesOfClassesImplementing(classloader, clazz)
|
||||
fun <T: Any> createInstancesOfClassesImplementing(classloader: ClassLoader, clazz: Class<T>,
|
||||
classVersionRange: IntRange? = null): Set<T> {
|
||||
return getNamesOfClassesImplementing(classloader, clazz, classVersionRange)
|
||||
.map { classloader.loadClass(it).asSubclass(clazz) }
|
||||
.mapTo(LinkedHashSet()) { it.kotlin.objectOrNewInstance() }
|
||||
}
|
||||
@ -28,17 +31,26 @@ fun <T: Any> createInstancesOfClassesImplementing(classloader: ClassLoader, claz
|
||||
* Scans for all the non-abstract classes in the classpath of the provided classloader which implement the interface of the provided class.
|
||||
* @param classloader the classloader, which will be searched for the classes.
|
||||
* @param clazz the class of the interface, which the classes - to be returned - must implement.
|
||||
* @param classVersionRange if specified an exception is raised if class version is not within the passed range.
|
||||
*
|
||||
* @return names of the identified classes.
|
||||
* @throws UnsupportedClassVersionError if the class version is not within range.
|
||||
*/
|
||||
@StubOutForDJVM
|
||||
fun <T: Any> getNamesOfClassesImplementing(classloader: ClassLoader, clazz: Class<T>): Set<String> {
|
||||
fun <T: Any> getNamesOfClassesImplementing(classloader: ClassLoader, clazz: Class<T>,
|
||||
classVersionRange: IntRange? = null): Set<String> {
|
||||
return ClassGraph().overrideClassLoaders(classloader)
|
||||
.enableURLScheme(attachmentScheme)
|
||||
.ignoreParentClassLoaders()
|
||||
.enableClassInfo()
|
||||
.pooledScan()
|
||||
.use { result ->
|
||||
classVersionRange?.let {
|
||||
result.allClasses.firstOrNull { c -> c.classfileMajorVersion !in classVersionRange }?.also {
|
||||
throw UnsupportedClassVersionError("Class ${it.name} found in ${it.classpathElementURL} " +
|
||||
"has an unsupported class version of ${it.classfileMajorVersion}")
|
||||
}
|
||||
}
|
||||
result.getClassesImplementing(clazz.name)
|
||||
.filterNot(ClassInfo::isAbstract)
|
||||
.mapTo(LinkedHashSet(), ClassInfo::getName)
|
||||
|
@ -28,7 +28,7 @@ import java.util.jar.JarInputStream
|
||||
|
||||
// *Internal* Corda-specific utilities.
|
||||
|
||||
const val PLATFORM_VERSION = 6
|
||||
const val PLATFORM_VERSION = 7
|
||||
|
||||
fun ServicesForResolution.ensureMinimumPlatformVersion(requiredMinPlatformVersion: Int, feature: String) {
|
||||
checkMinimumPlatformVersion(networkParameters.minimumPlatformVersion, requiredMinPlatformVersion, feature)
|
||||
@ -48,6 +48,20 @@ fun checkMinimumPlatformVersion(minimumPlatformVersion: Int, requiredMinPlatform
|
||||
@Throws(NumberFormatException::class)
|
||||
fun getJavaUpdateVersion(javaVersion: String): Long = javaVersion.substringAfter("_").substringBefore("-").toLong()
|
||||
|
||||
enum class JavaVersion(val versionString: String) {
|
||||
Java_1_8("1.8"),
|
||||
Java_11("11");
|
||||
|
||||
companion object {
|
||||
fun isVersionAtLeast(version: JavaVersion): Boolean {
|
||||
return currentVersion.toFloat() >= version.versionString.toFloat()
|
||||
}
|
||||
|
||||
private val currentVersion: String = System.getProperty("java.specification.version") ?:
|
||||
throw IllegalStateException("Unable to retrieve system property java.specification.version")
|
||||
}
|
||||
}
|
||||
|
||||
/** Provide access to internal method for AttachmentClassLoaderTests. */
|
||||
@DeleteForDJVM
|
||||
fun TransactionBuilder.toWireTransaction(services: ServicesForResolution, serializationContext: SerializationContext): WireTransaction {
|
||||
|
@ -8,6 +8,7 @@ import net.corda.core.context.InvocationContext
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import org.slf4j.Logger
|
||||
|
||||
/** This is an internal interface that is implemented by code in the node module. You should look at [FlowLogic]. */
|
||||
@ -17,6 +18,8 @@ interface FlowStateMachine<FLOWRETURN> {
|
||||
@Suspendable
|
||||
fun <SUSPENDRETURN : Any> suspend(ioRequest: FlowIORequest<SUSPENDRETURN>, maySkipCheckpoint: Boolean): SUSPENDRETURN
|
||||
|
||||
fun serialize(payloads: Map<FlowSession, Any>): Map<FlowSession, SerializedBytes<Any>>
|
||||
|
||||
@Suspendable
|
||||
fun initiateFlow(destination: Destination, wellKnownParty: Party): FlowSession
|
||||
|
||||
@ -44,4 +47,5 @@ interface FlowStateMachine<FLOWRETURN> {
|
||||
val ourIdentity: Party
|
||||
val ourSenderUUID: String?
|
||||
val creationTime: Long
|
||||
val isKilled: Boolean
|
||||
}
|
||||
|
@ -636,3 +636,6 @@ fun Logger.warnOnce(warning: String) {
|
||||
this.warn(warning)
|
||||
}
|
||||
}
|
||||
|
||||
const val JDK1_2_CLASS_FILE_FORMAT_MAJOR_VERSION = 46
|
||||
const val JDK8_CLASS_FILE_FORMAT_MAJOR_VERSION = 52
|
||||
|
@ -0,0 +1,10 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
/*
|
||||
Constants for new features that can only be switched on at specific platform versions can be specified in this file.
|
||||
The text constant describes the feature and the numeric specifies the platform version the feature is enabled at.
|
||||
*/
|
||||
object PlatformVersionSwitches {
|
||||
const val REMOVE_NO_OVERLAP_RULE_FOR_REFERENCE_DATA_ATTACHMENTS = 7
|
||||
const val ENABLE_P2P_COMPRESSION = 7
|
||||
}
|
@ -2,6 +2,7 @@ package net.corda.core.internal
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.internal.notary.NotaryService
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.StatesToRecord
|
||||
import java.util.concurrent.ExecutorService
|
||||
@ -14,6 +15,11 @@ interface ServiceHubCoreInternal : ServiceHub {
|
||||
|
||||
val attachmentTrustCalculator: AttachmentTrustCalculator
|
||||
|
||||
/**
|
||||
* Optional `NotaryService` which will be `null` for all non-Notary nodes.
|
||||
*/
|
||||
val notaryService: NotaryService?
|
||||
|
||||
fun createTransactionsResolver(flow: ResolveTransactionsFlow): TransactionsResolver
|
||||
}
|
||||
|
||||
|
@ -77,7 +77,12 @@ fun <T : Any> deserialiseComponentGroup(componentGroups: List<ComponentGroup>,
|
||||
try {
|
||||
factory.deserialize(component, clazz.java, context)
|
||||
} catch (e: MissingAttachmentsException) {
|
||||
throw e
|
||||
/**
|
||||
* [ServiceHub.signInitialTransaction] forgets to declare that
|
||||
* it may throw any checked exceptions. Wrap this one inside
|
||||
* an unchecked version to avoid breaking Java CorDapps.
|
||||
*/
|
||||
throw MissingAttachmentsRuntimeException(e.ids, e.message, e)
|
||||
} catch (e: Exception) {
|
||||
throw TransactionDeserialisationException(groupEnum, internalIndex, e)
|
||||
}
|
||||
@ -88,7 +93,7 @@ fun <T : Any> deserialiseComponentGroup(componentGroups: List<ComponentGroup>,
|
||||
* Exception raised if an error was encountered while attempting to deserialise a component group in a transaction.
|
||||
*/
|
||||
class TransactionDeserialisationException(groupEnum: ComponentGroupEnum, index: Int, cause: Exception):
|
||||
Exception("Failed to deserialise group $groupEnum at index $index in transaction: ${cause.message}", cause)
|
||||
RuntimeException("Failed to deserialise group $groupEnum at index $index in transaction: ${cause.message}", cause)
|
||||
|
||||
/**
|
||||
* Method to deserialise Commands from its two groups:
|
||||
|
@ -401,8 +401,7 @@ class ContractVerifier(private val transactionClassLoader: ClassLoader) : Functi
|
||||
|
||||
override fun apply(ltx: LedgerTransaction) {
|
||||
val contractClassNames = (ltx.inputs.map(StateAndRef<ContractState>::state) + ltx.outputs)
|
||||
.map(TransactionState<*>::contract)
|
||||
.toSet()
|
||||
.mapTo(LinkedHashSet(), TransactionState<*>::contract)
|
||||
|
||||
contractClassNames.associateBy(
|
||||
{ it }, { createContractClass(ltx.id, it) }
|
||||
@ -410,7 +409,7 @@ class ContractVerifier(private val transactionClassLoader: ClassLoader) : Functi
|
||||
try {
|
||||
/**
|
||||
* This function must execute with the DJVM's sandbox, which does not
|
||||
* permit user code to access [java.lang.reflect.Constructor] objects.
|
||||
* permit user code to invoke [java.lang.Class.getDeclaredConstructor].
|
||||
*
|
||||
* [Class.newInstance] is deprecated as of Java 9.
|
||||
*/
|
||||
|
@ -4,7 +4,6 @@ package net.corda.core.internal
|
||||
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import org.bouncycastle.asn1.ASN1Encodable
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||
import org.bouncycastle.asn1.x500.AttributeTypeAndValue
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
|
@ -14,6 +14,19 @@ abstract class NotaryService : SingletonSerializeAsToken() {
|
||||
abstract val services: ServiceHub
|
||||
abstract val notaryIdentityKey: PublicKey
|
||||
|
||||
/**
|
||||
* Interfaces for the request and result formats of queries supported by notary services. To
|
||||
* implement a new query, you must:
|
||||
*
|
||||
* - Define data classes which implement the [Query.Request] and [Query.Result] interfaces
|
||||
* - Add corresponding handling for the new classes within the notary service implementations
|
||||
* that you want to support the query.
|
||||
*/
|
||||
interface Query {
|
||||
interface Request
|
||||
interface Result
|
||||
}
|
||||
|
||||
abstract fun start()
|
||||
abstract fun stop()
|
||||
|
||||
@ -22,4 +35,18 @@ abstract class NotaryService : SingletonSerializeAsToken() {
|
||||
* @param otherPartySession client [Party] making the request
|
||||
*/
|
||||
abstract fun createServiceFlow(otherPartySession: FlowSession): FlowLogic<Void?>
|
||||
|
||||
/**
|
||||
* Processes a [Query.Request] and returns a [Query.Result].
|
||||
*
|
||||
* Note that this always throws an [UnsupportedOperationException] to handle notary
|
||||
* implementations that do not support this functionality. This must be overridden by
|
||||
* notary implementations wishing to support query functionality.
|
||||
*
|
||||
* Overrides of this function may themselves still throw an [UnsupportedOperationException],
|
||||
* if they do not support specific query implementations
|
||||
*/
|
||||
open fun processQuery(query: Query.Request): Query.Result {
|
||||
throw UnsupportedOperationException("Notary has not implemented query support")
|
||||
}
|
||||
}
|
@ -252,7 +252,7 @@ interface CordaRPCOps : RPCOps {
|
||||
* Note: This operation may be restricted only to node administrators.
|
||||
* @param parametersHash hash of network parameters to accept
|
||||
* @throws IllegalArgumentException if network map advertises update with different parameters hash then the one accepted by node's operator.
|
||||
* @throws IOException if failed to send the approval to network map
|
||||
* @throws [IOException] if failed to send the approval to network map
|
||||
*/
|
||||
// TODO This operation should be restricted to just node admins.
|
||||
fun acceptNewNetworkParameters(parametersHash: SecureHash)
|
||||
|
@ -380,6 +380,26 @@ interface ServiceHub : ServicesForResolution {
|
||||
* When used within a flow, this session automatically forms part of the enclosing flow transaction boundary,
|
||||
* and thus queryable data will include everything committed as of the last checkpoint.
|
||||
*
|
||||
* We want to make sure users have a restricted access to administrative functions, this function will return a [RestrictedConnection] instance.
|
||||
* The following methods are blocked:
|
||||
* - abort(executor: Executor?)
|
||||
* - clearWarnings()
|
||||
* - close()
|
||||
* - commit()
|
||||
* - setSavepoint()
|
||||
* - setSavepoint(name : String?)
|
||||
* - releaseSavepoint(savepoint: Savepoint?)
|
||||
* - rollback()
|
||||
* - rollback(savepoint: Savepoint?)
|
||||
* - setCatalog(catalog : String?)
|
||||
* - setTransactionIsolation(level: Int)
|
||||
* - setTypeMap(map: MutableMap<String, Class<*>>?)
|
||||
* - setHoldability(holdability: Int)
|
||||
* - setSchema(schema: String?)
|
||||
* - setNetworkTimeout(executor: Executor?, milliseconds: Int)
|
||||
* - setAutoCommit(autoCommit: Boolean)
|
||||
* - setReadOnly(readOnly: Boolean)
|
||||
*
|
||||
* @throws IllegalStateException if called outside of a transaction.
|
||||
* @return A [Connection]
|
||||
*/
|
||||
@ -393,6 +413,24 @@ interface ServiceHub : ServicesForResolution {
|
||||
* NOTE: Suspendable flow operations such as send, receive, subFlow and sleep, cannot be called within the lambda.
|
||||
*
|
||||
* @param block a lambda function with access to an [EntityManager].
|
||||
*
|
||||
* We want to make sure users have a restricted access to administrative functions.
|
||||
* The following methods are blocked:
|
||||
* - close()
|
||||
* - unwrap(cls: Class<T>?)
|
||||
* - getDelegate(): Any
|
||||
* - getMetamodel()
|
||||
* - joinTransaction()
|
||||
* - lock(entity: Any?, lockMode: LockModeType?)
|
||||
* - lock(entity: Any?, lockMode: LockModeType?, properties: MutableMap<String, Any>?)
|
||||
* - setProperty(propertyName: String?, value: Any?)
|
||||
*
|
||||
* getTransaction returns a [RestrictedEntityTransaction] to prevent unsafe manipulation of a flow's underlying
|
||||
* database transaction.
|
||||
* The following methods are blocked:
|
||||
* - begin()
|
||||
* - commit()
|
||||
* - rollback()
|
||||
*/
|
||||
fun <T : Any?> withEntityManager(block: EntityManager.() -> T): T
|
||||
|
||||
@ -404,6 +442,24 @@ interface ServiceHub : ServicesForResolution {
|
||||
* NOTE: Suspendable flow operations such as send, receive, subFlow and sleep, cannot be called within the lambda.
|
||||
*
|
||||
* @param block a lambda function with access to an [EntityManager].
|
||||
*
|
||||
* We want to make sure users have a restricted access to administrative functions.
|
||||
* The following methods are blocked:
|
||||
* - close()
|
||||
* - unwrap(cls: Class<T>?)
|
||||
* - getDelegate(): Any
|
||||
* - getMetamodel()
|
||||
* - joinTransaction()
|
||||
* - lock(entity: Any?, lockMode: LockModeType?)
|
||||
* - lock(entity: Any?, lockMode: LockModeType?, properties: MutableMap<String, Any>?)
|
||||
* - setProperty(propertyName: String?, value: Any?)
|
||||
*
|
||||
* getTransaction returns a [RestrictedEntityTransaction] to prevent unsafe manipulation of a flow's underlying
|
||||
* database transaction.
|
||||
* The following methods are blocked:
|
||||
* - begin()
|
||||
* - commit()
|
||||
* - rollback()
|
||||
*/
|
||||
fun withEntityManager(block: Consumer<EntityManager>)
|
||||
|
||||
|
@ -8,7 +8,6 @@ import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.schemas.StatePersistable
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
|
@ -0,0 +1,17 @@
|
||||
package net.corda.core.serialization
|
||||
|
||||
import net.corda.core.CordaRuntimeException
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
|
||||
@KeepForDJVM
|
||||
@CordaSerializable
|
||||
class MissingAttachmentsRuntimeException(val ids: List<AttachmentId>, message: String?, cause: Throwable?)
|
||||
: CordaRuntimeException(message, cause) {
|
||||
|
||||
@Suppress("unused")
|
||||
constructor(ids: List<AttachmentId>, message: String?) : this(ids, message, null)
|
||||
|
||||
@Suppress("unused")
|
||||
constructor(ids: List<AttachmentId>) : this(ids, null, null)
|
||||
}
|
@ -323,7 +323,13 @@ object AttachmentsClassLoaderBuilder {
|
||||
val serializationContext = cache.computeIfAbsent(Key(attachmentIds, params)) {
|
||||
// Create classloader and load serializers, whitelisted classes
|
||||
val transactionClassLoader = AttachmentsClassLoader(attachments, params, txId, isAttachmentTrusted, parent)
|
||||
val serializers = createInstancesOfClassesImplementing(transactionClassLoader, SerializationCustomSerializer::class.java)
|
||||
val serializers = try {
|
||||
createInstancesOfClassesImplementing(transactionClassLoader, SerializationCustomSerializer::class.java,
|
||||
JDK1_2_CLASS_FILE_FORMAT_MAJOR_VERSION..JDK8_CLASS_FILE_FORMAT_MAJOR_VERSION)
|
||||
}
|
||||
catch(ex: UnsupportedClassVersionError) {
|
||||
throw TransactionVerificationException.UnsupportedClassVersionError(txId, ex.message!!, ex)
|
||||
}
|
||||
val whitelistedClasses = ServiceLoader.load(SerializationWhitelist::class.java, transactionClassLoader)
|
||||
.flatMap(SerializationWhitelist::whitelist)
|
||||
|
||||
|
@ -3,7 +3,6 @@ package net.corda.core.serialization.internal
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.DoNotImplement
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.core.utilities.sequence
|
||||
|
@ -100,7 +100,7 @@ open class TransactionBuilder(
|
||||
commands = ArrayList(commands),
|
||||
window = window,
|
||||
privacySalt = privacySalt,
|
||||
references = references,
|
||||
references = ArrayList(references),
|
||||
serviceHub = serviceHub
|
||||
)
|
||||
t.inputsWithTransactionState.addAll(this.inputsWithTransactionState)
|
||||
@ -134,7 +134,7 @@ open class TransactionBuilder(
|
||||
*
|
||||
* @returns A new [WireTransaction] that will be unaffected by further changes to this [TransactionBuilder].
|
||||
*
|
||||
* @throws ZoneVersionTooLowException if there are reference states and the zone minimum platform version is less than 4.
|
||||
* @throws [ZoneVersionTooLowException] if there are reference states and the zone minimum platform version is less than 4.
|
||||
*/
|
||||
@Throws(MissingContractAttachments::class)
|
||||
fun toWireTransaction(services: ServicesForResolution): WireTransaction = toWireTransactionWithContext(services, null)
|
||||
|
@ -317,7 +317,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
/**
|
||||
* Checks that the given signature matches one of the commands and that it is a correct signature over the tx.
|
||||
*
|
||||
* @throws SignatureException if the signature didn't match the transaction contents.
|
||||
* @throws [SignatureException] if the signature didn't match the transaction contents.
|
||||
* @throws IllegalArgumentException if the signature key doesn't appear in any command.
|
||||
*/
|
||||
fun checkSignature(sig: TransactionSignature) {
|
||||
|
@ -471,9 +471,9 @@ class CryptoUtilsTest {
|
||||
val privKeyDecoded = Crypto.decodePrivateKey(privKey.encoded)
|
||||
val pubKeyDecoded = Crypto.decodePublicKey(pubKey.encoded)
|
||||
|
||||
assertEquals(privKeyDecoded.algorithm, "ECDSA")
|
||||
assertEquals(privKeyDecoded.algorithm, "EC")
|
||||
assertEquals((privKeyDecoded as ECKey).parameters, ECNamedCurveTable.getParameterSpec("secp256k1"))
|
||||
assertEquals(pubKeyDecoded.algorithm, "ECDSA")
|
||||
assertEquals(pubKeyDecoded.algorithm, "EC")
|
||||
assertEquals((pubKeyDecoded as ECKey).parameters, ECNamedCurveTable.getParameterSpec("secp256k1"))
|
||||
}
|
||||
|
||||
@ -481,9 +481,9 @@ class CryptoUtilsTest {
|
||||
fun `ECDSA secp256r1 scheme finder by key type`() {
|
||||
val keyPairR1 = Crypto.generateKeyPair(ECDSA_SECP256R1_SHA256)
|
||||
val (privR1, pubR1) = keyPairR1
|
||||
assertEquals(privR1.algorithm, "ECDSA")
|
||||
assertEquals(privR1.algorithm, "EC")
|
||||
assertEquals((privR1 as ECKey).parameters, ECNamedCurveTable.getParameterSpec("secp256r1"))
|
||||
assertEquals(pubR1.algorithm, "ECDSA")
|
||||
assertEquals(pubR1.algorithm, "EC")
|
||||
assertEquals((pubR1 as ECKey).parameters, ECNamedCurveTable.getParameterSpec("secp256r1"))
|
||||
}
|
||||
|
||||
@ -530,11 +530,11 @@ class CryptoUtilsTest {
|
||||
val encodedPubK1 = pubK1.encoded
|
||||
|
||||
val decodedPrivK1 = Crypto.decodePrivateKey(encodedPrivK1)
|
||||
assertEquals(decodedPrivK1.algorithm, "ECDSA")
|
||||
assertEquals(decodedPrivK1.algorithm, "EC")
|
||||
assertEquals(decodedPrivK1, privK1)
|
||||
|
||||
val decodedPubK1 = Crypto.decodePublicKey(encodedPubK1)
|
||||
assertEquals(decodedPubK1.algorithm, "ECDSA")
|
||||
assertEquals(decodedPubK1.algorithm, "EC")
|
||||
assertEquals(decodedPubK1, pubK1)
|
||||
}
|
||||
|
||||
@ -546,11 +546,11 @@ class CryptoUtilsTest {
|
||||
val encodedPubR1 = pubR1.encoded
|
||||
|
||||
val decodedPrivR1 = Crypto.decodePrivateKey(encodedPrivR1)
|
||||
assertEquals(decodedPrivR1.algorithm, "ECDSA")
|
||||
assertEquals(decodedPrivR1.algorithm, "EC")
|
||||
assertEquals(decodedPrivR1, privR1)
|
||||
|
||||
val decodedPubR1 = Crypto.decodePublicKey(encodedPubR1)
|
||||
assertEquals(decodedPubR1.algorithm, "ECDSA")
|
||||
assertEquals(decodedPubR1.algorithm, "EC")
|
||||
assertEquals(decodedPubR1, pubR1)
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user