From eb52c8be350e9b9915a41acd9c59c39ebb038703 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Thu, 29 Mar 2018 10:47:48 +0200 Subject: [PATCH 1/5] Improve the upgrade-test-packages.sh script to work on macOS. Fixes an issue found by tom on pubslack. (#2894) --- tools/scripts/upgrade-test-packages.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) mode change 100644 => 100755 tools/scripts/upgrade-test-packages.sh diff --git a/tools/scripts/upgrade-test-packages.sh b/tools/scripts/upgrade-test-packages.sh old mode 100644 new mode 100755 index 466219c410..a7e79aa0e0 --- a/tools/scripts/upgrade-test-packages.sh +++ b/tools/scripts/upgrade-test-packages.sh @@ -1,5 +1,15 @@ #!/bin/bash -find $1 -type f \( -iname \*.kt -o -iname \*.java \) -exec sed -i " +# +# Run this script with the path to your source code as the first argument. It will do some basic search/replace +# changes to the files to ease the port from 2.0 to 3.0 (but it isn't a complete solution). +# + +s=$( which gsed ) +if [[ $? == 1 ]]; then + s="sed" +fi + +find $1 -type f \( -iname \*.kt -o -iname \*.java \) -exec $s -i " s/net.corda.testing.\(\*\|generateStateRef\|freeLocalHostAndPort\|getFreeLocalPorts\|getTestPartyAndCertificate\|TestIdentity\|chooseIdentity\|singleIdentity\|TEST_TX_TIME\|DUMMY_NOTARY_NAME\|DUMMY_BANK_A_NAME\|DUMMY_BANK_B_NAME\|DUMMY_BANK_C_NAME\|BOC_NAME\|ALICE_NAME\|BOB_NAME\|CHARLIE_NAME\|DEV_INTERMEDIATE_CA\|DEV_ROOT_CA\|dummyCommand\|DummyCommandData\|MAX_MESSAGE_SIZE\|SerializationEnvironmentRule\|setGlobalSerialization\|expect\|sequence\|parallel\|replicate\|genericExpectEvents\)/net.corda.testing.core.\1/g " '{}' \; \ No newline at end of file From 2e1f591e7f9b72e7df7e26bec9d59efa08a69ed0 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Wed, 28 Mar 2018 16:36:08 +0200 Subject: [PATCH 2/5] Generate a PDF version of the docsite. Request from a user in Slack. --- docs/Makefile | 3 +++ docs/make-docsite.sh | 3 +++ docs/requirements.txt | 1 + docs/source/conf.py | 8 +++++++- docs/source/index.rst | 4 ++++ 5 files changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/Makefile b/docs/Makefile index 101260607b..e74f6866f9 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -190,3 +190,6 @@ pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." + +pdf: + $(SPHINXBUILD) -b pdf $(ALLSPHINXOPTS) $(BUILDDIR)/pdf diff --git a/docs/make-docsite.sh b/docs/make-docsite.sh index 8c2488ca04..bc317c3e63 100755 --- a/docs/make-docsite.sh +++ b/docs/make-docsite.sh @@ -11,4 +11,7 @@ else source virtualenv/Scripts/activate fi +# TODO: The PDF rendering is pretty ugly and can be improved a lot. +make pdf +mv build/pdf/corda-developer-site.pdf build/html/_static/corda-developer-site.pdf make html diff --git a/docs/requirements.txt b/docs/requirements.txt index f759847b57..fcb4fd0faf 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -10,3 +10,4 @@ six==1.10.0 snowballstemmer==1.2.1 Sphinx==1.4.4 sphinx-rtd-theme==0.1.9 +rst2pdf==0.93 diff --git a/docs/source/conf.py b/docs/source/conf.py index ebf92265fb..5684a4962e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -28,7 +28,13 @@ import sphinx_rtd_theme # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [] +extensions = ['rst2pdf.pdfbuilder'] + +# PDF configuration +pdf_documents = [('index', u'corda-developer-site', u'Corda Developer Documentation', u'R3')] +pdf_stylesheets = ['sphinx', 'kerning', 'a4', 'murphy', 'tenpoint'] +pdf_compressed = True +pdf_fit_mode = "shrink" # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/source/index.rst b/docs/source/index.rst index 8db0458253..bd85109131 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -22,8 +22,12 @@ If you have questions or comments, then get in touch on `Slack Date: Thu, 29 Mar 2018 18:25:56 +0800 Subject: [PATCH 3/5] Optimize imports (#2872) --- .../net/corda/testing/node/internal/network/NetworkMapServer.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/network/NetworkMapServer.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/network/NetworkMapServer.kt index 4285c983c3..5884cebb3f 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/network/NetworkMapServer.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/network/NetworkMapServer.kt @@ -2,7 +2,6 @@ package net.corda.testing.node.internal.network import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignedData -import net.corda.core.internal.signWithCert import net.corda.core.node.NodeInfo import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize From 0d1d7daedc62026eba2edebbfdf754f2569d0b3c Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Thu, 29 Mar 2018 13:01:41 +0100 Subject: [PATCH 4/5] [CORDA-1264]: Ensure correct serialisation and masking for throwables raised by a node and propagated through RPC. (#2892) --- .ci/api-current.txt | 2 +- .../corda/client/rpc/CordaRPCClientTest.kt | 4 +- .../corda/client/rpc/PermissionException.kt | 4 +- core/src/main/kotlin/net/corda/core/Utils.kt | 27 ++++ .../internal/concurrent/CordaFutureImpl.kt | 23 +++ docs/source/changelog.rst | 2 + .../exceptions/InternalNodeException.kt | 32 ++++ .../OutdatedNetworkParameterHashException.kt | 11 ++ .../exceptions/RejectedCommandException.kt | 2 +- .../exceptions/RpcSerializableError.kt | 9 ++ .../adapters/InternalObfuscatingFlowHandle.kt | 15 ++ .../InternalObfuscatingFlowProgressHandle.kt | 22 +++ .../net/corda/ClientRelevantException.kt | 6 + .../node/services/AttachmentLoadingTests.kt | 4 +- .../services/rpc/RpcExceptionHandlingTest.kt | 120 ++++++++++++++ .../corda/node/internal/CordaRPCOpsImpl.kt | 9 +- .../kotlin/net/corda/node/internal/Node.kt | 11 +- .../internal/RpcExceptionHandlingProxy.kt | 149 ++++++++++++++++++ .../services/network/NetworkMapUpdater.kt | 4 +- 19 files changed, 437 insertions(+), 19 deletions(-) create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/exceptions/InternalNodeException.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/exceptions/OutdatedNetworkParameterHashException.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/exceptions/RpcSerializableError.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/exceptions/adapters/InternalObfuscatingFlowHandle.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/exceptions/adapters/InternalObfuscatingFlowProgressHandle.kt create mode 100644 node/src/integration-test/kotlin/net/corda/ClientRelevantException.kt create mode 100644 node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcExceptionHandlingTest.kt create mode 100644 node/src/main/kotlin/net/corda/node/internal/RpcExceptionHandlingProxy.kt diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 1aa2494645..4e12066579 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -4390,7 +4390,7 @@ public static final class net.corda.client.rpc.CordaRPCClientConfiguration$Compa public int getServerProtocolVersion() public void notifyServerAndClose() ## -public final class net.corda.client.rpc.PermissionException extends net.corda.core.CordaRuntimeException +public final class net.corda.client.rpc.PermissionException extends net.corda.core.CordaRuntimeException implements net.corda.nodeapi.exceptions.RpcSerializableError public (String) ## @net.corda.core.DoNotImplement public interface net.corda.client.rpc.RPCConnection extends java.io.Closeable diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt index 2eef0c704f..7826a549ba 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt @@ -12,13 +12,13 @@ import net.corda.finance.DOLLARS import net.corda.finance.USD import net.corda.finance.contracts.getCashBalance import net.corda.finance.contracts.getCashBalances -import net.corda.finance.flows.CashException import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.finance.schemas.CashSchemaV1 import net.corda.node.internal.Node import net.corda.node.internal.StartedNode import net.corda.node.services.Permissions.Companion.all +import net.corda.nodeapi.exceptions.InternalNodeException import net.corda.testing.core.* import net.corda.testing.node.User import net.corda.testing.node.internal.NodeBasedTest @@ -158,7 +158,7 @@ class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance.contracts", C fun `sub-type of FlowException thrown by flow`() { login(rpcUser.username, rpcUser.password) val handle = connection!!.proxy.startFlow(::CashPaymentFlow, 100.DOLLARS, identity) - assertThatExceptionOfType(CashException::class.java).isThrownBy { + assertThatExceptionOfType(InternalNodeException::class.java).isThrownBy { handle.returnValue.getOrThrow() } } diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/PermissionException.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/PermissionException.kt index 71596c6e5e..bbb2057732 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/PermissionException.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/PermissionException.kt @@ -1,10 +1,10 @@ package net.corda.client.rpc import net.corda.core.CordaRuntimeException -import net.corda.core.serialization.CordaSerializable +import net.corda.nodeapi.exceptions.RpcSerializableError /** * Thrown to indicate that the calling user does not have permission for something they have requested (for example * calling a method). */ -class PermissionException(msg: String) : CordaRuntimeException(msg) +class PermissionException(message: String) : CordaRuntimeException(message), RpcSerializableError diff --git a/core/src/main/kotlin/net/corda/core/Utils.kt b/core/src/main/kotlin/net/corda/core/Utils.kt index d9702f53aa..d2d15b87aa 100644 --- a/core/src/main/kotlin/net/corda/core/Utils.kt +++ b/core/src/main/kotlin/net/corda/core/Utils.kt @@ -5,6 +5,7 @@ package net.corda.core import net.corda.core.concurrent.CordaFuture import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.thenMatch +import net.corda.core.messaging.DataFeed import rx.Observable import rx.Observer @@ -44,3 +45,29 @@ fun Observable.toFuture(): CordaFuture = openFuture().also { } } } + +/** + * Returns a [DataFeed] that transforms errors according to the provided [transform] function. + */ +fun DataFeed.mapErrors(transform: (Throwable) -> Throwable): DataFeed { + + return copy(updates = updates.mapErrors(transform)) +} + +/** + * Returns a [DataFeed] that processes errors according to the provided [action]. + */ +fun DataFeed.doOnError(action: (Throwable) -> Unit): DataFeed { + + return copy(updates = updates.doOnError(action)) +} + +/** + * Returns an [Observable] that transforms errors according to the provided [transform] function. + */ +fun Observable.mapErrors(transform: (Throwable) -> Throwable): Observable { + + return onErrorResumeNext { error -> + Observable.error(transform(error)) + } +} diff --git a/core/src/main/kotlin/net/corda/core/internal/concurrent/CordaFutureImpl.kt b/core/src/main/kotlin/net/corda/core/internal/concurrent/CordaFutureImpl.kt index 6ff8ce4f2c..c51deac096 100644 --- a/core/src/main/kotlin/net/corda/core/internal/concurrent/CordaFutureImpl.kt +++ b/core/src/main/kotlin/net/corda/core/internal/concurrent/CordaFutureImpl.kt @@ -39,6 +39,29 @@ fun CordaFuture.map(transform: (V) -> W): CordaFuture = CordaFu }) } +/** + * Returns a future that will also apply the passed closure on an error. + */ +fun CordaFuture.doOnError(accept: (Throwable) -> Unit): CordaFuture = CordaFutureImpl().also { result -> + thenMatch({ + result.capture { it } + }, { + accept(it) + result.setException(it) + }) +} + +/** + * Returns a future that will map an error thrown using the provided [transform] function. + */ +fun CordaFuture.mapError(transform: (Throwable) -> Throwable): CordaFuture = CordaFutureImpl().also { result -> + thenMatch({ + result.capture { it } + }, { + result.setException(transform(it)) + }) +} + /** * Returns a future that will have the same outcome as the future returned by the given transform. * But if this future or the transform fails, the returned future's outcome is the same throwable. diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index cb95f072b5..b567354c5a 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -7,6 +7,8 @@ Unreleased Here are brief summaries of what's changed between each snapshot release. This includes guidance on how to upgrade code from the previous milestone release. +* Errors thrown by a Corda node will now reported to a calling RPC client with attention to serialization and obfuscation of internal data. + * Serializing an inner class (non-static nested class in Java, inner class in Kotlin) will be rejected explicitly by the serialization framework. Prior to this change it didn't work, but the error thrown was opaque (complaining about too few arguments to a constructor). Whilst this was possible in the older Kryo implementation (Kryo passing null as the synthesised diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/InternalNodeException.kt b/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/InternalNodeException.kt new file mode 100644 index 0000000000..805841ee7c --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/InternalNodeException.kt @@ -0,0 +1,32 @@ +package net.corda.nodeapi.exceptions + +import net.corda.core.CordaRuntimeException +import java.io.InvalidClassException + +// could change to use package name matching but trying to avoid reflection for now +private val whitelisted = setOf( + InvalidClassException::class, + RpcSerializableError::class +) + +/** + * An [Exception] to signal RPC clients that something went wrong within a Corda node. + */ +class InternalNodeException(message: String) : CordaRuntimeException(message) { + + companion object { + + private const val DEFAULT_MESSAGE = "Something went wrong within the Corda node." + + fun defaultMessage(): String = DEFAULT_MESSAGE + + fun obfuscateIfInternal(wrapped: Throwable): Throwable { + + (wrapped as? CordaRuntimeException)?.setCause(null) + return when { + whitelisted.any { it.isInstance(wrapped) } -> wrapped + else -> InternalNodeException(DEFAULT_MESSAGE) + } + } + } +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/OutdatedNetworkParameterHashException.kt b/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/OutdatedNetworkParameterHashException.kt new file mode 100644 index 0000000000..8c21c2b943 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/OutdatedNetworkParameterHashException.kt @@ -0,0 +1,11 @@ +package net.corda.nodeapi.exceptions + +import net.corda.core.CordaRuntimeException +import net.corda.core.crypto.SecureHash + +class OutdatedNetworkParameterHashException(old: SecureHash, new: SecureHash) : CordaRuntimeException(TEMPLATE.format(old, new)), RpcSerializableError { + + private companion object { + private const val TEMPLATE = "Refused to accept parameters with hash %s because network map advertises update with hash %s. Please check newest version" + } +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/RejectedCommandException.kt b/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/RejectedCommandException.kt index 8fd3004046..024535d274 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/RejectedCommandException.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/RejectedCommandException.kt @@ -5,4 +5,4 @@ import net.corda.core.CordaRuntimeException /** * Thrown to indicate that the command was rejected by the node, typically due to a special temporary mode. */ -class RejectedCommandException(msg: String) : CordaRuntimeException(msg) \ No newline at end of file +class RejectedCommandException(message: String) : CordaRuntimeException(message), RpcSerializableError \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/RpcSerializableError.kt b/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/RpcSerializableError.kt new file mode 100644 index 0000000000..8756f0f72f --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/RpcSerializableError.kt @@ -0,0 +1,9 @@ +package net.corda.nodeapi.exceptions + +import net.corda.core.serialization.CordaSerializable + +/** + * Allows an implementing [Throwable] to be propagated to RPC clients. + */ +@CordaSerializable +interface RpcSerializableError \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/adapters/InternalObfuscatingFlowHandle.kt b/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/adapters/InternalObfuscatingFlowHandle.kt new file mode 100644 index 0000000000..fbef080c7a --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/adapters/InternalObfuscatingFlowHandle.kt @@ -0,0 +1,15 @@ +package net.corda.nodeapi.exceptions.adapters + +import net.corda.core.internal.concurrent.mapError +import net.corda.core.messaging.FlowHandle +import net.corda.core.serialization.CordaSerializable +import net.corda.nodeapi.exceptions.InternalNodeException + +/** + * Adapter able to mask errors within a Corda node for RPC clients. + */ +@CordaSerializable +data class InternalObfuscatingFlowHandle(val wrapped: FlowHandle) : FlowHandle by wrapped { + + override val returnValue = wrapped.returnValue.mapError(InternalNodeException.Companion::obfuscateIfInternal) +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/adapters/InternalObfuscatingFlowProgressHandle.kt b/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/adapters/InternalObfuscatingFlowProgressHandle.kt new file mode 100644 index 0000000000..7b8f13f3cb --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/adapters/InternalObfuscatingFlowProgressHandle.kt @@ -0,0 +1,22 @@ +package net.corda.nodeapi.exceptions.adapters + +import net.corda.core.internal.concurrent.mapError +import net.corda.core.mapErrors +import net.corda.core.messaging.FlowProgressHandle +import net.corda.core.serialization.CordaSerializable +import net.corda.nodeapi.exceptions.InternalNodeException + +/** + * Adapter able to mask errors within a Corda node for RPC clients. + */ +@CordaSerializable +class InternalObfuscatingFlowProgressHandle(val wrapped: FlowProgressHandle) : FlowProgressHandle by wrapped { + + override val returnValue = wrapped.returnValue.mapError(InternalNodeException.Companion::obfuscateIfInternal) + + override val progress = wrapped.progress.mapErrors(InternalNodeException.Companion::obfuscateIfInternal) + + override val stepsTreeIndexFeed = wrapped.stepsTreeIndexFeed?.mapErrors(InternalNodeException.Companion::obfuscateIfInternal) + + override val stepsTreeFeed = wrapped.stepsTreeFeed?.mapErrors(InternalNodeException.Companion::obfuscateIfInternal) +} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/ClientRelevantException.kt b/node/src/integration-test/kotlin/net/corda/ClientRelevantException.kt new file mode 100644 index 0000000000..614cbae99f --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/ClientRelevantException.kt @@ -0,0 +1,6 @@ +package net.corda + +import net.corda.core.CordaRuntimeException +import net.corda.nodeapi.exceptions.RpcSerializableError + +class ClientRelevantException(message: String?, cause: Throwable?) : CordaRuntimeException(message, cause), RpcSerializableError \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt index 166056f0bf..016eaa1f4f 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt @@ -5,7 +5,6 @@ import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.* import net.corda.core.cordapp.CordappProvider import net.corda.core.flows.FlowLogic -import net.corda.core.flows.UnexpectedFlowEndException import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.concurrent.transpose @@ -22,6 +21,7 @@ import net.corda.core.utilities.contextLogger import net.corda.core.utilities.getOrThrow import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl +import net.corda.nodeapi.exceptions.InternalNodeException import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME @@ -114,7 +114,7 @@ class AttachmentLoadingTests { driver { installIsolatedCordappTo(bankAName) val (bankA, bankB) = createTwoNodes() - assertFailsWith("Party C=CH,L=Zurich,O=BankB rejected session request: Don't know net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator") { + assertFailsWith { bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow() } } diff --git a/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcExceptionHandlingTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcExceptionHandlingTest.kt new file mode 100644 index 0000000000..ad3b08daaa --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcExceptionHandlingTest.kt @@ -0,0 +1,120 @@ +package net.corda.node.services.rpc + +import co.paralleluniverse.fibers.Suspendable +import net.corda.ClientRelevantException +import net.corda.core.flows.* +import net.corda.core.identity.Party +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.unwrap +import net.corda.node.services.Permissions +import net.corda.nodeapi.exceptions.InternalNodeException +import net.corda.testing.core.singleIdentity +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.NodeParameters +import net.corda.testing.driver.driver +import net.corda.testing.node.User +import org.assertj.core.api.Assertions.assertThatCode +import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat +import org.hibernate.exception.GenericJDBCException +import org.junit.Test +import java.sql.SQLException + +class RpcExceptionHandlingTest { + + private val user = User("mark", "dadada", setOf(Permissions.all())) + private val users = listOf(user) + + @Test + fun `rpc client handles exceptions thrown on node side`() { + + driver(DriverParameters(startNodesInProcess = true)) { + + val node = startNode(NodeParameters(rpcUsers = users)).getOrThrow() + + assertThatCode { node.rpc.startFlow(::Flow).returnValue.getOrThrow() }.isInstanceOfSatisfying(InternalNodeException::class.java) { exception -> + + assertThat(exception).hasNoCause() + assertThat(exception.stackTrace).isEmpty() + assertThat(exception.message).isEqualTo(InternalNodeException.defaultMessage()) + } + } + } + + @Test + fun `rpc client handles client-relevant exceptions thrown on node side`() { + + driver(DriverParameters(startNodesInProcess = true)) { + + val node = startNode(NodeParameters(rpcUsers = users)).getOrThrow() + val clientRelevantMessage = "This is for the players!" + + assertThatCode { node.rpc.startFlow(::ClientRelevantErrorFlow, clientRelevantMessage).returnValue.getOrThrow() }.isInstanceOfSatisfying(ClientRelevantException::class.java) { exception -> + + assertThat(exception).hasNoCause() + assertThat(exception.stackTrace).isEmpty() + assertThat(exception.message).isEqualTo(clientRelevantMessage) + } + } + } + + @Test + fun `rpc client handles exceptions thrown on counter-party side`() { + + driver(DriverParameters(startNodesInProcess = true)) { + + val nodeA = startNode(NodeParameters(rpcUsers = users)).getOrThrow() + val nodeB = startNode(NodeParameters(rpcUsers = users)).getOrThrow() + + assertThatCode { nodeA.rpc.startFlow(::InitFlow, nodeB.nodeInfo.singleIdentity()).returnValue.getOrThrow() }.isInstanceOfSatisfying(InternalNodeException::class.java) { exception -> + + assertThat(exception).hasNoCause() + assertThat(exception.stackTrace).isEmpty() + assertThat(exception.message).isEqualTo(InternalNodeException.defaultMessage()) + } + } + } +} + +@StartableByRPC +class Flow : FlowLogic() { + + @Suspendable + override fun call(): String { + + throw GenericJDBCException("Something went wrong!", SQLException("Oops!")) + } +} + +@StartableByRPC +@InitiatingFlow +class InitFlow(private val party: Party) : FlowLogic() { + + @Suspendable + override fun call(): String { + + val session = initiateFlow(party) + return session.sendAndReceive("hey").unwrap { it } + } +} + +@InitiatedBy(InitFlow::class) +class InitiatedFlow(private val initiatingSession: FlowSession) : FlowLogic() { + + @Suspendable + override fun call() { + + initiatingSession.receive().unwrap { it } + throw GenericJDBCException("Something went wrong!", SQLException("Oops!")) + } +} + +@StartableByRPC +class ClientRelevantErrorFlow(private val message: String) : FlowLogic() { + + @Suspendable + override fun call(): String { + + throw ClientRelevantException(message, SQLException("Oops!")) + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt index 44214ba206..7a4a8a94e5 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -204,14 +204,9 @@ internal class CordaRPCOpsImpl( } override fun queryAttachments(query: AttachmentQueryCriteria, sorting: AttachmentSort?): List { - try { - return database.transaction { + // TODO: this operation should not require an explicit transaction + return database.transaction { services.attachments.queryAttachments(query, sorting) - } - } catch (e: Exception) { - // log and rethrow exception so we keep a copy server side - log.error(e.message) - throw e.cause ?: e } } diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 4879d5868b..3ce19af998 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -27,8 +27,11 @@ import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.serialization.KryoServerSerializationScheme import net.corda.node.services.api.NodePropertiesStore import net.corda.node.services.api.SchemaService -import net.corda.node.services.config.* +import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.SecurityConfiguration +import net.corda.node.services.config.VerifierType import net.corda.node.services.config.shell.shellUser +import net.corda.node.services.config.shouldInitCrashShell import net.corda.node.services.messaging.* import net.corda.node.services.rpc.ArtemisRpcBroker import net.corda.node.services.transactions.InMemoryTransactionVerifierService @@ -281,7 +284,11 @@ open class Node(configuration: NodeConfiguration, // Start up the MQ clients. rpcMessagingClient?.run { runOnStop += this::close - start(rpcOps, securityManager) + when (rpcOps) { + // not sure what this RPCOps base interface is for + is SecureCordaRPCOps -> start(RpcExceptionHandlingProxy(rpcOps), securityManager) + else -> start(rpcOps, securityManager) + } } verifierMessagingClient?.run { runOnStop += this::stop diff --git a/node/src/main/kotlin/net/corda/node/internal/RpcExceptionHandlingProxy.kt b/node/src/main/kotlin/net/corda/node/internal/RpcExceptionHandlingProxy.kt new file mode 100644 index 0000000000..f8434b9b52 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/RpcExceptionHandlingProxy.kt @@ -0,0 +1,149 @@ +package net.corda.node.internal + +import net.corda.core.concurrent.CordaFuture +import net.corda.core.contracts.ContractState +import net.corda.core.crypto.SecureHash +import net.corda.core.doOnError +import net.corda.core.flows.FlowLogic +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.concurrent.doOnError +import net.corda.core.internal.concurrent.mapError +import net.corda.core.mapErrors +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.messaging.DataFeed +import net.corda.core.messaging.FlowHandle +import net.corda.core.messaging.FlowProgressHandle +import net.corda.core.node.services.vault.* +import net.corda.core.utilities.loggerFor +import net.corda.nodeapi.exceptions.InternalNodeException +import net.corda.nodeapi.exceptions.adapters.InternalObfuscatingFlowHandle +import net.corda.nodeapi.exceptions.adapters.InternalObfuscatingFlowProgressHandle +import java.io.InputStream +import java.security.PublicKey + +class RpcExceptionHandlingProxy(private val delegate: SecureCordaRPCOps) : CordaRPCOps { + + private companion object { + private val logger = loggerFor() + } + + override val protocolVersion: Int get() = delegate.protocolVersion + + override fun startFlowDynamic(logicType: Class>, vararg args: Any?): FlowHandle = wrap { + + val handle = delegate.startFlowDynamic(logicType, *args) + val result = InternalObfuscatingFlowHandle(handle) + result.returnValue.doOnError { error -> logger.error(error.message, error) } + result + } + + override fun startTrackedFlowDynamic(logicType: Class>, vararg args: Any?): FlowProgressHandle = wrap { + + val handle = delegate.startTrackedFlowDynamic(logicType, *args) + val result = InternalObfuscatingFlowProgressHandle(handle) + result.returnValue.doOnError { error -> logger.error(error.message, error) } + result + } + + override fun waitUntilNetworkReady() = wrapFuture(delegate::waitUntilNetworkReady) + + override fun stateMachinesFeed() = wrapFeed(delegate::stateMachinesFeed) + + override fun vaultTrackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class) = wrapFeed { delegate.vaultTrackBy(criteria, paging, sorting, contractStateType) } + + override fun vaultTrack(contractStateType: Class) = wrapFeed { delegate.vaultTrack(contractStateType) } + + override fun vaultTrackByCriteria(contractStateType: Class, criteria: QueryCriteria) = wrapFeed { delegate.vaultTrackByCriteria(contractStateType, criteria) } + + override fun vaultTrackByWithPagingSpec(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification) = wrapFeed { delegate.vaultTrackByWithPagingSpec(contractStateType, criteria, paging) } + + override fun vaultTrackByWithSorting(contractStateType: Class, criteria: QueryCriteria, sorting: Sort) = wrapFeed { delegate.vaultTrackByWithSorting(contractStateType, criteria, sorting) } + + override fun stateMachineRecordedTransactionMappingFeed() = wrapFeed(delegate::stateMachineRecordedTransactionMappingFeed) + + override fun networkMapFeed() = wrapFeed(delegate::networkMapFeed) + + override fun networkParametersFeed() = wrapFeed(delegate::networkParametersFeed) + + override fun internalVerifiedTransactionsFeed() = wrapFeed(delegate::internalVerifiedTransactionsFeed) + + override fun stateMachinesSnapshot() = wrap(delegate::stateMachinesSnapshot) + + override fun vaultQueryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class) = wrap { delegate.vaultQueryBy(criteria, paging, sorting, contractStateType) } + + override fun vaultQuery(contractStateType: Class) = wrap { delegate.vaultQuery(contractStateType) } + + override fun vaultQueryByCriteria(criteria: QueryCriteria, contractStateType: Class) = wrap { delegate.vaultQueryByCriteria(criteria, contractStateType) } + + override fun vaultQueryByWithPagingSpec(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification) = wrap { delegate.vaultQueryByWithPagingSpec(contractStateType, criteria, paging) } + + override fun vaultQueryByWithSorting(contractStateType: Class, criteria: QueryCriteria, sorting: Sort) = wrap { delegate.vaultQueryByWithSorting(contractStateType, criteria, sorting) } + + override fun internalVerifiedTransactionsSnapshot() = wrap(delegate::internalVerifiedTransactionsSnapshot) + + override fun stateMachineRecordedTransactionMappingSnapshot() = wrap(delegate::stateMachineRecordedTransactionMappingSnapshot) + + override fun networkMapSnapshot() = wrap(delegate::networkMapSnapshot) + + override fun acceptNewNetworkParameters(parametersHash: SecureHash) = wrap { delegate.acceptNewNetworkParameters(parametersHash) } + + override fun nodeInfo() = wrap(delegate::nodeInfo) + + override fun notaryIdentities() = wrap(delegate::notaryIdentities) + + override fun addVaultTransactionNote(txnId: SecureHash, txnNote: String) = wrap { delegate.addVaultTransactionNote(txnId, txnNote) } + + override fun getVaultTransactionNotes(txnId: SecureHash) = wrap { delegate.getVaultTransactionNotes(txnId) } + + override fun attachmentExists(id: SecureHash) = wrap { delegate.attachmentExists(id) } + + override fun openAttachment(id: SecureHash) = wrap { delegate.openAttachment(id) } + + override fun uploadAttachment(jar: InputStream) = wrap { delegate.uploadAttachment(jar) } + + override fun uploadAttachmentWithMetadata(jar: InputStream, uploader: String, filename: String) = wrap { delegate.uploadAttachmentWithMetadata(jar, uploader, filename) } + + override fun queryAttachments(query: AttachmentQueryCriteria, sorting: AttachmentSort?) = wrap { delegate.queryAttachments(query, sorting) } + + override fun currentNodeTime() = wrap(delegate::currentNodeTime) + + override fun wellKnownPartyFromAnonymous(party: AbstractParty) = wrap { delegate.wellKnownPartyFromAnonymous(party) } + + override fun partyFromKey(key: PublicKey) = wrap { delegate.partyFromKey(key) } + + override fun wellKnownPartyFromX500Name(x500Name: CordaX500Name) = wrap { delegate.wellKnownPartyFromX500Name(x500Name) } + + override fun notaryPartyFromX500Name(x500Name: CordaX500Name) = wrap { delegate.notaryPartyFromX500Name(x500Name) } + + override fun partiesFromName(query: String, exactMatch: Boolean) = wrap { delegate.partiesFromName(query, exactMatch) } + + override fun registeredFlows() = wrap(delegate::registeredFlows) + + override fun nodeInfoFromParty(party: AbstractParty) = wrap { delegate.nodeInfoFromParty(party) } + + override fun clearNetworkMapCache() = wrap(delegate::clearNetworkMapCache) + + override fun setFlowsDrainingModeEnabled(enabled: Boolean) = wrap { delegate.setFlowsDrainingModeEnabled(enabled) } + + override fun isFlowsDrainingModeEnabled() = wrap(delegate::isFlowsDrainingModeEnabled) + + override fun shutdown() = wrap(delegate::shutdown) + + private fun wrap(call: () -> RESULT): RESULT { + + return try { + call.invoke() + } catch (error: Throwable) { + logger.error(error.message, error) + throw InternalNodeException.obfuscateIfInternal(error) + } + } + + private fun wrapFeed(call: () -> DataFeed) = wrap { + + call.invoke().doOnError { error -> logger.error(error.message, error) }.mapErrors(InternalNodeException.Companion::obfuscateIfInternal) + } + + private fun wrapFuture(call: () -> CordaFuture): CordaFuture = wrap { call.invoke().mapError(InternalNodeException.Companion::obfuscateIfInternal).doOnError { error -> logger.error(error.message, error) } } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt index c7b4c98930..a8b3e7c448 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt @@ -14,6 +14,7 @@ import net.corda.core.utilities.contextLogger import net.corda.core.utilities.minutes import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.utilities.NamedThreadFactory +import net.corda.nodeapi.exceptions.OutdatedNetworkParameterHashException import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME @@ -183,8 +184,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, networkMapClient.ackNetworkParametersUpdate(sign(parametersHash)) logger.info("Accepted network parameter update $update: $newNetParams") } else { - throw IllegalArgumentException("Refused to accept parameters with hash $parametersHash because network map " + - "advertises update with hash $newParametersHash. Please check newest version") + throw OutdatedNetworkParameterHashException(parametersHash, newParametersHash) } } } From 558f5cddce93e01a46f718191b8ac4b0f99a8c39 Mon Sep 17 00:00:00 2001 From: sollecitom Date: Thu, 29 Mar 2018 13:17:22 +0100 Subject: [PATCH 5/5] Porting change to make JPA entities non-final and serializable. --- .../net/corda/node/internal/RpcExceptionHandlingProxy.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/node/src/main/kotlin/net/corda/node/internal/RpcExceptionHandlingProxy.kt b/node/src/main/kotlin/net/corda/node/internal/RpcExceptionHandlingProxy.kt index f8434b9b52..60f285ba2b 100644 --- a/node/src/main/kotlin/net/corda/node/internal/RpcExceptionHandlingProxy.kt +++ b/node/src/main/kotlin/net/corda/node/internal/RpcExceptionHandlingProxy.kt @@ -5,6 +5,7 @@ import net.corda.core.contracts.ContractState import net.corda.core.crypto.SecureHash import net.corda.core.doOnError import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.internal.concurrent.doOnError @@ -128,6 +129,8 @@ class RpcExceptionHandlingProxy(private val delegate: SecureCordaRPCOps) : Corda override fun isFlowsDrainingModeEnabled() = wrap(delegate::isFlowsDrainingModeEnabled) + override fun killFlow(id: StateMachineRunId) = wrap { delegate.killFlow(id) } + override fun shutdown() = wrap(delegate::shutdown) private fun wrap(call: () -> RESULT): RESULT {