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 8c00a925ee..2e6996978b 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 @@ -1,26 +1,33 @@ package net.corda.client.rpc +import net.corda.client.rpc.internal.createCordaRPCClientWithSslAndClassLoader import net.corda.core.context.* +import net.corda.core.contracts.FungibleAsset import net.corda.core.crypto.random63BitValue import net.corda.core.identity.Party import net.corda.core.internal.concurrent.flatMap -import net.corda.core.internal.packageName +import net.corda.core.internal.location +import net.corda.core.internal.toPath import net.corda.core.messaging.* +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.finance.DOLLARS +import net.corda.finance.POUNDS import net.corda.finance.USD +import net.corda.finance.contracts.asset.Cash import net.corda.finance.contracts.getCashBalance import net.corda.finance.contracts.getCashBalances 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.testing.common.internal.checkNotOnClasspath import net.corda.testing.core.* import net.corda.testing.node.User import net.corda.testing.node.internal.NodeBasedTest +import net.corda.testing.node.internal.ProcessUtilities import org.apache.activemq.artemis.api.core.ActiveMQSecurityException import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType @@ -28,6 +35,10 @@ import org.junit.After import org.junit.Before import org.junit.Test import rx.subjects.PublishSubject +import java.io.File.pathSeparator +import java.net.URLClassLoader +import java.nio.file.Paths +import java.util.* import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService @@ -36,9 +47,11 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance.contracts", CashSchemaV1::class.packageName)) { - private val rpcUser = User("user1", "test", permissions = setOf(all()) - ) +class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance")) { + companion object { + val rpcUser = User("user1", "test", permissions = setOf(all())) + } + private lateinit var node: StartedNode private lateinit var identity: Party private lateinit var client: CordaRPCClient @@ -51,7 +64,7 @@ class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance.contracts", C @Before fun setUp() { node = startNode(ALICE_NAME, rpcUsers = listOf(rpcUser)) - client = CordaRPCClient(node.internals.configuration.rpcOptions.address!!, CordaRPCClientConfiguration.DEFAULT.copy( + client = CordaRPCClient(node.internals.configuration.rpcOptions.address, CordaRPCClientConfiguration.DEFAULT.copy( maxReconnectAttempts = 5 )) identity = node.info.identityFromX500Name(ALICE_NAME) @@ -83,7 +96,6 @@ class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance.contracts", C @Test fun `shutdown command stops the node`() { - val nodeIsShut: PublishSubject = PublishSubject.create() val latch = CountDownLatch(1) var successful = false @@ -130,7 +142,6 @@ class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance.contracts", C } private class CloseableExecutor(private val delegate: ScheduledExecutorService) : AutoCloseable, ScheduledExecutorService by delegate { - override fun close() { delegate.shutdown() } @@ -209,19 +220,70 @@ class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance.contracts", C ) } } -} -private fun checkShellNotification(info: StateMachineInfo) { - val context = info.invocationContext - assertThat(context.origin).isInstanceOf(InvocationOrigin.Shell::class.java) -} + // WireTransaction stores its components as blobs which are deserialised in its constructor. This test makes sure + // the extra class loader given to the CordaRPCClient is used in this deserialisation, as otherwise any WireTransaction + // containing Cash.State objects are not receivable by the client. + // + // We run the client in a separate process, without the finance module on its system classpath to ensure that the + // additional class loader that we give it is used. Cash.State objects are used as they can't be synthesised fully + // by the carpenter, and thus avoiding any false-positive results. + @Test + fun `additional class loader used by WireTransaction when it deserialises its components`() { + val financeLocation = Cash::class.java.location.toPath().toString() + val classpathWithoutFinance = ProcessUtilities.defaultClassPath + .split(pathSeparator) + .filter { financeLocation !in it } + .joinToString(pathSeparator) -private fun checkRpcNotification(info: StateMachineInfo, rpcUsername: String, historicalIds: MutableSet, externalTrace: Trace?, impersonatedActor: Actor?) { - val context = info.invocationContext - assertThat(context.origin).isInstanceOf(InvocationOrigin.RPC::class.java) - assertThat(context.externalTrace).isEqualTo(externalTrace) - assertThat(context.impersonatedActor).isEqualTo(impersonatedActor) - assertThat(context.actor?.id?.value).isEqualTo(rpcUsername) - assertThat(historicalIds).doesNotContain(context.trace.invocationId) - historicalIds.add(context.trace.invocationId) + // Create a Cash.State object for the StandaloneCashRpcClient to get + node.services.startFlow(CashIssueFlow(100.POUNDS, OpaqueBytes.of(1), identity), InvocationContext.shell()) + val outOfProcessRpc = ProcessUtilities.startJavaProcess( + classpath = classpathWithoutFinance, + arguments = listOf(node.internals.configuration.rpcOptions.address.toString(), financeLocation) + ) + assertThat(outOfProcessRpc.waitFor()).isZero() // i.e. no exceptions were thrown + } + + private fun checkShellNotification(info: StateMachineInfo) { + val context = info.invocationContext + assertThat(context.origin).isInstanceOf(InvocationOrigin.Shell::class.java) + } + + private fun checkRpcNotification(info: StateMachineInfo, + rpcUsername: String, + historicalIds: MutableSet, + externalTrace: Trace?, + impersonatedActor: Actor?) { + val context = info.invocationContext + assertThat(context.origin).isInstanceOf(InvocationOrigin.RPC::class.java) + assertThat(context.externalTrace).isEqualTo(externalTrace) + assertThat(context.impersonatedActor).isEqualTo(impersonatedActor) + assertThat(context.actor?.id?.value).isEqualTo(rpcUsername) + assertThat(historicalIds).doesNotContain(context.trace.invocationId) + historicalIds.add(context.trace.invocationId) + } + + private object StandaloneCashRpcClient { + @JvmStatic + fun main(args: Array) { + checkNotOnClasspath("net.corda.finance.contracts.asset.Cash") { + "The finance module cannot be on the system classpath" + } + val address = NetworkHostAndPort.parse(args[0]) + val financeClassLoader = URLClassLoader(arrayOf(Paths.get(args[1]).toUri().toURL())) + val rpcUser = CordaRPCClientTest.rpcUser + val client = createCordaRPCClientWithSslAndClassLoader(address, classLoader = financeClassLoader) + val state = client.use(rpcUser.username, rpcUser.password) { + // financeClassLoader should be allowing the Cash.State to materialise + @Suppress("DEPRECATION") + it.proxy.internalVerifiedTransactionsSnapshot()[0].tx.outputsOfType>()[0] + } + assertThat(state.javaClass.name).isEqualTo("net.corda.finance.contracts.asset.Cash${'$'}State") + assertThat(state.amount.quantity).isEqualTo(10000) + assertThat(state.amount.token.product).isEqualTo(Currency.getInstance("GBP")) + // This particular check assures us that the Cash.State that we have hasn't been carpented. + assertThat(state.participants).isEqualTo(listOf(state.owner)) + } + } } diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt index 968620cef9..e41a7ed75c 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt @@ -286,7 +286,7 @@ class CordaRPCClient private constructor( effectiveSerializationEnv } catch (e: IllegalStateException) { try { - AMQPClientSerializationScheme.initialiseSerialization() + AMQPClientSerializationScheme.initialiseSerialization(classLoader) } catch (e: IllegalStateException) { // Race e.g. two of these constructed in parallel, ignore. } diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt index 389609f84c..1f82119356 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt @@ -3,6 +3,7 @@ package net.corda.client.rpc.internal.serialization.amqp import net.corda.core.cordapp.Cordapp import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.SerializationContext.* import net.corda.core.serialization.SerializationCustomSerializer import net.corda.core.serialization.internal.SerializationEnvironment import net.corda.core.serialization.internal.SerializationEnvironmentImpl @@ -29,25 +30,26 @@ class AMQPClientSerializationScheme( companion object { /** Call from main only. */ - fun initialiseSerialization() { - nodeSerializationEnv = createSerializationEnv() + fun initialiseSerialization(classLoader: ClassLoader? = null) { + nodeSerializationEnv = createSerializationEnv(classLoader) } - fun createSerializationEnv(): SerializationEnvironment { + fun createSerializationEnv(classLoader: ClassLoader? = null): SerializationEnvironment { return SerializationEnvironmentImpl( SerializationFactoryImpl().apply { registerScheme(AMQPClientSerializationScheme(emptyList())) }, storageContext = AMQP_STORAGE_CONTEXT, - p2pContext = AMQP_P2P_CONTEXT, + p2pContext = if (classLoader != null) AMQP_P2P_CONTEXT.withClassLoader(classLoader) else AMQP_P2P_CONTEXT, rpcClientContext = AMQP_RPC_CLIENT_CONTEXT, - rpcServerContext = AMQP_RPC_SERVER_CONTEXT) + rpcServerContext = AMQP_RPC_SERVER_CONTEXT + ) } } - override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase) = - magic == amqpMagic && ( - target == SerializationContext.UseCase.RPCClient || target == SerializationContext.UseCase.P2P) + override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean { + return magic == amqpMagic && (target == UseCase.RPCClient || target == UseCase.P2P) + } override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory { return SerializerFactory(context.whitelist, ClassLoader.getSystemClassLoader(), context.lenientCarpenterEnabled).apply { @@ -60,4 +62,4 @@ class AMQPClientSerializationScheme( override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory { throw UnsupportedOperationException() } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt index 49864ef3b5..7ad5129538 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt @@ -24,9 +24,8 @@ data class TransactionState @JvmOverloads constructor( * Currently these are loaded from the classpath of the node which includes the cordapp directory - at some * point these will also be loaded and run from the attachment store directly, allowing contracts to be * sent across, and run, from the network from within a sandbox environment. - * - * TODO: Implement the contract sandbox loading of the contract attachments - * */ + */ + // TODO: Implement the contract sandbox loading of the contract attachments val contract: ContractClassName, /** Identity of the notary that ensures the state is not used as an input to a transaction more than once */ val notary: Party, diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt index 8871907eed..8be1bab049 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt @@ -74,11 +74,11 @@ open class SerializerFactory( @DeleteForDJVM constructor(whitelist: ClassWhitelist, - classLoader: ClassLoader, + carpenterClassLoader: ClassLoader, lenientCarpenter: Boolean = false, evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(), fingerPrinter: FingerPrinter = SerializerFingerPrinter() - ) : this(whitelist, ClassCarpenterImpl(whitelist, classLoader, lenientCarpenter), evolutionSerializerGetter, fingerPrinter) + ) : this(whitelist, ClassCarpenterImpl(whitelist, carpenterClassLoader, lenientCarpenter), evolutionSerializerGetter, fingerPrinter) init { fingerPrinter.setOwner(this) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ProcessUtilities.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ProcessUtilities.kt index 4bb77cd503..309c3af8b3 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ProcessUtilities.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ProcessUtilities.kt @@ -1,17 +1,16 @@ package net.corda.testing.node.internal import net.corda.core.internal.div -import net.corda.core.internal.exists -import java.io.File.pathSeparator import java.nio.file.Path object ProcessUtilities { inline fun startJavaProcess( arguments: List, + classpath: String = defaultClassPath, jdwpPort: Int? = null, extraJvmArguments: List = emptyList() ): Process { - return startJavaProcessImpl(C::class.java.name, arguments, defaultClassPath, jdwpPort, extraJvmArguments, null, null) + return startJavaProcessImpl(C::class.java.name, arguments, classpath, jdwpPort, extraJvmArguments, null, null) } fun startCordaProcess( diff --git a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt index a339d1040a..37b994641d 100644 --- a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt +++ b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt @@ -8,6 +8,7 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.testing.common.internal.asContextEnv +import net.corda.testing.common.internal.checkNotOnClasspath import net.corda.testing.common.internal.testNetworkParameters import java.nio.file.Path import java.nio.file.Paths @@ -67,11 +68,8 @@ class NodeProcess( } init { - try { - Class.forName("net.corda.node.Corda") - throw Error("Smoke test has the node in its classpath. Please remove the offending dependency.") - } catch (e: ClassNotFoundException) { - // If the class can't be found then we're good! + checkNotOnClasspath("net.corda.node.Corda") { + "Smoke test has the node in its classpath. Please remove the offending dependency." } } } diff --git a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/TestCommonUtils.kt b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/TestCommonUtils.kt new file mode 100644 index 0000000000..5ae693498e --- /dev/null +++ b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/TestCommonUtils.kt @@ -0,0 +1,10 @@ +package net.corda.testing.common.internal + +inline fun checkNotOnClasspath(className: String, errorMessage: () -> Any) { + try { + Class.forName(className) + throw IllegalStateException(errorMessage().toString()) + } catch (e: ClassNotFoundException) { + // If the class can't be found then we're good! + } +} diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt index 1afe13d0da..21814df176 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt @@ -15,18 +15,10 @@ import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.UniqueIdentifier import net.corda.core.flows.FlowLogic -import net.corda.core.internal.Emoji +import net.corda.core.internal.* import net.corda.core.internal.concurrent.doneFuture import net.corda.core.internal.concurrent.openFuture -import net.corda.core.internal.createDirectories -import net.corda.core.internal.div -import net.corda.core.internal.rootCause -import net.corda.core.internal.uncheckedCast -import net.corda.core.messaging.CordaRPCOps -import net.corda.core.messaging.DataFeed -import net.corda.core.messaging.FlowProgressHandle -import net.corda.core.messaging.StateMachineUpdate -import net.corda.core.messaging.pendingFlowsCount +import net.corda.core.messaging.* import net.corda.tools.shell.utlities.ANSIProgressRenderer import net.corda.tools.shell.utlities.StdoutANSIProgressRenderer import org.crsh.command.InvocationContext @@ -131,8 +123,7 @@ object InteractiveShell { config["crash.ssh.port"] = configuration.sshdPort?.toString() config["crash.auth"] = "corda" configuration.sshHostKeyDirectory?.apply { - val sshKeysDir = configuration.sshHostKeyDirectory - sshKeysDir.createDirectories() + val sshKeysDir = configuration.sshHostKeyDirectory.createDirectories() config["crash.ssh.keypath"] = (sshKeysDir / "hostkey.pem").toString() config["crash.ssh.keygen"] = "true" } @@ -275,7 +266,7 @@ object InteractiveShell { val stateObservable = runFlowFromString({ clazz, args -> rpcOps.startTrackedFlowDynamic(clazz, *args) }, inputData, flowClazz, om) val latch = CountDownLatch(1) - ansiProgressRenderer.render(stateObservable, { latch.countDown() }) + ansiProgressRenderer.render(stateObservable, latch::countDown) // Wait for the flow to end and the progress tracker to notice. By the time the latch is released // the tracker is done with the screen. while (!Thread.currentThread().isInterrupted) { @@ -291,11 +282,7 @@ object InteractiveShell { } } } - stateObservable.returnValue.get()?.apply { - if (this !is Throwable) { - output.println("Flow completed with result: $this") - } - } + output.println("Flow completed with result: ${stateObservable.returnValue.get()}") } catch (e: NoApplicableConstructor) { output.println("No matching constructor found:", Color.red) e.errors.forEach { output.println("- $it", Color.red) }