diff --git a/.ci/api-current.txt b/.ci/api-current.txt index d3363ad577..a4e640eb8e 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -2532,7 +2532,6 @@ public interface net.corda.core.messaging.CordaRPCOps extends net.corda.core.mes public abstract void clearNetworkMapCache() @NotNull public abstract java.time.Instant currentNodeTime() - public abstract int getProtocolVersion() @NotNull public abstract Iterable<String> getVaultTransactionNotes(net.corda.core.crypto.SecureHash) @RPCReturnsObservables @@ -4449,8 +4448,6 @@ public interface net.corda.core.serialization.SerializationCustomSerializer public abstract PROXY toProxy(OBJ) ## public final class net.corda.core.serialization.SerializationDefaults extends java.lang.Object - @NotNull - public final net.corda.core.serialization.SerializationContext getCHECKPOINT_CONTEXT() @NotNull public final net.corda.core.serialization.SerializationContext getP2P_CONTEXT() @NotNull @@ -6887,8 +6884,6 @@ public final class net.corda.testing.core.SerializationEnvironmentRule extends j @NotNull public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement, org.junit.runner.Description) @NotNull - public final net.corda.core.serialization.SerializationContext getCheckpointContext() - @NotNull public final net.corda.core.serialization.SerializationFactory getSerializationFactory() public static final net.corda.testing.core.SerializationEnvironmentRule$Companion Companion ## diff --git a/.ci/ci-gradle-build-cache-init.sh b/.ci/ci-gradle-build-cache-init.sh new file mode 100755 index 0000000000..1b076beb0d --- /dev/null +++ b/.ci/ci-gradle-build-cache-init.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +export GRADLE_BUILD_CACHE_URL="${GRADLE_BUILD_CACHE_URL:-http://localhost:5071/cache/}" +export USE_GRADLE_DAEMON="${USE_GRADLE_DAEMON:-false}" +export GRADLE_CACHE_DEBUG="${GRADLE_CACHE_DEBUG:-false}" +export PERFORM_GRADLE_SCAN="${PERFORM_GRADLE_SCAN:---scan}" + +# cd %teamcity.build.checkoutDir% +echo "Using Gradle Build Cache: $GRADLE_BUILD_CACHE_URL" \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml index ef6407f0b3..d981d5fd35 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -235,4 +235,4 @@ <component name="JavacSettings"> <option name="ADDITIONAL_OPTIONS_STRING" value="-parameters" /> </component> -</project> \ No newline at end of file +</project> diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index bf3094165c..31cdf7d71c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -84,6 +84,7 @@ see changes to this list. * Giulio Katis (Westpac) * Giuseppe Cardone (Intesa Sanpaolo) * Guy Hochstetler (R3) +* Hristo Gatsinski (Industria) * Ian Cusden (UBS) * Ian Grigg (R3) * Igor Nitto (R3) @@ -91,6 +92,7 @@ see changes to this list. * Ivan Schasny (R3) * James Brown (R3) * James Carlyle (R3) +* Janis Olekss (Accenture) * Jared Harwayne-Gidansky (BNY Mellon) * Jayavaradhan Sambedu (Société Générale) * Joel Dudley (R3) @@ -116,7 +118,7 @@ see changes to this list. * Lars Stage Thomsen (Danske Bank) * Lee Braine (Barclays) * Lucas Salmen (Itau) -* Lulu Ren (S-Labs) +* Lulu Ren (Monad-Labs) * Maksymilian Pawlak (R3) * Marek Scocovsky (ABSA) * marekdapps @@ -137,6 +139,7 @@ see changes to this list. * Mike Hearn (R3) * Mike Ward (R3) * Mike Reichelt (US Bank) +* Milen Dobrinov (Industria) * Mohamed Amine LEGHERABA * Mustafa Ozturk (Natixis) * Nick Skinner (Northern Trust) @@ -145,6 +148,7 @@ see changes to this list. * Nuam Athaweth (MUFG) * Oscar Zibordi de Paiva (Scopus Soluções em TI) * OP Financial +* Parnika Sharma (BCS Technology) * Patrick Kuo (R3) * Pekka Kaipio (OP Financial) * Phillip Griffin @@ -176,6 +180,7 @@ see changes to this list. * Scott James * Sean Zhang (Wells Fargo) * Shams Asari (R3) +* Shivan Sawant (Persistent Systems Limited) * Siddhartha Sengupta (Tradewind Markets) * Simon Taylor (Barclays) * Sofus Mortensen (Digital Asset Holdings) diff --git a/build.gradle b/build.gradle index ae3ed9e67c..d99ec0aa13 100644 --- a/build.gradle +++ b/build.gradle @@ -17,9 +17,9 @@ buildscript { ext.quasar_group = 'co.paralleluniverse' ext.quasar_version = '0.7.10' - // gradle-capsule-plugin:1.0.2 contains capsule:1.0.1 - // TODO: Upgrade gradle-capsule-plugin to a version with capsule:1.0.3 - ext.capsule_version = '1.0.1' + // gradle-capsule-plugin:1.0.2 contains capsule:1.0.1 by default. + // We must configure it manually to use the latest capsule version. + ext.capsule_version = '1.0.3' ext.asm_version = '5.0.4' ext.artemis_version = '2.6.2' @@ -32,6 +32,7 @@ buildscript { ext.bouncycastle_version = constants.getProperty("bouncycastleVersion") ext.guava_version = constants.getProperty("guavaVersion") ext.caffeine_version = constants.getProperty("caffeineVersion") + ext.disruptor_version = constants.getProperty("disruptorVersion") ext.metrics_version = constants.getProperty("metricsVersion") ext.metrics_new_relic_version = constants.getProperty("metricsNewRelicVersion") ext.okhttp_version = '3.5.0' @@ -46,7 +47,7 @@ buildscript { ext.hibernate_version = '5.3.6.Final' ext.h2_version = '1.4.197' // Update docs if renamed or removed. ext.postgresql_version = '42.1.4' - ext.rxjava_version = '1.2.4' + ext.rxjava_version = '1.3.8' ext.dokka_version = '0.9.17' ext.eddsa_version = '0.2.0' ext.dependency_checker_version = '3.1.0' @@ -55,9 +56,8 @@ buildscript { ext.crash_version = 'cadb53544fbb3c0fb901445da614998a6a419488' ext.jsr305_version = constants.getProperty("jsr305Version") ext.shiro_version = '1.4.0' - ext.shadow_version = '2.0.4' ext.artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion') - ext.liquibase_version = '3.6.2' + ext.liquibase_version = '3.5.5' ext.artifactory_contextUrl = 'https://ci-artifactory.corda.r3cev.com/artifactory' ext.snake_yaml_version = constants.getProperty('snakeYamlVersion') ext.docker_compose_rule_version = '0.33.0' @@ -78,6 +78,7 @@ buildscript { // Update 121 is required for ObjectInputFilter. // Updates [131, 161] also have zip compression bugs on MacOS (High Sierra). + // when the java version in NodeStartup.hasMinimumJavaVersion() changes, so must this check ext.java8_minUpdateVersion = '171' repositories { @@ -115,8 +116,12 @@ buildscript { plugins { // TODO The capsule plugin requires the newer DSL plugin block.It would be nice if we could unify all the plugins into one style, // but the DSL has some restrictions e.g can't be used on the allprojects section. So we should revisit this if there are improvements in Gradle. - // Version 1.0.2 of this plugin uses capsule:1.0.1 - id "us.kirchmeier.capsule" version "1.0.2" + // Version 1.0.2 of this plugin uses capsule:1.0.1 by default. + id 'us.kirchmeier.capsule' version '1.0.2' apply false + + // Add the shadow plugin to the plugins classpath for the entire project. + id 'com.github.johnrengelman.shadow' version '2.0.4' apply false + id "com.gradle.build-scan" version "1.16" } ext { @@ -195,11 +200,18 @@ allprojects { if (System.getProperty("test.maxParallelForks") != null) { maxParallelForks = Integer.valueOf(System.getProperty("test.maxParallelForks")) + logger.debug("System property test.maxParallelForks found - setting max parallel forks to $maxParallelForks for $project") } if (project.path.startsWith(':experimental') && System.getProperty("experimental.test.enable") == null) { enabled = false } + + // Required to use Gradle build cache (until Gradle 5.0 is released with default value of "append" set to false) + // See https://github.com/gradle/gradle/issues/5269 and https://github.com/gradle/gradle/pull/6419 + extensions.configure(TypeOf.typeOf(JacocoTaskExtension)) { ex -> + ex.append = false + } } group 'net.corda' @@ -209,6 +221,7 @@ allprojects { mavenLocal() mavenCentral() jcenter() + maven { url "$artifactory_contextUrl/corda-dependencies" } maven { url 'https://jitpack.io' } } @@ -235,6 +248,8 @@ allprojects { // We want to use SLF4J's version of these bindings: jcl-over-slf4j // Remove any transitive dependency on Apache's version. exclude group: 'commons-logging', module: 'commons-logging' + // Remove any transitive dependency on Logback (e.g. Liquibase 3.6 introduces this dependency) + exclude group: 'ch.qos.logback' // Netty-All is an uber-jar which contains every Netty module. // Exclude it to force us to use the individual Netty modules instead. @@ -255,12 +270,6 @@ allprojects { if (!JavaVersion.current().java8Compatible) throw new GradleException("Corda requires Java 8, please upgrade to at least 1.8.0_$java8_minUpdateVersion") -repositories { - mavenLocal() - mavenCentral() - jcenter() -} - // Required for building out the fat JAR. dependencies { compile project(':node') @@ -333,6 +342,8 @@ bintrayConfig { 'corda-rpc', 'corda-core', 'corda-core-deterministic', + 'corda-deterministic-verifier', + 'corda-djvm', 'corda', 'corda-finance', 'corda-node', @@ -350,7 +361,8 @@ bintrayConfig { 'corda-serialization-deterministic', 'corda-tools-blob-inspector', 'corda-tools-explorer', - 'corda-tools-network-bootstrapper' + 'corda-tools-network-bootstrapper', + 'corda-tools-cliutils' ] license { name = 'Apache-2.0' @@ -382,7 +394,7 @@ artifactory { contextUrl = artifactory_contextUrl repository { repoKey = 'corda-dev' - username = 'teamcity' + username = System.getenv('CORDA_ARTIFACTORY_USERNAME') password = System.getenv('CORDA_ARTIFACTORY_PASSWORD') } @@ -431,6 +443,11 @@ if (file('corda-docs-only-build').exists() || (System.getenv('CORDA_DOCS_ONLY_BU } wrapper { - gradleVersion = "4.8.1" + gradleVersion = "4.10.1" distributionType = Wrapper.DistributionType.ALL } + +buildScan { + termsOfServiceUrl = 'https://gradle.com/terms-of-service' + termsOfServiceAgree = 'yes' +} \ No newline at end of file diff --git a/buildCacheSettings.gradle b/buildCacheSettings.gradle new file mode 100644 index 0000000000..fcfc1513bf --- /dev/null +++ b/buildCacheSettings.gradle @@ -0,0 +1,15 @@ +// Gradle Build Cache configuration recommendation: https://docs.gradle.org/current/userguide/build_cache.html +ext { + isCiServer = System.getenv().containsKey("CORDA_CI") + gradleBuildCacheURL = System.getenv().containsKey("GRADLE_BUILD_CACHE_URL") ? System.getenv().get("GRADLE_BUILD_CACHE_URL") : 'http://localhost:5071/cache/' +} + +buildCache { + local { + enabled = !isCiServer + } + remote(HttpBuildCache) { + url = gradleBuildCacheURL + push = isCiServer + } +} diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle index c46d96de90..2449091bd9 100644 --- a/buildSrc/settings.gradle +++ b/buildSrc/settings.gradle @@ -1,2 +1,4 @@ rootProject.name = 'buildSrc' include 'canonicalizer' + +apply from: '../buildCacheSettings.gradle' \ No newline at end of file diff --git a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt index ae64ab4004..3f5546b2e9 100644 --- a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt +++ b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt @@ -197,7 +197,7 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: fun DigitalSignatureWithCert() { val digitalSignature = DigitalSignatureWithCert(MINI_CORP.identity.certificate, secureRandomBytes(128)) val json = mapper.valueToTree<ObjectNode>(digitalSignature) - val (by, bytes) = json.assertHasOnlyFields("by", "bytes") + val (by, bytes) = json.assertHasOnlyFields("by", "bytes", "parentCertsChain") assertThat(by.valueAs<X509Certificate>(mapper)).isEqualTo(MINI_CORP.identity.certificate) assertThat(bytes.binaryValue()).isEqualTo(digitalSignature.bytes) assertThat(mapper.convertValue<DigitalSignatureWithCert>(json)).isEqualTo(digitalSignature) @@ -610,7 +610,8 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: assertThat(json["serialNumber"].bigIntegerValue()).isEqualTo(cert.serialNumber) assertThat(json["issuer"].valueAs<X500Principal>(mapper)).isEqualTo(cert.issuerX500Principal) assertThat(json["subject"].valueAs<X500Principal>(mapper)).isEqualTo(cert.subjectX500Principal) - assertThat(json["publicKey"].valueAs<PublicKey>(mapper)).isEqualTo(cert.publicKey) + // cert.publicKey should be converted to a supported format (this is required because [Certificate] returns keys as SUN EC keys, not BC). + assertThat(json["publicKey"].valueAs<PublicKey>(mapper)).isEqualTo(Crypto.toSupportedPublicKey(cert.publicKey)) assertThat(json["notAfter"].valueAs<Date>(mapper)).isEqualTo(cert.notAfter) assertThat(json["notBefore"].valueAs<Date>(mapper)).isEqualTo(cert.notBefore) assertThat(json["encoded"].binaryValue()).isEqualTo(cert.encoded) diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt index 0b09957678..aa7f7e50f2 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt @@ -1,5 +1,6 @@ package net.corda.client.jfx.model +import com.github.benmanes.caffeine.cache.CacheLoader import com.github.benmanes.caffeine.cache.Caffeine import javafx.beans.value.ObservableValue import javafx.collections.FXCollections @@ -31,7 +32,7 @@ class NetworkIdentityModel { private val rpcProxy by observableValue(NodeMonitorModel::proxyObservable) private val identityCache = Caffeine.newBuilder() - .buildNamed<PublicKey, ObservableValue<NodeInfo?>>("NetworkIdentityModel_identity", { publicKey -> + .buildNamed<PublicKey, ObservableValue<NodeInfo?>>("NetworkIdentityModel_identity", CacheLoader { publicKey: PublicKey -> publicKey.let { rpcProxy.map { it?.cordaRPCOps?.nodeInfoFromParty(AnonymousParty(publicKey)) } } }) val notaries = ChosenList(rpcProxy.map { FXCollections.observableList(it?.cordaRPCOps?.notaryIdentities() ?: emptyList()) }, "notaries") diff --git a/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java b/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java index 44f65f0bc0..366fb6802e 100644 --- a/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java +++ b/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java @@ -55,7 +55,7 @@ public class CordaRPCJavaClientTest extends NodeBasedTest { @Before public void setUp() throws Exception { - node = startNode(ALICE_NAME, 1, singletonList(rpcUser)); + node = startNode(ALICE_NAME, 1000, singletonList(rpcUser)); client = new CordaRPCClient(requireNonNull(node.getNode().getConfiguration().getRpcOptions().getAddress())); } diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt index 6ceb5c2cad..b05e33c729 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt @@ -47,7 +47,7 @@ class RPCStabilityTests { } object DummyOps : RPCOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 } private fun waitUntilNumberOfThreadsStable(executorService: ScheduledExecutorService): Map<Thread, List<StackTraceElement>> { @@ -107,7 +107,7 @@ class RPCStabilityTests { Try.on { startRpcClient<RPCOps>( server.get().broker.hostAndPort!!, - configuration = CordaRPCClientConfiguration.DEFAULT.copy(minimumServerProtocolVersion = 1) + configuration = CordaRPCClientConfiguration.DEFAULT.copy(minimumServerProtocolVersion = 1000) ).get() } } @@ -203,7 +203,7 @@ class RPCStabilityTests { rpcDriver { val leakObservableOpsImpl = object : LeakObservableOps { val leakedUnsubscribedCount = AtomicInteger(0) - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun leakObservable(): Observable<Nothing> { return PublishSubject.create<Nothing>().doOnUnsubscribe { leakedUnsubscribedCount.incrementAndGet() @@ -234,7 +234,7 @@ class RPCStabilityTests { fun `client reconnects to rebooted server`() { rpcDriver { val ops = object : ReconnectOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun ping() = "pong" } @@ -259,7 +259,7 @@ class RPCStabilityTests { fun `connection failover fails, rpc calls throw`() { rpcDriver { val ops = object : ReconnectOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun ping() = "pong" } @@ -290,7 +290,7 @@ class RPCStabilityTests { fun `observables error when connection breaks`() { rpcDriver { val ops = object : NoOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun subscribe(): Observable<Nothing> { return PublishSubject.create<Nothing>() } @@ -350,7 +350,7 @@ class RPCStabilityTests { fun `client connects to first available server`() { rpcDriver { val ops = object : ServerOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun serverId() = "server" } val serverFollower = shutdownManager.follower() @@ -371,15 +371,15 @@ class RPCStabilityTests { fun `3 server failover`() { rpcDriver { val ops1 = object : ServerOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun serverId() = "server1" } val ops2 = object : ServerOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun serverId() = "server2" } val ops3 = object : ServerOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun serverId() = "server3" } val serverFollower1 = shutdownManager.follower() @@ -443,7 +443,7 @@ class RPCStabilityTests { fun `server cleans up queues after disconnected clients`() { rpcDriver { val trackSubscriberOpsImpl = object : TrackSubscriberOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 val subscriberCount = AtomicInteger(0) val trackSubscriberCountObservable = UnicastSubject.create<Unit>().share(). doOnSubscribe { subscriberCount.incrementAndGet() }. @@ -486,7 +486,7 @@ class RPCStabilityTests { } class SlowConsumerRPCOpsImpl : SlowConsumerRPCOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun streamAtInterval(interval: Duration, size: Int): Observable<ByteArray> { val chunk = ByteArray(size) @@ -587,7 +587,7 @@ class RPCStabilityTests { val request = RPCApi.ClientToServer.fromClientMessage(it) when (request) { is RPCApi.ClientToServer.RpcRequest -> { - val reply = RPCApi.ServerToClient.RpcReply(request.replyId, Try.Success(0), "server") + val reply = RPCApi.ServerToClient.RpcReply(request.replyId, Try.Success(1000), "server") val message = session.createMessage(false) reply.writeToClientMessage(SerializationDefaults.RPC_SERVER_CONTEXT, message) message.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, dedupeId.getAndIncrement()) 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 e41a7ed75c..116e6baf84 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 @@ -4,15 +4,16 @@ import net.corda.client.rpc.internal.RPCClient import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme import net.corda.core.context.Actor import net.corda.core.context.Trace +import net.corda.core.identity.CordaX500Name import net.corda.core.messaging.CordaRPCOps import net.corda.core.serialization.internal.effectiveSerializationEnv import net.corda.core.utilities.NetworkHostAndPort -import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport import net.corda.core.messaging.ClientRpcSslOptions import net.corda.core.utilities.days import net.corda.core.utilities.minutes import net.corda.core.utilities.seconds -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport +import net.corda.nodeapi.internal.PLATFORM_VERSION import net.corda.serialization.internal.AMQP_RPC_CLIENT_CONTEXT import java.time.Duration @@ -29,65 +30,76 @@ class CordaRPCConnection internal constructor(connection: RPCConnection<CordaRPC open class CordaRPCClientConfiguration @JvmOverloads constructor( /** - * Maximum retry interval. + * The maximum retry interval for re-connections. The client will retry connections if the host is lost with + * ever increasing spacing until the max is reached. The default is 3 minutes. */ open val connectionMaxRetryInterval: Duration = 3.minutes, /** - * The minimum protocol version required from the server. + * The minimum protocol version required from the server. This is equivalent to the node's platform version + * number. If this minimum version is not met, an exception will be thrown at startup. If you use features + * introduced in a later version, you can bump this to match the platform version you need and get an early + * check that runs before you do anything. + * + * If you leave it at the default then things will work but attempting to use an RPC added in a version later + * than the server supports will throw [UnsupportedOperationException]. + * + * The default value is whatever version of Corda this RPC library was shipped as a part of. Therefore if you + * use the RPC library from Corda 4, it will by default only connect to a node of version 4 or above. */ - open val minimumServerProtocolVersion: Int = 0, + open val minimumServerProtocolVersion: Int = PLATFORM_VERSION, /** - * If set to true the client will track RPC call sites. If an error occurs subsequently during the RPC or in a - * returned Observable stream the stack trace of the originating RPC will be shown as well. Note that - * constructing call stacks is a moderately expensive operation. + * If set to true the client will track RPC call sites (default is false). If an error occurs subsequently + * during the RPC or in a returned Observable stream the stack trace of the originating RPC will be shown as + * well. Note that constructing call stacks is a moderately expensive operation. */ - open val trackRpcCallSites: Boolean = false, + open val trackRpcCallSites: Boolean = java.lang.Boolean.getBoolean("net.corda.client.rpc.trackRpcCallSites"), /** * The interval of unused observable reaping. Leaked Observables (unused ones) are detected using weak references * and are cleaned up in batches in this interval. If set too large it will waste server side resources for this - * duration. If set too low it wastes client side cycles. + * duration. If set too low it wastes client side cycles. The default is to check once per second. */ open val reapInterval: Duration = 1.seconds, /** - * The number of threads to use for observations (for executing [Observable.onNext]). + * The number of threads to use for observations for executing [Observable.onNext]. This only has any effect + * if [observableExecutor] is null (which is the default). The default is 4. */ open val observationExecutorPoolSize: Int = 4, /** - * Determines the concurrency level of the Observable Cache. This is exposed because it implicitly determines - * the limit on the number of leaked observables reaped because of garbage collection per reaping. - * See the implementation of [com.google.common.cache.LocalCache] for details. + * This property is no longer used and has no effect. + * @suppress */ + @Deprecated("This field is no longer used and has no effect.") open val cacheConcurrencyLevel: Int = 1, /** - * The retry interval of Artemis connections in milliseconds. + * The base retry interval for reconnection attempts. The default is 5 seconds. */ open val connectionRetryInterval: Duration = 5.seconds, /** - * The retry interval multiplier for exponential backoff. + * The retry interval multiplier for exponential backoff. The default is 1.5 */ open val connectionRetryIntervalMultiplier: Double = 1.5, /** - * Maximum reconnect attempts on failover> + * Maximum reconnect attempts on failover or disconnection. The default is -1 which means unlimited. */ open val maxReconnectAttempts: Int = unlimitedReconnectAttempts, /** - * Maximum file size, in bytes. + * Maximum size of RPC responses, in bytes. Default is 10mb. */ open val maxFileSize: Int = 10485760, // 10 MiB maximum allowed file size for attachments, including message headers. // TODO: acquire this value from Network Map when supported. /** - * The cache expiry of a deduplication watermark per client. + * The cache expiry of a deduplication watermark per client. Default is 1 day. */ open val deduplicationCacheExpiry: Duration = 1.days @@ -97,6 +109,7 @@ open class CordaRPCClientConfiguration @JvmOverloads constructor( private const val unlimitedReconnectAttempts = -1 + /** Provides an instance of this class with the parameters set to our recommended defaults. */ @JvmField val DEFAULT: CordaRPCClientConfiguration = CordaRPCClientConfiguration() @@ -104,7 +117,10 @@ open class CordaRPCClientConfiguration @JvmOverloads constructor( /** * Create a new copy of a configuration object with zero or more parameters modified. + * + * @suppress */ + @Suppress("DEPRECATION") @JvmOverloads fun copy( connectionMaxRetryInterval: Duration = this.connectionMaxRetryInterval, @@ -169,6 +185,7 @@ open class CordaRPCClientConfiguration @JvmOverloads constructor( return result } + @Suppress("DEPRECATION") override fun toString(): String { return "CordaRPCClientConfiguration(" + "connectionMaxRetryInterval=$connectionMaxRetryInterval, " + @@ -180,7 +197,8 @@ open class CordaRPCClientConfiguration @JvmOverloads constructor( "deduplicationCacheExpiry=$deduplicationCacheExpiry)" } - // Left is for backwards compatibility with version 3.1 + // Left in for backwards compatibility with version 3.1 + @Deprecated("Binary compatibility stub") operator fun component1() = connectionMaxRetryInterval } @@ -226,10 +244,8 @@ class CordaRPCClient private constructor( private val hostAndPort: NetworkHostAndPort, private val configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT, private val sslConfiguration: ClientRpcSslOptions? = null, - private val nodeSslConfiguration: SSLConfiguration? = null, private val classLoader: ClassLoader? = null, - private val haAddressPool: List<NetworkHostAndPort> = emptyList(), - private val internalConnection: Boolean = false + private val haAddressPool: List<NetworkHostAndPort> = emptyList() ) { @JvmOverloads constructor(hostAndPort: NetworkHostAndPort, @@ -243,7 +259,7 @@ class CordaRPCClient private constructor( * @param configuration An optional configuration used to tweak client behaviour. */ @JvmOverloads - constructor(haAddressPool: List<NetworkHostAndPort>, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT) : this(haAddressPool.first(), configuration, null, null, null, haAddressPool) + constructor(haAddressPool: List<NetworkHostAndPort>, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT) : this(haAddressPool.first(), configuration, null, null, haAddressPool) companion object { fun createWithSsl( @@ -268,16 +284,7 @@ class CordaRPCClient private constructor( sslConfiguration: ClientRpcSslOptions? = null, classLoader: ClassLoader? = null ): CordaRPCClient { - return CordaRPCClient(hostAndPort, configuration, sslConfiguration, null, classLoader) - } - - internal fun createWithInternalSslAndClassLoader( - hostAndPort: NetworkHostAndPort, - configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT, - sslConfiguration: SSLConfiguration?, - classLoader: ClassLoader? = null - ): CordaRPCClient { - return CordaRPCClient(hostAndPort, configuration, null, sslConfiguration, classLoader, internalConnection = true) + return CordaRPCClient(hostAndPort, configuration, sslConfiguration, classLoader) } } @@ -295,9 +302,6 @@ class CordaRPCClient private constructor( private fun getRpcClient(): RPCClient<CordaRPCOps> { return when { - // Node->RPC broker, mutually authenticated SSL. This is used when connecting the integrated shell - internalConnection == true -> RPCClient(hostAndPort, nodeSslConfiguration!!) - // Client->RPC broker haAddressPool.isEmpty() -> RPCClient( rpcConnectorTcpTransport(hostAndPort, config = sslConfiguration), @@ -326,6 +330,21 @@ class CordaRPCClient private constructor( return start(username, password, null, null) } + /** + * Logs in to the target server and returns an active connection. The returned connection is a [java.io.Closeable] + * and can be used with a try-with-resources statement. If you don't use that, you should use the + * [RPCConnection.notifyServerAndClose] or [RPCConnection.forceClose] methods to dispose of the connection object + * when done. + * + * @param username The username to authenticate with. + * @param password The password to authenticate with. + * @param targetLegalIdentity in case of multi-identity RPC endpoint specific legal identity to which the calls must be addressed. + * @throws RPCException if the server version is too low or if the server isn't reachable within a reasonable timeout. + */ + fun start(username: String, password: String, targetLegalIdentity: CordaX500Name): CordaRPCConnection { + return start(username, password, null, null, targetLegalIdentity) + } + /** * Logs in to the target server and returns an active connection. The returned connection is a [java.io.Closeable] * and can be used with a try-with-resources statement. If you don't use that, you should use the @@ -335,10 +354,28 @@ class CordaRPCClient private constructor( * @param username The username to authenticate with. * @param password The password to authenticate with. * @param externalTrace external [Trace] for correlation. + * @param impersonatedActor the actor on behalf of which all the invocations will be made. * @throws RPCException if the server version is too low or if the server isn't reachable within a reasonable timeout. */ fun start(username: String, password: String, externalTrace: Trace?, impersonatedActor: Actor?): CordaRPCConnection { - return CordaRPCConnection(getRpcClient().start(CordaRPCOps::class.java, username, password, externalTrace, impersonatedActor)) + return start(username, password, externalTrace, impersonatedActor, null) + } + + /** + * Logs in to the target server and returns an active connection. The returned connection is a [java.io.Closeable] + * and can be used with a try-with-resources statement. If you don't use that, you should use the + * [RPCConnection.notifyServerAndClose] or [RPCConnection.forceClose] methods to dispose of the connection object + * when done. + * + * @param username The username to authenticate with. + * @param password The password to authenticate with. + * @param externalTrace external [Trace] for correlation. + * @param impersonatedActor the actor on behalf of which all the invocations will be made. + * @param targetLegalIdentity in case of multi-identity RPC endpoint specific legal identity to which the calls must be addressed. + * @throws RPCException if the server version is too low or if the server isn't reachable within a reasonable timeout. + */ + fun start(username: String, password: String, externalTrace: Trace?, impersonatedActor: Actor?, targetLegalIdentity: CordaX500Name?): CordaRPCConnection { + return CordaRPCConnection(getRpcClient().start(CordaRPCOps::class.java, username, password, externalTrace, impersonatedActor, targetLegalIdentity)) } /** diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/RPCException.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/RPCException.kt index 32ea9928be..7dc04f1666 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/RPCException.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/RPCException.kt @@ -3,9 +3,13 @@ package net.corda.client.rpc import net.corda.core.CordaRuntimeException /** - * Thrown to indicate a fatal error in the RPC system itself, as opposed to an error generated by the invoked - * method. + * Thrown to indicate a fatal error in the RPC system itself, as opposed to an error generated by the invoked method. */ open class RPCException(message: String?, cause: Throwable?) : CordaRuntimeException(message, cause) { constructor(msg: String) : this(msg, null) } + +/** + * Signals that the underlying [RPCConnection] dropped. + */ +open class ConnectionFailureException(cause: Throwable? = null) : RPCException("Connection failure detected.", cause) \ No newline at end of file diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt index 7781d0f135..86a74d1728 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt @@ -2,12 +2,8 @@ package net.corda.client.rpc.internal import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClientConfiguration -import net.corda.core.messaging.CordaRPCOps -import net.corda.core.messaging.pendingFlowsCount import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.messaging.ClientRpcSslOptions -import net.corda.nodeapi.internal.config.SSLConfiguration -import rx.Observable /** Utility which exposes the internal Corda RPC constructor to other internal Corda components */ fun createCordaRPCClientWithSslAndClassLoader( @@ -15,21 +11,4 @@ fun createCordaRPCClientWithSslAndClassLoader( configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT, sslConfiguration: ClientRpcSslOptions? = null, classLoader: ClassLoader? = null -) = CordaRPCClient.createWithSslAndClassLoader(hostAndPort, configuration, sslConfiguration, classLoader) - -fun createCordaRPCClientWithInternalSslAndClassLoader( - hostAndPort: NetworkHostAndPort, - configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT, - sslConfiguration: SSLConfiguration? = null, - classLoader: ClassLoader? = null -) = CordaRPCClient.createWithInternalSslAndClassLoader(hostAndPort, configuration, sslConfiguration, classLoader) - -fun CordaRPCOps.drainAndShutdown(): Observable<Unit> { - - setFlowsDrainingModeEnabled(true) - return pendingFlowsCount().updates - .doOnError { error -> - throw error - } - .doOnCompleted { shutdown() }.map { } -} \ No newline at end of file +) = CordaRPCClient.createWithSslAndClassLoader(hostAndPort, configuration, sslConfiguration, classLoader) \ No newline at end of file diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt index 1d7969caaa..a29c481d47 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt @@ -6,6 +6,7 @@ import net.corda.client.rpc.RPCException import net.corda.core.context.Actor import net.corda.core.context.Trace import net.corda.core.crypto.random63BitValue +import net.corda.core.identity.CordaX500Name import net.corda.core.internal.logElapsedTime import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.ClientRpcSslOptions @@ -15,11 +16,11 @@ import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger -import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport -import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcConnectorTcpTransportsFromList -import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcInternalClientTcpTransport import net.corda.nodeapi.RPCApi -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcConnectorTcpTransportsFromList +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcInternalClientTcpTransport +import net.corda.nodeapi.internal.config.SslConfiguration import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.TransportConfiguration import org.apache.activemq.artemis.api.core.client.ActiveMQClient @@ -43,7 +44,7 @@ class RPCClient<I : RPCOps>( constructor( hostAndPort: NetworkHostAndPort, - sslConfiguration: SSLConfiguration, + sslConfiguration: SslConfiguration, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT, serializationContext: SerializationContext = SerializationDefaults.RPC_CLIENT_CONTEXT ) : this(rpcInternalClientTcpTransport(hostAndPort, sslConfiguration), configuration, serializationContext) @@ -65,7 +66,8 @@ class RPCClient<I : RPCOps>( username: String, password: String, externalTrace: Trace? = null, - impersonatedActor: Actor? = null + impersonatedActor: Actor? = null, + targetLegalIdentity: CordaX500Name? = null ): RPCConnection<I> { return log.logElapsedTime("Startup") { val clientAddress = SimpleString("${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username.${random63BitValue()}") @@ -85,7 +87,8 @@ class RPCClient<I : RPCOps>( isUseGlobalPools = nodeSerializationEnv != null } val sessionId = Trace.SessionId.newInstance() - val proxyHandler = RPCClientProxyHandler(rpcConfiguration, username, password, serverLocator, clientAddress, rpcOpsClass, serializationContext, sessionId, externalTrace, impersonatedActor) + val proxyHandler = RPCClientProxyHandler(rpcConfiguration, username, password, serverLocator, clientAddress, + rpcOpsClass, serializationContext, sessionId, externalTrace, impersonatedActor, targetLegalIdentity) try { proxyHandler.start() val ops: I = uncheckedCast(Proxy.newProxyInstance(rpcOpsClass.classLoader, arrayOf(rpcOpsClass), proxyHandler)) diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt index f10607ad97..be74d7b316 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt @@ -7,6 +7,7 @@ import com.github.benmanes.caffeine.cache.RemovalCause import com.github.benmanes.caffeine.cache.RemovalListener import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.ThreadFactoryBuilder +import net.corda.client.rpc.ConnectionFailureException import net.corda.client.rpc.CordaRPCClientConfiguration import net.corda.client.rpc.RPCException import net.corda.client.rpc.RPCSinceVersion @@ -14,6 +15,7 @@ import net.corda.client.rpc.internal.serialization.amqp.RpcClientObservableDeSer import net.corda.core.context.Actor import net.corda.core.context.Trace import net.corda.core.context.Trace.InvocationId +import net.corda.core.identity.CordaX500Name import net.corda.core.internal.* import net.corda.core.messaging.RPCOps import net.corda.core.serialization.SerializationContext @@ -64,7 +66,7 @@ import kotlin.reflect.jvm.javaMethod * automatically signal the server. This is done using a cache that holds weak references to the [UnicastSubject]s. * The cleanup happens in batches using a dedicated reaper, scheduled on [reaperExecutor]. * - * The client will attempt to failover in case the server become unreachable. Depending on the [ServerLocataor] instance + * The client will attempt to failover in case the server become unreachable. Depending on the [ServerLocator] instance * passed in the constructor, failover is either handle at Artemis level or client level. If only one transport * was used to create the [ServerLocator], failover is handled by Artemis (retrying based on [CordaRPCClientConfiguration]. * If a list of transport configurations was used, failover is handled locally. Artemis is able to do it, however the @@ -80,7 +82,8 @@ class RPCClientProxyHandler( serializationContext: SerializationContext, private val sessionId: Trace.SessionId, private val externalTrace: Trace?, - private val impersonatedActor: Actor? + private val impersonatedActor: Actor?, + private val targetLegalIdentity: CordaX500Name? ) : InvocationHandler { private enum class State { @@ -97,12 +100,18 @@ class RPCClientProxyHandler( // To check whether toString() is being invoked val toStringMethod: Method = Object::toString.javaMethod!! - private fun addRpcCallSiteToThrowable(throwable: Throwable, callSite: Throwable) { + private fun addRpcCallSiteToThrowable(throwable: Throwable, callSite: CallSite) { var currentThrowable = throwable while (true) { val cause = currentThrowable.cause if (cause == null) { - currentThrowable.initCause(callSite) + try { + currentThrowable.initCause(callSite) + } catch (e: IllegalStateException) { + // OK, we did our best, but the first throwable with a null cause was instantiated using + // Throwable(Throwable) or Throwable(String, Throwable) which means initCause can't ever + // be called even if it was passed null. + } break } else { currentThrowable = cause @@ -146,15 +155,17 @@ class RPCClientProxyHandler( private fun createRpcObservableMap(): RpcObservableMap { val onObservableRemove = RemovalListener<InvocationId, UnicastSubject<Notification<*>>> { key, _, cause -> val observableId = key!! - val rpcCallSite = callSiteMap?.remove(observableId) + val rpcCallSite: CallSite? = callSiteMap?.remove(observableId) if (cause == RemovalCause.COLLECTED) { log.warn(listOf( "A hot observable returned from an RPC was never subscribed to.", "This wastes server-side resources because it was queueing observations for retrieval.", "It is being closed now, but please adjust your code to call .notUsed() on the observable", - "to close it explicitly. (Java users: subscribe to it then unsubscribe). This warning", - "will appear less frequently in future versions of the platform and you can ignore it", - "if you want to.").joinToString(" "), rpcCallSite) + "to close it explicitly. (Java users: subscribe to it then unsubscribe). If you aren't sure", + "where the leak is coming from, set -Dnet.corda.client.rpc.trackRpcCallSites=true on the JVM", + "command line and you will get a stack trace with this warning." + ).joinToString(" "), rpcCallSite) + rpcCallSite?.printStackTrace() } observablesToReap.locked { observables.add(observableId) } } @@ -215,6 +226,9 @@ class RPCClientProxyHandler( startSessions() } + /** A throwable that doesn't represent a real error - it's just here to wrap a stack trace. */ + class CallSite(val rpcName: String) : Throwable("<Call site of root RPC '$rpcName'>") + // This is the general function that transforms a client side RPC to internal Artemis messages. override fun invoke(proxy: Any, method: Method, arguments: Array<out Any?>?): Any? { lifeCycle.requireState { it == State.STARTED || it == State.SERVER_VERSION_NOT_SET } @@ -230,7 +244,7 @@ class RPCClientProxyHandler( throw RPCException("RPC server is not available.") val replyId = InvocationId.newInstance() - callSiteMap?.set(replyId, Throwable("<Call site of root RPC '${method.name}'>")) + callSiteMap?.set(replyId, CallSite(method.name)) try { val serialisedArguments = (arguments?.toList() ?: emptyList()).serialize(context = serializationContextWithObservableContext) val request = RPCApi.ClientToServer.RpcRequest( @@ -263,6 +277,9 @@ class RPCClientProxyHandler( private fun sendMessage(message: RPCApi.ClientToServer) { val artemisMessage = producerSession!!.createMessage(false) message.writeToClientMessage(artemisMessage) + targetLegalIdentity?.let { + artemisMessage.putStringProperty(RPCApi.RPC_TARGET_LEGAL_IDENTITY, it.toString()) + } sendExecutor!!.submit { artemisMessage.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, deduplicationSequenceNumber.getAndIncrement()) log.debug { "-> RPC -> $message" } @@ -273,7 +290,7 @@ class RPCClientProxyHandler( // The handler for Artemis messages. private fun artemisMessageHandler(message: ClientMessage) { fun completeExceptionally(id: InvocationId, e: Throwable, future: SettableFuture<Any?>?) { - val rpcCallSite: Throwable? = callSiteMap?.get(id) + val rpcCallSite: CallSite? = callSiteMap?.get(id) if (rpcCallSite != null) addRpcCallSiteToThrowable(e, rpcCallSite) future?.setException(e.cause ?: e) } @@ -536,7 +553,7 @@ class RPCClientProxyHandler( m.keys.forEach { k -> observationExecutorPool.run(k) { try { - m[k]?.onError(RPCException("Connection failure detected.")) + m[k]?.onError(ConnectionFailureException()) } catch (th: Throwable) { log.error("Unexpected exception when RPC connection failure handling", th) } @@ -545,7 +562,7 @@ class RPCClientProxyHandler( observableContext.observableMap.invalidateAll() rpcReplyMap.forEach { _, replyFuture -> - replyFuture.setException(RPCException("Connection failure detected.")) + replyFuture.setException(ConnectionFailureException()) } rpcReplyMap.clear() @@ -555,13 +572,14 @@ class RPCClientProxyHandler( private typealias RpcObservableMap = Cache<InvocationId, UnicastSubject<Notification<*>>> private typealias RpcReplyMap = ConcurrentHashMap<InvocationId, SettableFuture<Any?>> -private typealias CallSiteMap = ConcurrentHashMap<InvocationId, Throwable?> +private typealias CallSiteMap = ConcurrentHashMap<InvocationId, RPCClientProxyHandler.CallSite?> /** * Holds a context available during de-serialisation of messages that are expected to contain Observables. * - * @param observableMap holds the Observables that are ultimately exposed to the user. - * @param hardReferenceStore holds references to Observables we want to keep alive while they are subscribed to. + * @property observableMap holds the Observables that are ultimately exposed to the user. + * @property hardReferenceStore holds references to Observables we want to keep alive while they are subscribed to. + * @property callSiteMap keeps stack traces captured when an RPC was invoked, useful for debugging when an observable leaks. */ data class ObservableContext( val callSiteMap: CallSiteMap?, diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/RpcClientObservableDeSerializer.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/RpcClientObservableDeSerializer.kt index 52e9dc7cab..17ba71e200 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/RpcClientObservableDeSerializer.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/RpcClientObservableDeSerializer.kt @@ -2,8 +2,10 @@ package net.corda.client.rpc.internal.serialization.amqp import net.corda.client.rpc.internal.ObservableContext +import net.corda.client.rpc.internal.RPCClientProxyHandler import net.corda.core.context.Trace import net.corda.core.serialization.SerializationContext +import net.corda.core.utilities.loggerFor import net.corda.nodeapi.RPCApi import net.corda.serialization.internal.amqp.* import org.apache.qpid.proton.codec.Data @@ -17,11 +19,12 @@ import java.util.concurrent.atomic.AtomicInteger import javax.transaction.NotSupportedException /** - * De-serializer for Rx[Observable] instances for the RPC Client library. Can only be used to deserialize such objects, - * just as the corresponding RPC server side code ([RpcServerObservableSerializer]) can only serialize them. Observables are only notionally serialized, - * what is actually sent is a reference to the observable that can then be subscribed to. + * De-serializer for Rx [Observable] instances for the RPC Client library. Can only be used to deserialize such objects, + * just as the corresponding RPC server side class [RpcServerObservableSerializer] can only serialize them. Observables + * are only notionally serialized, what is actually sent is a reference to the observable that can then be subscribed to. */ object RpcClientObservableDeSerializer : CustomSerializer.Implements<Observable<*>>(Observable::class.java) { + private val log = loggerFor<RpcClientObservableDeSerializer>() private object RpcObservableContextKey fun createContext( @@ -96,22 +99,23 @@ object RpcClientObservableDeSerializer : CustomSerializer.Implements<Observable< } val rpcCallSite = getRpcCallSite(context, observableContext) - observableContext.observableMap.put(observableId, observable) observableContext.callSiteMap?.put(observableId, rpcCallSite) + log.trace("Deserialising observable $observableId", rpcCallSite) // We pin all Observables into a hard reference store (rooted in the RPC proxy) on subscription so that users // don't need to store a reference to the Observables themselves. return pinInSubscriptions(observable, observableContext.hardReferenceStore).doOnUnsubscribe { // This causes Future completions to give warnings because the corresponding OnComplete sent from the server // will arrive after the client unsubscribes from the observable and consequently invalidates the mapping. - // The unsubscribe is due to [ObservableToFuture]'s use of first(). + // The unsubscribe is due to ObservableToFuture's use of first(). observableContext.observableMap.invalidate(observableId) }.dematerialize<Any>() } - private fun getRpcCallSite(context: SerializationContext, observableContext: ObservableContext): Throwable? { + private fun getRpcCallSite(context: SerializationContext, observableContext: ObservableContext): RPCClientProxyHandler.CallSite? { val rpcRequestOrObservableId = context.properties[RPCApi.RpcRequestOrObservableIdKey] as Trace.InvocationId + // Will only return non-null if the trackRpcCallSites option in the RPC configuration has been specified. return observableContext.callSiteMap?.get(rpcRequestOrObservableId) } diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt index d2a0a2c977..caa363908c 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt @@ -48,7 +48,7 @@ class ClientRPCInfrastructureTests : AbstractRPCTest() { fun makeComplicatedListenableFuture(): CordaFuture<Pair<String, CordaFuture<String>>> - @RPCSinceVersion(2) + @RPCSinceVersion(2000) fun addedLater() fun captureUser(): String @@ -58,7 +58,7 @@ class ClientRPCInfrastructureTests : AbstractRPCTest() { private lateinit var complicatedListenableFuturee: CordaFuture<Pair<String, CordaFuture<String>>> inner class TestOpsImpl : TestOps { - override val protocolVersion = 1 + override val protocolVersion = 1000 // do not remove Unit override fun barf(): Unit = throw IllegalArgumentException("Barf!") override fun void() {} diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt index 0b15cc0a5e..b7492db120 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt @@ -33,7 +33,7 @@ class RPCConcurrencyTests : AbstractRPCTest() { @CordaSerializable data class ObservableRose<out A>(val value: A, val branches: Observable<out ObservableRose<A>>) - private interface TestOps : RPCOps { + interface TestOps : RPCOps { fun newLatch(numberOfDowns: Int): Long fun waitLatch(id: Long) fun downLatch(id: Long) @@ -43,7 +43,7 @@ class RPCConcurrencyTests : AbstractRPCTest() { class TestOpsImpl(private val pool: Executor) : TestOps { private val latches = ConcurrentHashMap<Long, CountDownLatch>() - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun newLatch(numberOfDowns: Int): Long { val id = random63BitValue() diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCFailureTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCFailureTests.kt index 7806bc9b40..f294d68587 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCFailureTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCFailureTests.kt @@ -26,7 +26,7 @@ class RPCFailureTests { } class OpsImpl : Ops { - override val protocolVersion = 1 + override val protocolVersion = 1000 override fun getUnserializable() = Unserializable() override fun getUnserializableAsync(): CordaFuture<Unserializable> { return openFuture<Unserializable>().apply { capture { getUnserializable() } } diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCHighThroughputObservableTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCHighThroughputObservableTests.kt index 72013ca955..4f3a09d507 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCHighThroughputObservableTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCHighThroughputObservableTests.kt @@ -24,7 +24,7 @@ class RPCHighThroughputObservableTests : AbstractRPCTest() { } internal class TestOpsImpl : TestOps { - override val protocolVersion = 1 + override val protocolVersion = 1000 override fun makeObservable(): Observable<Int> = Observable.interval(0, TimeUnit.MICROSECONDS).map { it.toInt() + 1 } } diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt index f6e9a8aa83..9f38487fb9 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt @@ -5,8 +5,8 @@ import net.corda.core.messaging.RPCOps import net.corda.core.utilities.minutes import net.corda.core.utilities.seconds import net.corda.node.services.messaging.RPCServerConfiguration -import net.corda.testing.node.internal.RPCDriverDSL import net.corda.testing.internal.performance.div +import net.corda.testing.node.internal.RPCDriverDSL import net.corda.testing.node.internal.performance.startPublishingFixedRateInjector import net.corda.testing.node.internal.performance.startReporter import net.corda.testing.node.internal.performance.startTightLoopInjector @@ -34,7 +34,7 @@ class RPCPerformanceTests : AbstractRPCTest() { } class TestOpsImpl : TestOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun simpleReply(input: ByteArray, sizeOfReply: Int): ByteArray { return ByteArray(sizeOfReply) } diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt index 21ebac1fbd..dee4c07257 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt @@ -25,7 +25,7 @@ class RPCPermissionsTests : AbstractRPCTest() { } class TestOpsImpl : TestOps { - override val protocolVersion = 1 + override val protocolVersion = 1000 override fun validatePermission(method: String, target: String?) { val authorized = if (target == null) { rpcContext().isPermitted(method) diff --git a/config/dev/log4j2.xml b/config/dev/log4j2.xml index 63681f41a8..b351e59536 100644 --- a/config/dev/log4j2.xml +++ b/config/dev/log4j2.xml @@ -46,7 +46,7 @@ <Policies> <TimeBasedTriggeringPolicy/> - <SizeBasedTriggeringPolicy size="10MB"/> + <SizeBasedTriggeringPolicy size="100MB"/> </Policies> <DefaultRolloverStrategy min="1" max="100"> diff --git a/constants.properties b/constants.properties index d9f06face2..8d23d66503 100644 --- a/constants.properties +++ b/constants.properties @@ -1,9 +1,12 @@ gradlePluginsVersion=4.0.29 kotlinVersion=1.2.51 +# When adjusting platformVersion upwards please also modify CordaRPCClientConfiguration.minimumServerProtocolVersion \ +# if there have been any RPC changes. Also please modify InternalMockNetwork.kt:MOCK_VERSION_INFO and NodeBasedTest.startNode platformVersion=4 guavaVersion=25.1-jre proguardVersion=6.0.3 -bouncycastleVersion=1.57 +bouncycastleVersion=1.60 +disruptorVersion=3.4.2 typesafeConfigVersion=1.3.1 jsr305Version=3.0.2 artifactoryPluginVersion=4.7.3 diff --git a/core-deterministic/build.gradle b/core-deterministic/build.gradle index 6627a80cd8..f7cdb65e9c 100644 --- a/core-deterministic/build.gradle +++ b/core-deterministic/build.gradle @@ -11,8 +11,8 @@ def javaHome = System.getProperty('java.home') def jarBaseName = "corda-${project.name}".toString() configurations { - runtimeLibraries - runtimeArtifacts.extendsFrom runtimeLibraries + deterministicLibraries + deterministicArtifacts.extendsFrom deterministicLibraries } dependencies { @@ -20,14 +20,14 @@ dependencies { // Configure these by hand. It should be a minimal subset of core's dependencies, // and without any obviously non-deterministic ones such as Hibernate. - runtimeLibraries "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - runtimeLibraries "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - runtimeLibraries "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final" - runtimeLibraries "org.bouncycastle:bcprov-jdk15on:$bouncycastle_version" - runtimeLibraries "org.bouncycastle:bcpkix-jdk15on:$bouncycastle_version" - runtimeLibraries "com.google.code.findbugs:jsr305:$jsr305_version" - runtimeLibraries "net.i2p.crypto:eddsa:$eddsa_version" - runtimeLibraries "org.slf4j:slf4j-api:$slf4j_version" + deterministicLibraries "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + deterministicLibraries "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + deterministicLibraries "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final" + deterministicLibraries "org.bouncycastle:bcprov-jdk15on:$bouncycastle_version" + deterministicLibraries "org.bouncycastle:bcpkix-jdk15on:$bouncycastle_version" + deterministicLibraries "com.google.code.findbugs:jsr305:$jsr305_version" + deterministicLibraries "net.i2p.crypto:eddsa:$eddsa_version" + deterministicLibraries "org.slf4j:slf4j-api:$slf4j_version" } jar { @@ -50,6 +50,7 @@ task patchCore(type: Zip, dependsOn: coreJarTask) { from(zipTree(originalJar)) { exclude 'net/corda/core/internal/*ToggleField*.class' exclude 'net/corda/core/serialization/*SerializationFactory*.class' + exclude 'net/corda/core/serialization/internal/CheckpointSerializationFactory*.class' } reproducibleFileOrder = true @@ -112,7 +113,7 @@ task determinise(type: ProGuardTask) { libraryjars file("$javaHome/lib/rt.jar") libraryjars file("$javaHome/lib/jce.jar") - configurations.runtimeLibraries.forEach { + configurations.deterministicLibraries.forEach { libraryjars it, filter: '!META-INF/versions/**' } @@ -152,7 +153,7 @@ task checkDeterminism(type: ProGuardTask, dependsOn: jdkTask) { libraryjars deterministic_rt_jar - configurations.runtimeLibraries.forEach { + configurations.deterministicLibraries.forEach { libraryjars it, filter: '!META-INF/versions/**' } @@ -173,12 +174,12 @@ assemble.dependsOn checkDeterminism def deterministicJar = metafix.outputs.files.singleFile artifacts { - runtimeArtifacts file: deterministicJar, name: jarBaseName, type: 'jar', extension: 'jar', builtBy: metafix + deterministicArtifacts file: deterministicJar, name: jarBaseName, type: 'jar', extension: 'jar', builtBy: metafix publish file: deterministicJar, name: jarBaseName, type: 'jar', extension: 'jar', builtBy: metafix } publish { - dependenciesFrom configurations.runtimeArtifacts + dependenciesFrom configurations.deterministicArtifacts publishSources = false publishJavadoc = false name jarBaseName diff --git a/core-deterministic/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationFactory.kt b/core-deterministic/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationFactory.kt new file mode 100644 index 0000000000..dbb6fb54c0 --- /dev/null +++ b/core-deterministic/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationFactory.kt @@ -0,0 +1,74 @@ +package net.corda.core.serialization.internal + +import net.corda.core.KeepForDJVM +import net.corda.core.serialization.SerializedBytes +import net.corda.core.utilities.ByteSequence +import java.io.NotSerializableException + +/** + * A deterministic version of [CheckpointSerializationFactory] that does not use thread-locals to manage serialization + * context. + */ +@KeepForDJVM +class CheckpointSerializationFactory( + private val scheme: CheckpointSerializationScheme +) { + + val defaultContext: CheckpointSerializationContext get() = _currentContext ?: effectiveSerializationEnv.checkpointContext + + private val creator: List<StackTraceElement> = Exception().stackTrace.asList() + + /** + * Deserialize the bytes in to an object, using the prefixed bytes to determine the format. + * + * @param byteSequence The bytes to deserialize, including a format header prefix. + * @param clazz The class or superclass or the object to be deserialized, or [Any] or [Object] if unknown. + * @param context A context that configures various parameters to deserialization. + */ + @Throws(NotSerializableException::class) + fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: CheckpointSerializationContext): T { + return withCurrentContext(context) { scheme.deserialize(byteSequence, clazz, context) } + } + + /** + * Serialize an object to bytes using the preferred serialization format version from the context. + * + * @param obj The object to be serialized. + * @param context A context that configures various parameters to serialization, including the serialization format version. + */ + fun <T : Any> serialize(obj: T, context: CheckpointSerializationContext): SerializedBytes<T> { + return withCurrentContext(context) { scheme.serialize(obj, context) } + } + + override fun toString(): String { + return "${this.javaClass.name} scheme=$scheme ${creator.joinToString("\n")}" + } + + override fun equals(other: Any?): Boolean { + return other is CheckpointSerializationFactory && other.scheme == this.scheme + } + + override fun hashCode(): Int = scheme.hashCode() + + private var _currentContext: CheckpointSerializationContext? = null + + /** + * Change the current context inside the block to that supplied. + */ + fun <T> withCurrentContext(context: CheckpointSerializationContext?, block: () -> T): T { + val priorContext = _currentContext + if (context != null) _currentContext = context + try { + return block() + } finally { + if (context != null) _currentContext = priorContext + } + } + + companion object { + /** + * A default factory for serialization/deserialization. + */ + val defaultFactory: CheckpointSerializationFactory get() = effectiveSerializationEnv.checkpointSerializationFactory + } +} \ No newline at end of file diff --git a/core-deterministic/testing/build.gradle b/core-deterministic/testing/build.gradle index a472f5cde5..bb007715eb 100644 --- a/core-deterministic/testing/build.gradle +++ b/core-deterministic/testing/build.gradle @@ -1,10 +1,10 @@ apply plugin: 'kotlin' dependencies { - testCompile project(path: ':core-deterministic', configuration: 'runtimeArtifacts') - testCompile project(path: ':serialization-deterministic', configuration: 'runtimeArtifacts') + testCompile project(path: ':core-deterministic', configuration: 'deterministicArtifacts') + testCompile project(path: ':serialization-deterministic', configuration: 'deterministicArtifacts') + testCompile project(path: ':core-deterministic:testing:verifier', configuration: 'deterministicArtifacts') testCompile project(path: ':core-deterministic:testing:data', configuration: 'testData') - testCompile project(':core-deterministic:testing:common') testCompile(project(':finance')) { transitive = false } diff --git a/core-deterministic/testing/common/build.gradle b/core-deterministic/testing/common/build.gradle deleted file mode 100644 index f7c48a6c7a..0000000000 --- a/core-deterministic/testing/common/build.gradle +++ /dev/null @@ -1,16 +0,0 @@ -apply from: '../../../deterministic.gradle' -apply plugin: 'idea' - -dependencies { - compileOnly project(path: ':core-deterministic', configuration: 'runtimeArtifacts') - compileOnly project(path: ':serialization-deterministic', configuration: 'runtimeArtifacts') - compileOnly "junit:junit:$junit_version" -} - -idea { - module { - if (project.hasProperty("deterministic_idea_sdk")) { - jdkName project.property("deterministic_idea_sdk") as String - } - } -} diff --git a/core-deterministic/testing/data/build.gradle b/core-deterministic/testing/data/build.gradle index d203ae5572..59992d92eb 100644 --- a/core-deterministic/testing/data/build.gradle +++ b/core-deterministic/testing/data/build.gradle @@ -8,7 +8,7 @@ dependencies { testCompile project(':core') testCompile project(':finance') testCompile project(':node-driver') - testCompile project(':core-deterministic:testing:common') + testCompile project(path: ':core-deterministic:testing:verifier', configuration: 'runtimeArtifacts') testCompile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" testCompile "org.jetbrains.kotlin:kotlin-reflect" diff --git a/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/GenerateData.kt b/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/GenerateData.kt index 0304661183..1ada19e231 100644 --- a/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/GenerateData.kt +++ b/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/GenerateData.kt @@ -1,8 +1,8 @@ package net.corda.deterministic.data import net.corda.core.serialization.deserialize -import net.corda.deterministic.common.LocalSerializationRule -import net.corda.deterministic.common.TransactionVerificationRequest +import net.corda.deterministic.verifier.LocalSerializationRule +import net.corda.deterministic.verifier.TransactionVerificationRequest import org.junit.Before import org.junit.Rule import org.junit.Test diff --git a/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/TransactionGenerator.kt b/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/TransactionGenerator.kt index a6b704077e..d777bf9df1 100644 --- a/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/TransactionGenerator.kt +++ b/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/TransactionGenerator.kt @@ -7,9 +7,9 @@ import net.corda.core.identity.AnonymousParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.serialization.serialize -import net.corda.deterministic.common.MockContractAttachment -import net.corda.deterministic.common.SampleCommandData -import net.corda.deterministic.common.TransactionVerificationRequest +import net.corda.deterministic.verifier.MockContractAttachment +import net.corda.deterministic.verifier.SampleCommandData +import net.corda.deterministic.verifier.TransactionVerificationRequest import net.corda.finance.POUNDS import net.corda.finance.`issued by` import net.corda.finance.contracts.asset.Cash.* diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt index 5935c13b3c..4825d4787f 100644 --- a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt +++ b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt @@ -3,7 +3,7 @@ package net.corda.deterministic.crypto import net.corda.core.crypto.* import net.corda.deterministic.KeyStoreProvider import net.corda.deterministic.CheatingSecurityProvider -import net.corda.deterministic.common.LocalSerializationRule +import net.corda.deterministic.verifier.LocalSerializationRule import org.junit.* import org.junit.rules.RuleChain import java.security.* diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/EnclaveletTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/EnclaveletTest.kt deleted file mode 100644 index 88125a4072..0000000000 --- a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/EnclaveletTest.kt +++ /dev/null @@ -1,52 +0,0 @@ -@file:JvmName("Enclavelet") -package net.corda.deterministic.txverify - -import net.corda.core.serialization.deserialize -import net.corda.core.transactions.LedgerTransaction -import net.corda.deterministic.bytesOfResource -import net.corda.deterministic.common.LocalSerializationRule -import net.corda.deterministic.common.TransactionVerificationRequest -import net.corda.finance.contracts.asset.Cash.Commands.* -import org.assertj.core.api.Assertions.assertThat -import org.junit.ClassRule -import org.junit.Test -import kotlin.test.assertFailsWith - -class EnclaveletTest { - companion object { - @ClassRule - @JvmField - val serialization = LocalSerializationRule(EnclaveletTest::class) - } - - @Test - fun success() { - verifyInEnclave(bytesOfResource("txverify/tx-success.bin")) - } - - @Test - fun failure() { - val e = assertFailsWith<Exception> { verifyInEnclave(bytesOfResource("txverify/tx-failure.bin")) } - assertThat(e).hasMessageContaining("Required ${Move::class.java.canonicalName} command") - } -} - -/** - * Returns either null to indicate success when the transactions are validated, or a string with the - * contents of the error. Invoked via JNI in response to an enclave RPC. The argument is a serialised - * [TransactionVerificationRequest]. - * - * Note that it is assumed the signatures were already checked outside the sandbox: the purpose of this code - * is simply to check the sensitive, app specific parts of a transaction. - * - * TODO: Transaction data is meant to be encrypted under an enclave-private key. - */ -@Throws(Exception::class) -private fun verifyInEnclave(reqBytes: ByteArray) { - deserialize(reqBytes).verify() -} - -private fun deserialize(reqBytes: ByteArray): LedgerTransaction { - return reqBytes.deserialize<TransactionVerificationRequest>() - .toLedgerTransaction() -} diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/VerifyTransactionTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/VerifyTransactionTest.kt new file mode 100644 index 0000000000..6526ca3c51 --- /dev/null +++ b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/VerifyTransactionTest.kt @@ -0,0 +1,29 @@ +package net.corda.deterministic.txverify + +import net.corda.deterministic.bytesOfResource +import net.corda.deterministic.verifier.LocalSerializationRule +import net.corda.deterministic.verifier.verifyTransaction +import net.corda.finance.contracts.asset.Cash.Commands.* +import org.assertj.core.api.Assertions.assertThat +import org.junit.ClassRule +import org.junit.Test +import kotlin.test.assertFailsWith + +class VerifyTransactionTest { + companion object { + @ClassRule + @JvmField + val serialization = LocalSerializationRule(VerifyTransactionTest::class) + } + + @Test + fun success() { + verifyTransaction(bytesOfResource("txverify/tx-success.bin")) + } + + @Test + fun failure() { + val e = assertFailsWith<Exception> { verifyTransaction(bytesOfResource("txverify/tx-failure.bin")) } + assertThat(e).hasMessageContaining("Required ${Move::class.java.canonicalName} command") + } +} diff --git a/core-deterministic/testing/verifier/build.gradle b/core-deterministic/testing/verifier/build.gradle new file mode 100644 index 0000000000..259a9c3110 --- /dev/null +++ b/core-deterministic/testing/verifier/build.gradle @@ -0,0 +1,48 @@ +apply plugin: 'java-library' +apply from: '../../../deterministic.gradle' +apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' +apply plugin: 'idea' + +description 'Test utilities for deterministic contract verification' + +configurations { + deterministicArtifacts + runtimeArtifacts.extendsFrom api +} + +dependencies { + deterministicArtifacts project(path: ':serialization-deterministic', configuration: 'deterministicArtifacts') + deterministicArtifacts project(path: ':core-deterministic', configuration: 'deterministicArtifacts') + + runtimeArtifacts project(':serialization') + runtimeArtifacts project(':core') + + // Compile against the deterministic artifacts to ensure that we use only the deterministic API subset. + compileOnly configurations.deterministicArtifacts + api "junit:junit:$junit_version" +} + +jar { + baseName 'corda-deterministic-verifier' +} + +artifacts { + deterministicArtifacts jar + runtimeArtifacts jar + publish jar +} + +publish { + // Our published POM will contain dependencies on the non-deterministic Corda artifacts. + dependenciesFrom configurations.runtimeArtifacts + name jar.baseName +} + +idea { + module { + if (project.hasProperty("deterministic_idea_sdk")) { + jdkName project.property("deterministic_idea_sdk") as String + } + } +} diff --git a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/LocalSerializationRule.kt b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/LocalSerializationRule.kt similarity index 98% rename from core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/LocalSerializationRule.kt rename to core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/LocalSerializationRule.kt index 05c8c3bc5c..15848a4be4 100644 --- a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/LocalSerializationRule.kt +++ b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/LocalSerializationRule.kt @@ -1,4 +1,4 @@ -package net.corda.deterministic.common +package net.corda.deterministic.verifier import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.SerializationContext @@ -83,4 +83,4 @@ class LocalSerializationRule(private val label: String) : TestRule { return canDeserializeVersion(magic) && target == P2P } } -} \ No newline at end of file +} diff --git a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/MockContractAttachment.kt b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/MockContractAttachment.kt similarity index 93% rename from core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/MockContractAttachment.kt rename to core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/MockContractAttachment.kt index a4b3b8a21e..f7e90ce2cc 100644 --- a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/MockContractAttachment.kt +++ b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/MockContractAttachment.kt @@ -1,4 +1,4 @@ -package net.corda.deterministic.common +package net.corda.deterministic.verifier import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractClassName diff --git a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/SampleData.kt b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/SampleData.kt similarity index 76% rename from core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/SampleData.kt rename to core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/SampleData.kt index 025fa148fa..9c4cfdcb59 100644 --- a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/SampleData.kt +++ b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/SampleData.kt @@ -1,5 +1,5 @@ @file:JvmName("SampleData") -package net.corda.deterministic.common +package net.corda.deterministic.verifier import net.corda.core.contracts.TypeOnlyCommandData diff --git a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/TransactionVerificationRequest.kt b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/TransactionVerificationRequest.kt similarity index 97% rename from core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/TransactionVerificationRequest.kt rename to core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/TransactionVerificationRequest.kt index f2c825ba61..96a886a618 100644 --- a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/TransactionVerificationRequest.kt +++ b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/TransactionVerificationRequest.kt @@ -1,4 +1,4 @@ -package net.corda.deterministic.common +package net.corda.deterministic.verifier import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractAttachment diff --git a/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/Verifier.kt b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/Verifier.kt new file mode 100644 index 0000000000..e7a710d707 --- /dev/null +++ b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/Verifier.kt @@ -0,0 +1,21 @@ +@file:JvmName("Verifier") +package net.corda.deterministic.verifier + +import net.corda.core.serialization.deserialize +import net.corda.core.transactions.LedgerTransaction + +/** + * We assume the signatures were already checked outside the sandbox: the purpose of this code + * is simply to check the sensitive, app-specific parts of a transaction. + * + * TODO: Transaction data is meant to be encrypted under an enclave-private key. + */ +@Throws(Exception::class) +fun verifyTransaction(reqBytes: ByteArray) { + deserialize(reqBytes).verify() +} + +private fun deserialize(reqBytes: ByteArray): LedgerTransaction { + return reqBytes.deserialize<TransactionVerificationRequest>() + .toLedgerTransaction() +} diff --git a/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt b/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt index 8e23c5c01b..ddf369b668 100644 --- a/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt +++ b/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt @@ -48,20 +48,4 @@ interface Cordapp { val jarPath: URL val cordappClasses: List<String> val jarHash: SecureHash.SHA256 - - /** - * CorDapp's information, including vendor and version. - * - * @property shortName Cordapp's shortName - * @property vendor Cordapp's vendor - * @property version Cordapp's version - */ - @DoNotImplement - interface Info { - val shortName: String - val vendor: String - val version: String - - fun hasUnknownFields(): Boolean - } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt index 0f6b87e6d8..1cc29b5fdd 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -102,7 +102,11 @@ fun PublicKey.isValid(content: ByteArray, signature: DigitalSignature): Boolean /** Render a public key to its hash (in Base58) of its serialised form using the DL prefix. */ fun PublicKey.toStringShort(): String = "DL" + this.toSHA256Bytes().toBase58() -/** Return a [Set] of the contained keys if this is a [CompositeKey]; otherwise, return a [Set] with a single element (this [PublicKey]). */ +/** + * Return a [Set] of the contained leaf keys if this is a [CompositeKey]. + * Otherwise, return a [Set] with a single element (this [PublicKey]). + * <i>Note that leaf keys cannot be of type [CompositeKey].</i> + */ val PublicKey.keys: Set<PublicKey> get() = (this as? CompositeKey)?.leafKeys ?: setOf(this) /** Return true if [otherKey] fulfils the requirements of this [PublicKey]. */ @@ -110,7 +114,12 @@ fun PublicKey.isFulfilledBy(otherKey: PublicKey): Boolean = isFulfilledBy(setOf( /** Return true if [otherKeys] fulfil the requirements of this [PublicKey]. */ fun PublicKey.isFulfilledBy(otherKeys: Iterable<PublicKey>): Boolean = (this as? CompositeKey)?.isFulfilledBy(otherKeys) ?: (this in otherKeys) -/** Checks whether any of the given [keys] matches a leaf on the [CompositeKey] tree or a single [PublicKey]. */ +/** + * Checks whether any of the given [keys] matches a leaf on the [CompositeKey] tree or a single [PublicKey]. + * + * <i>Note that this function checks against leaves, which cannot be of type [CompositeKey]. Due to that, if any of the + * [otherKeys] is a [CompositeKey], this function will not find a match.</i> + */ fun PublicKey.containsAny(otherKeys: Iterable<PublicKey>): Boolean { return if (this is CompositeKey) keys.intersect(otherKeys).isNotEmpty() else this in otherKeys diff --git a/core/src/main/kotlin/net/corda/core/crypto/internal/ProviderMap.kt b/core/src/main/kotlin/net/corda/core/crypto/internal/ProviderMap.kt index 010d894453..03c4a01d72 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/internal/ProviderMap.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/internal/ProviderMap.kt @@ -11,9 +11,11 @@ import net.i2p.crypto.eddsa.EdDSASecurityProvider import org.bouncycastle.asn1.ASN1ObjectIdentifier import org.bouncycastle.asn1.pkcs.PrivateKeyInfo import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +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.SecureRandom import java.security.Security internal val cordaSecurityProvider = CordaSecurityProvider().also { @@ -29,6 +31,8 @@ internal val cordaBouncyCastleProvider = BouncyCastleProvider().apply { override fun generatePublic(keyInfo: SubjectPublicKeyInfo) = decodePublicKey(EDDSA_ED25519_SHA512, keyInfo.encoded) override fun generatePrivate(keyInfo: PrivateKeyInfo) = decodePrivateKey(EDDSA_ED25519_SHA512, keyInfo.encoded) }) + // Required due to [X509CRL].verify() reported issues in network-services after BC 1.60 update. + put("AlgorithmParameters.SHA256WITHECDSA", AlgorithmParametersSpi::class.java.name) }.also { // This registration is needed for reading back EdDSA key from java keystore. // TODO: Find a way to make JKS work with bouncy castle provider or implement our own provide so we don't have to register bouncy castle provider. @@ -46,4 +50,4 @@ internal val bouncyCastlePQCProvider = BouncyCastlePQCProvider().apply { internal val providerMap = listOf(cordaBouncyCastleProvider, cordaSecurityProvider, bouncyCastlePQCProvider).map { it.name to it }.toMap() @DeleteForDJVM -internal fun platformSecureRandomFactory() = platformSecureRandom() // To minimise diff of CryptoUtils against open-source. \ No newline at end of file +internal fun platformSecureRandomFactory(): SecureRandom = platformSecureRandom() // To minimise diff of CryptoUtils against open-source. diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowException.kt b/core/src/main/kotlin/net/corda/core/flows/FlowException.kt index cd00434c37..80a648227a 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowException.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowException.kt @@ -21,13 +21,13 @@ import net.corda.core.CordaRuntimeException * the exception is handled. This ID is propagated to counterparty flows, even when the [FlowException] is * downgraded to an [UnexpectedFlowEndException]. This is so the error conditions may be correlated later on. */ -open class FlowException(message: String?, cause: Throwable?) : +open class FlowException(message: String?, cause: Throwable?, var originalErrorId: Long? = null) : CordaException(message, cause), IdentifiableException { + constructor(message: String?, cause: Throwable?) : this(message, cause, null) constructor(message: String?) : this(message, null) constructor(cause: Throwable?) : this(cause?.toString(), cause) constructor() : this(null, null) - var originalErrorId: Long? = null override fun getErrorId(): Long? = originalErrorId } // DOCEND 1 diff --git a/core/src/main/kotlin/net/corda/core/internal/CertRole.kt b/core/src/main/kotlin/net/corda/core/internal/CertRole.kt index 29d24b80e0..b53f8977e8 100644 --- a/core/src/main/kotlin/net/corda/core/internal/CertRole.kt +++ b/core/src/main/kotlin/net/corda/core/internal/CertRole.kt @@ -24,22 +24,22 @@ import java.security.cert.X509Certificate // also note that IDs are numbered from 1 upwards, matching numbering of other enum types in ASN.1 specifications. // TODO: Link to the specification once it has a permanent URL enum class CertRole(val validParents: NonEmptySet<CertRole?>, val isIdentity: Boolean, val isWellKnown: Boolean) : ASN1Encodable { - /** Intermediate CA (Doorman service). */ - INTERMEDIATE_CA(NonEmptySet.of(null), false, false), + /** Signing certificate for the Doorman CA. */ + DOORMAN_CA(NonEmptySet.of(null), false, false), /** Signing certificate for the network map. */ NETWORK_MAP(NonEmptySet.of(null), false, false), /** Well known (publicly visible) identity of a service (such as notary). */ - SERVICE_IDENTITY(NonEmptySet.of(INTERMEDIATE_CA), true, true), + SERVICE_IDENTITY(NonEmptySet.of(DOORMAN_CA), true, true), /** Node level CA from which the TLS and well known identity certificates are issued. */ - NODE_CA(NonEmptySet.of(INTERMEDIATE_CA), false, false), + NODE_CA(NonEmptySet.of(DOORMAN_CA), false, false), /** Transport layer security certificate for a node. */ TLS(NonEmptySet.of(NODE_CA), false, false), /** Well known (publicly visible) identity of a legal entity. */ - // TODO: at the moment, Legal Identity certs are issued by Node CA only. However, [INTERMEDIATE_CA] is also added + // TODO: at the moment, Legal Identity certs are issued by Node CA only. However, [DOORMAN_CA] is also added // as a valid parent of [LEGAL_IDENTITY] for backwards compatibility purposes (eg. if we decide TLS has its - // own Root CA and Intermediate CA directly issues Legal Identities; thus, there won't be a requirement for - // Node CA). Consider removing [INTERMEDIATE_CA] from [validParents] when the model is finalised. - LEGAL_IDENTITY(NonEmptySet.of(INTERMEDIATE_CA, NODE_CA), true, true), + // own Root CA and Doorman CA directly issues Legal Identities; thus, there won't be a requirement for + // Node CA). Consider removing [DOORMAN_CA] from [validParents] when the model is finalised. + LEGAL_IDENTITY(NonEmptySet.of(DOORMAN_CA, NODE_CA), true, true), /** Confidential (limited visibility) identity of a legal entity. */ CONFIDENTIAL_LEGAL_IDENTITY(NonEmptySet.of(LEGAL_IDENTITY), true, false); diff --git a/core/src/main/kotlin/net/corda/core/internal/DigitalSignatureWithCert.kt b/core/src/main/kotlin/net/corda/core/internal/DigitalSignatureWithCert.kt index 04ca25c6cb..c06cfe2e55 100644 --- a/core/src/main/kotlin/net/corda/core/internal/DigitalSignatureWithCert.kt +++ b/core/src/main/kotlin/net/corda/core/internal/DigitalSignatureWithCert.kt @@ -4,17 +4,41 @@ import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SignedData import net.corda.core.crypto.verify import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.DeprecatedConstructorForDeserialization import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize import net.corda.core.utilities.OpaqueBytes -import java.security.cert.X509Certificate +import java.security.cert.* // TODO: Rename this to DigitalSignature.WithCert once we're happy for it to be public API. The methods will need documentation // and the correct exceptions will be need to be annotated -/** A digital signature with attached certificate of the public key. */ -class DigitalSignatureWithCert(val by: X509Certificate, bytes: ByteArray) : DigitalSignature(bytes) { +/** A digital signature with attached certificate of the public key and (optionally) the remaining chain of the certificates from the certificate path. */ +class DigitalSignatureWithCert(val by: X509Certificate, val parentCertsChain: List<X509Certificate>, bytes: ByteArray) : DigitalSignature(bytes) { + @DeprecatedConstructorForDeserialization(1) + constructor(by: X509Certificate, bytes: ByteArray) : this(by, emptyList(), bytes) + + val fullCertChain: List<X509Certificate> get() = listOf(by) + parentCertsChain + val fullCertPath: CertPath get() = CertificateFactory.getInstance("X.509").generateCertPath(fullCertChain) + fun verify(content: ByteArray): Boolean = by.publicKey.verify(content, this) fun verify(content: OpaqueBytes): Boolean = verify(content.bytes) + + init { + if (parentCertsChain.isNotEmpty()) { + val parameters = PKIXParameters(setOf(TrustAnchor(parentCertsChain.last(), null))).apply { isRevocationEnabled = false } + try { + CertPathValidator.getInstance("PKIX").validate(fullCertPath, parameters) + } catch (e: CertPathValidatorException) { + throw IllegalArgumentException( + """Cert path failed to validate. +Reason: ${e.reason} +Offending cert index: ${e.index} +Cert path: $fullCertPath +""", e) + } + } + + } } /** Similar to [SignedData] but instead of just attaching the public key, the certificate for the key is attached instead. */ diff --git a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt index 4bb32757f5..29ae9b8c2c 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt @@ -17,6 +17,7 @@ import net.corda.core.serialization.SerializeAsTokenContext import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NonEmptySet import net.corda.core.utilities.UntrustworthyData +import net.corda.core.utilities.debug import net.corda.core.utilities.unwrap import java.nio.file.FileAlreadyExistsException import java.util.* @@ -75,7 +76,7 @@ sealed class FetchDataFlow<T : NamedByHash, in W : Any>( return if (toFetch.isEmpty()) { Result(fromDisk, emptyList()) } else { - logger.info("Requesting ${toFetch.size} dependency(s) for verification from ${otherSideSession.counterparty.name}") + logger.debug { "Requesting ${toFetch.size} dependency(s) for verification from ${otherSideSession.counterparty.name}" } // TODO: Support "large message" response streaming so response sizes are not limited by RAM. // We can then switch to requesting items in large batches to minimise the latency penalty. @@ -93,7 +94,7 @@ sealed class FetchDataFlow<T : NamedByHash, in W : Any>( } // Check for a buggy/malicious peer answering with something that we didn't ask for. val downloaded = validateFetchResponse(UntrustworthyData(maybeItems), toFetch) - logger.info("Fetched ${downloaded.size} elements from ${otherSideSession.counterparty.name}") + logger.debug { "Fetched ${downloaded.size} elements from ${otherSideSession.counterparty.name}" } maybeWriteToDisk(downloaded) Result(fromDisk, downloaded) } diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index 9626cf88a0..078cfcaa11 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -4,21 +4,11 @@ package net.corda.core.internal import net.corda.core.DeleteForDJVM import net.corda.core.KeepForDJVM -import net.corda.core.cordapp.Cordapp -import net.corda.core.cordapp.CordappConfig -import net.corda.core.cordapp.CordappContext import net.corda.core.crypto.* -import net.corda.core.flows.FlowLogic -import net.corda.core.node.ServicesForResolution import net.corda.core.serialization.* -import net.corda.core.transactions.LedgerTransaction -import net.corda.core.transactions.SignedTransaction -import net.corda.core.transactions.TransactionBuilder -import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.UntrustworthyData import org.slf4j.Logger -import org.slf4j.MDC import rx.Observable import rx.Observer import rx.subjects.PublishSubject diff --git a/core/src/main/kotlin/net/corda/core/internal/NamedCache.kt b/core/src/main/kotlin/net/corda/core/internal/NamedCache.kt index 7b54b6ceb4..2a96de4835 100644 --- a/core/src/main/kotlin/net/corda/core/internal/NamedCache.kt +++ b/core/src/main/kotlin/net/corda/core/internal/NamedCache.kt @@ -29,12 +29,6 @@ fun <K, V> Caffeine<in K, in V>.buildNamed(name: String): Cache<K, V> { return this.build<K, V>() } -fun <K, V> Caffeine<in K, in V>.buildNamed(name: String, loadFunc: (K) -> V): LoadingCache<K, V> { - checkCacheName(name) - return this.build<K, V>(loadFunc) -} - - fun <K, V> Caffeine<in K, in V>.buildNamed(name: String, loader: CacheLoader<K, V>): LoadingCache<K, V> { checkCacheName(name) return this.build<K, V>(loader) diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt index a8021e8663..38891ef254 100644 --- a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt @@ -24,25 +24,29 @@ data class CordappImpl( override val customSchemas: Set<MappedSchema>, override val allFlows: List<Class<out FlowLogic<*>>>, override val jarPath: URL, + val info: Info, override val jarHash: SecureHash.SHA256) : Cordapp { - override val name: String = jarPath.toPath().fileName.toString().removeSuffix(".jar") + override val name: String = jarName(jarPath) + + companion object { + fun jarName(url: URL): String = url.toPath().fileName.toString().removeSuffix(".jar") + } /** * An exhaustive list of all classes relevant to the node within this CorDapp * * TODO: Also add [SchedulableFlow] as a Cordapp class */ - override val cordappClasses = ((rpcFlows + initiatedFlows + services + serializationWhitelists.map { javaClass }).map { it.name } + contractClassNames) + override val cordappClasses: List<String> = (rpcFlows + initiatedFlows + services + serializationWhitelists.map { javaClass }).map { it.name } + contractClassNames - data class Info(override val shortName: String, override val vendor: String, override val version: String): Cordapp.Info { + // TODO Why a seperate Info class and not just have the fields directly in CordappImpl? + data class Info(val shortName: String, val vendor: String, val version: String, val minimumPlatformVersion: Int, val targetPlatformVersion: Int) { companion object { private const val UNKNOWN_VALUE = "Unknown" - val UNKNOWN = Info(UNKNOWN_VALUE, UNKNOWN_VALUE, UNKNOWN_VALUE) + val UNKNOWN = Info(UNKNOWN_VALUE, UNKNOWN_VALUE, UNKNOWN_VALUE, 1, 1) } - override fun hasUnknownFields(): Boolean { - return setOf(shortName, vendor, version).any { it == UNKNOWN_VALUE } - } + fun hasUnknownFields(): Boolean = arrayOf(shortName, vendor, version).any { it == UNKNOWN_VALUE } } } diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappInfoResolver.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappInfoResolver.kt new file mode 100644 index 0000000000..63f7cb2188 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappInfoResolver.kt @@ -0,0 +1,68 @@ +package net.corda.core.internal.cordapp + +import net.corda.core.internal.VisibleForTesting +import net.corda.core.utilities.loggerFor +import java.util.concurrent.ConcurrentHashMap + +/** + * Provides a way to acquire information about the calling CorDapp. + */ +object CordappInfoResolver { + private val logger = loggerFor<CordappInfoResolver>() + private val cordappClasses: ConcurrentHashMap<String, Set<CordappImpl.Info>> = ConcurrentHashMap() + + // TODO use the StackWalker API once we migrate to Java 9+ + private var cordappInfoResolver: () -> CordappImpl.Info? = { + Exception().stackTrace + .mapNotNull { cordappClasses[it.className] } + // If there is more than one cordapp registered for a class name we can't determine the "correct" one and return null. + .firstOrNull { it.size < 2 }?.single() + } + + /* + * Associates class names with CorDapps or logs a warning when a CorDapp is already registered for a given class. + * This could happen when trying to run different versions of the same CorDapp on the same node. + */ + @Synchronized + fun register(classes: List<String>, cordapp: CordappImpl.Info) { + classes.forEach { + if (cordappClasses.containsKey(it)) { + logger.warn("More than one CorDapp registered for $it.") + cordappClasses[it] = cordappClasses[it]!! + cordapp + } else { + cordappClasses[it] = setOf(cordapp) + } + } + } + + /* + * This should only be used when making a change that would break compatibility with existing CorDapps. The change + * can then be version-gated, meaning the old behaviour is used if the calling CorDapp's target version is lower + * than the platform version that introduces the new behaviour. + * In situations where a `[CordappProvider]` is available the CorDapp context should be obtained from there. + * + * @return Information about the CorDapp from which the invoker is called, null if called outside a CorDapp or the + * calling CorDapp cannot be reliably determined.. + */ + fun getCorDappInfo(): CordappImpl.Info? = cordappInfoResolver() + + /** + * Temporarily switch out the internal resolver for another one. For use in testing. + */ + @Synchronized + @VisibleForTesting + fun withCordappInfoResolution(tempResolver: () -> CordappImpl.Info?, block: () -> Unit) { + val resolver = cordappInfoResolver + cordappInfoResolver = tempResolver + try { + block() + } finally { + cordappInfoResolver = resolver + } + } + + @VisibleForTesting + internal fun clear() { + cordappClasses.clear() + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/notary/AsyncUniquenessProvider.kt b/core/src/main/kotlin/net/corda/core/internal/notary/AsyncUniquenessProvider.kt new file mode 100644 index 0000000000..1d9328de88 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/notary/AsyncUniquenessProvider.kt @@ -0,0 +1,35 @@ +package net.corda.core.internal.notary + +import net.corda.core.concurrent.CordaFuture +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TimeWindow +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.NotarisationRequestSignature +import net.corda.core.flows.NotaryError +import net.corda.core.identity.Party + +/** + * A service that records input states of the given transaction and provides conflict information + * if any of the inputs have already been used in another transaction. + */ +interface AsyncUniquenessProvider : UniquenessProvider { + /** Commits all input states of the given transaction. */ + fun commitAsync(states: List<StateRef>, txId: SecureHash, callerIdentity: Party, requestSignature: NotarisationRequestSignature, timeWindow: TimeWindow?, references: List<StateRef>): CordaFuture<Result> + + /** Commits all input states of the given transaction synchronously. Use [commitAsync] for better performance. */ + override fun commit(states: List<StateRef>, txId: SecureHash, callerIdentity: Party, requestSignature: NotarisationRequestSignature, timeWindow: TimeWindow?, references: List<StateRef>) { + val result = commitAsync(states, txId, callerIdentity, requestSignature, timeWindow,references).get() + if (result is Result.Failure) { + throw NotaryInternalException(result.error) + } + } + + /** The outcome of committing a transaction. */ + sealed class Result { + /** Indicates that all input states have been committed successfully. */ + object Success : Result() + /** Indicates that the transaction has not been committed. */ + data class Failure(val error: NotaryError) : Result() + } +} + diff --git a/core/src/main/kotlin/net/corda/core/internal/notary/TrustedAuthorityNotaryService.kt b/core/src/main/kotlin/net/corda/core/internal/notary/TrustedAuthorityNotaryService.kt index 1d76244380..148b506e82 100644 --- a/core/src/main/kotlin/net/corda/core/internal/notary/TrustedAuthorityNotaryService.kt +++ b/core/src/main/kotlin/net/corda/core/internal/notary/TrustedAuthorityNotaryService.kt @@ -67,4 +67,4 @@ abstract class TrustedAuthorityNotaryService : NotaryService() { } // TODO: Sign multiple transactions at once by building their Merkle tree and then signing over its root. -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index f59aa49a40..1b924610d7 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -22,7 +22,6 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.Try import rx.Observable -import rx.subjects.PublishSubject import java.io.IOException import java.io.InputStream import java.security.PublicKey @@ -42,7 +41,8 @@ data class StateMachineInfo @JvmOverloads constructor( * An object representing information about the initiator of the flow. Note that this field is * superseded by the [invocationContext] property, which has more detail. */ - @Deprecated("There is more info available using 'context'") val initiator: FlowInitiator, + @Deprecated("There is more info available using 'invocationContext'") + val initiator: FlowInitiator, /** A [DataFeed] of the current progress step as a human readable string, and updates to that string. */ val progressTrackerStepAndUpdates: DataFeed<String, String>?, /** An [InvocationContext] describing why and by whom the flow was started. */ @@ -76,7 +76,8 @@ sealed class StateMachineUpdate { // DOCSTART 1 /** * Data class containing information about the scheduled network parameters update. The info is emitted every time node - * receives network map with [ParametersUpdate] which wasn't seen before. For more information see: [CordaRPCOps.networkParametersFeed] and [CordaRPCOps.acceptNewNetworkParameters]. + * receives network map with [ParametersUpdate] which wasn't seen before. For more information see: [CordaRPCOps.networkParametersFeed] + * and [CordaRPCOps.acceptNewNetworkParameters]. * @property hash new [NetworkParameters] hash * @property parameters new [NetworkParameters] data structure * @property description description of the update @@ -96,12 +97,6 @@ data class StateMachineTransactionMapping(val stateMachineRunId: StateMachineRun /** RPC operations that the node exposes to clients. */ interface CordaRPCOps : RPCOps { - /** - * Returns the RPC protocol version, which is the same the node's Platform Version. Exists since version 1 so guaranteed - * to be present. - */ - override val protocolVersion: Int get() = nodeInfo().platformVersion - /** Returns a list of currently in-progress state machine infos. */ fun stateMachinesSnapshot(): List<StateMachineInfo> @@ -233,6 +228,9 @@ interface CordaRPCOps : RPCOps { @RPCReturnsObservables fun networkMapFeed(): DataFeed<List<NodeInfo>, NetworkMapCache.MapChange> + /** Returns the network parameters the node is operating under. */ + val networkParameters: NetworkParameters + /** * Returns [DataFeed] object containing information on currently scheduled parameters update (null if none are currently scheduled) * and observable with future update events. Any update that occurs before the deadline automatically cancels the current one. @@ -406,38 +404,20 @@ interface CordaRPCOps : RPCOps { * This does not wait for flows to be completed. */ fun shutdown() -} -/** - * Returns a [DataFeed] that keeps track on the count of pending flows. - */ -fun CordaRPCOps.pendingFlowsCount(): DataFeed<Int, Pair<Int, Int>> { + /** + * Shuts the node down. Returns immediately. + * @param drainPendingFlows whether the node will wait for pending flows to be completed before exiting. While draining, new flows from RPC will be rejected. + */ + fun terminate(drainPendingFlows: Boolean = false) - val stateMachineState = stateMachinesFeed() - var pendingFlowsCount = stateMachineState.snapshot.size - var completedFlowsCount = 0 - val updates = PublishSubject.create<Pair<Int, Int>>() - stateMachineState - .updates - .doOnNext { update -> - when (update) { - is StateMachineUpdate.Added -> { - pendingFlowsCount++ - updates.onNext(completedFlowsCount to pendingFlowsCount) - } - is StateMachineUpdate.Removed -> { - completedFlowsCount++ - updates.onNext(completedFlowsCount to pendingFlowsCount) - if (completedFlowsCount == pendingFlowsCount) { - updates.onCompleted() - } - } - } - }.subscribe() - if (completedFlowsCount == 0) { - updates.onCompleted() - } - return DataFeed(pendingFlowsCount, updates) + /** + * Returns whether the node is waiting for pending flows to complete before shutting down. + * Disabling draining mode cancels this state. + * + * @return whether the node will shutdown when the pending flows count reaches zero. + */ + fun isWaitingForShutdown(): Boolean } inline fun <reified T : ContractState> CordaRPCOps.vaultQueryBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), diff --git a/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt index 62e5e2f1f6..60816009cb 100644 --- a/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt @@ -28,4 +28,4 @@ interface AppServiceHub : ServiceHub { * TODO it is assumed here that the flow object has an appropriate classloader. */ fun <T> startTrackedFlow(flow: FlowLogic<T>): FlowProgressHandle<T> -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt b/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt index 91c714b51f..b8f2bc7757 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt @@ -140,4 +140,4 @@ interface IdentityService { fun partiesFromName(query: String, exactMatch: Boolean): Set<Party> } -class UnknownAnonymousPartyException(msg: String) : CordaException(msg) +class UnknownAnonymousPartyException(message: String) : CordaException(message) diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index a05dbf7f37..20d1984d8d 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -160,7 +160,7 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) { val notary: AbstractParty?, val lockId: String?, val lockUpdateTime: Instant?, - val isRelevant: Vault.RelevancyStatus? + val relevancyStatus: Vault.RelevancyStatus? ) { constructor(ref: StateRef, contractStateClassName: String, diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt index d8a6f9374b..609434a60b 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt @@ -73,7 +73,7 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP abstract class CommonQueryCriteria : QueryCriteria() { abstract val status: Vault.StateStatus - open val isRelevant: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL + open val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL abstract val contractStateTypes: Set<Class<out ContractState>>? override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> { return parser.parseCriteria(this) @@ -90,7 +90,7 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP val notary: List<AbstractParty>? = null, val softLockingCondition: SoftLockingCondition? = null, val timeCondition: TimeCondition? = null, - override val isRelevant: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL + override val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL ) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> { super.visit(parser) @@ -125,15 +125,15 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP val externalId: List<String>? = null, override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, override val contractStateTypes: Set<Class<out ContractState>>? = null, - override val isRelevant: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL + override val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL ) : CommonQueryCriteria() { constructor( participants: List<AbstractParty>? = null, linearId: List<UniqueIdentifier>? = null, status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, contractStateTypes: Set<Class<out ContractState>>? = null, - isRelevant: Vault.RelevancyStatus - ) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes, isRelevant) + relevancyStatus: Vault.RelevancyStatus + ) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes, relevancyStatus) constructor( participants: List<AbstractParty>? = null, @@ -175,7 +175,7 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP val issuerRef: List<OpaqueBytes>? = null, override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, override val contractStateTypes: Set<Class<out ContractState>>? = null, - override val isRelevant: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL + override val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL ) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> { super.visit(parser) @@ -215,7 +215,7 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP val expression: CriteriaExpression<L, Boolean>, override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, override val contractStateTypes: Set<Class<out ContractState>>? = null, - override val isRelevant: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL + override val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL ) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> { super.visit(parser) diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt index fa98e1d54b..faac1f7fef 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt @@ -5,6 +5,10 @@ package net.corda.core.node.services.vault import net.corda.core.DoNotImplement import net.corda.core.internal.declaredField import net.corda.core.internal.uncheckedCast +import net.corda.core.node.services.vault.CollectionOperator.* +import net.corda.core.node.services.vault.ColumnPredicate.* +import net.corda.core.node.services.vault.EqualityComparisonOperator.* +import net.corda.core.node.services.vault.LikenessOperator.* import net.corda.core.schemas.PersistentState import net.corda.core.serialization.CordaSerializable import java.lang.reflect.Field @@ -24,7 +28,9 @@ enum class BinaryLogicalOperator : Operator { enum class EqualityComparisonOperator : Operator { EQUAL, - NOT_EQUAL + NOT_EQUAL, + EQUAL_IGNORE_CASE, + NOT_EQUAL_IGNORE_CASE } enum class BinaryComparisonOperator : Operator { @@ -41,12 +47,16 @@ enum class NullOperator : Operator { enum class LikenessOperator : Operator { LIKE, - NOT_LIKE + NOT_LIKE, + LIKE_IGNORE_CASE, + NOT_LIKE_IGNORE_CASE } enum class CollectionOperator : Operator { IN, - NOT_IN + NOT_IN, + IN_IGNORE_CASE, + NOT_IN_IGNORE_CASE } @CordaSerializable @@ -251,27 +261,45 @@ object Builder { fun <R : Comparable<R>> Field.comparePredicate(operator: BinaryComparisonOperator, value: R) = info().comparePredicate(operator, value) fun <R : Comparable<R>> FieldInfo.comparePredicate(operator: BinaryComparisonOperator, value: R) = predicate(compare(operator, value)) - fun <O, R> KProperty1<O, R?>.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value)) - fun <O, R> KProperty1<O, R?>.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value)) + @JvmOverloads + fun <O, R> KProperty1<O, R?>.equal(value: R, exactMatch: Boolean = true) = predicate(Builder.equal(value, exactMatch)) + + @JvmOverloads + fun <O, R> KProperty1<O, R?>.notEqual(value: R, exactMatch: Boolean = true) = predicate(Builder.notEqual(value, exactMatch)) + fun <O, R : Comparable<R>> KProperty1<O, R?>.lessThan(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN, value) + fun <O, R : Comparable<R>> KProperty1<O, R?>.lessThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value) + fun <O, R : Comparable<R>> KProperty1<O, R?>.greaterThan(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN, value) + fun <O, R : Comparable<R>> KProperty1<O, R?>.greaterThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value) + fun <O, R : Comparable<R>> KProperty1<O, R?>.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to)) - fun <O, R : Comparable<R>> KProperty1<O, R?>.`in`(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)) - fun <O, R : Comparable<R>> KProperty1<O, R?>.notIn(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)) + + @JvmOverloads + fun <O, R : Comparable<R>> KProperty1<O, R?>.`in`(collection: Collection<R>, exactMatch: Boolean = true) = predicate(Builder.`in`(collection, exactMatch)) + + @JvmOverloads + fun <O, R : Comparable<R>> KProperty1<O, R?>.notIn(collection: Collection<R>, exactMatch: Boolean = true) = predicate(Builder.notIn(collection, exactMatch)) @JvmStatic + @JvmOverloads @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") - fun <R> Field.equal(value: R) = info().equal(value) - @JvmStatic - fun <R> FieldInfo.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value)) + fun <R> Field.equal(value: R, exactMatch: Boolean = true) = info().equal(value, exactMatch) @JvmStatic - @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") - fun <R> Field.notEqual(value: R) = info().notEqual(value) + @JvmOverloads + fun <R> FieldInfo.equal(value: R, exactMatch: Boolean = true) = predicate(Builder.equal(value, exactMatch)) + @JvmStatic - fun <R> FieldInfo.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value)) + @JvmOverloads + @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") + fun <R> Field.notEqual(value: R, exactMatch: Boolean = true) = info().notEqual(value, exactMatch) + + @JvmStatic + @JvmOverloads + fun <R> FieldInfo.notEqual(value: R, exactMatch: Boolean = true) = predicate(Builder.equal(value, exactMatch)) @JvmStatic @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") @@ -304,44 +332,77 @@ object Builder { fun <R : Comparable<R>> FieldInfo.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to)) @JvmStatic + @JvmOverloads @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") - fun <R : Comparable<R>> Field.`in`(collection: Collection<R>) = info().`in`(collection) - fun <R : Comparable<R>> FieldInfo.`in`(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)) + fun <R : Comparable<R>> Field.`in`(collection: Collection<R>, exactMatch: Boolean = true) = info().`in`(collection, exactMatch) @JvmStatic - @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") - fun <R : Comparable<R>> Field.notIn(collection: Collection<R>) = info().notIn(collection) - @JvmStatic - fun <R : Comparable<R>> FieldInfo.notIn(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)) + @JvmOverloads + fun <R : Comparable<R>> FieldInfo.`in`(collection: Collection<R>, exactMatch: Boolean = true) = predicate(Builder.`in`(collection, exactMatch)) + + @JvmStatic + @JvmOverloads + @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") + fun <R : Comparable<R>> Field.notIn(collection: Collection<R>, exactMatch: Boolean = true) = info().notIn(collection, exactMatch) + + @JvmStatic + @JvmOverloads + fun <R : Comparable<R>> FieldInfo.notIn(collection: Collection<R>, exactMatch: Boolean = true) = predicate(Builder.notIn(collection, exactMatch)) + + @JvmOverloads + fun <R> equal(value: R, exactMatch: Boolean = true) = EqualityComparison(if (exactMatch) EQUAL else EQUAL_IGNORE_CASE, value) + + @JvmOverloads + fun <R> notEqual(value: R, exactMatch: Boolean = true) = EqualityComparison(if (exactMatch) NOT_EQUAL else NOT_EQUAL_IGNORE_CASE, value) - fun <R> equal(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value) - fun <R> notEqual(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value) fun <R : Comparable<R>> lessThan(value: R) = compare(BinaryComparisonOperator.LESS_THAN, value) + fun <R : Comparable<R>> lessThanOrEqual(value: R) = compare(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value) + fun <R : Comparable<R>> greaterThan(value: R) = compare(BinaryComparisonOperator.GREATER_THAN, value) + fun <R : Comparable<R>> greaterThanOrEqual(value: R) = compare(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value) + fun <R : Comparable<R>> between(from: R, to: R) = ColumnPredicate.Between(from, to) - fun <R : Comparable<R>> `in`(collection: Collection<R>) = ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection) - fun <R : Comparable<R>> notIn(collection: Collection<R>) = ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection) - fun like(string: String) = ColumnPredicate.Likeness(LikenessOperator.LIKE, string) - fun notLike(string: String) = ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string) + + @JvmOverloads + fun <R : Comparable<R>> `in`(collection: Collection<R>, exactMatch: Boolean = true) = CollectionExpression(if (exactMatch) IN else IN_IGNORE_CASE, collection) + + @JvmOverloads + fun <R : Comparable<R>> notIn(collection: Collection<R>, exactMatch: Boolean = true) = CollectionExpression(if (exactMatch) NOT_IN else NOT_IN_IGNORE_CASE, collection) + + @JvmOverloads + fun like(string: String, exactMatch: Boolean = true) = Likeness(if (exactMatch) LIKE else LIKE_IGNORE_CASE, string) + + @JvmOverloads + fun notLike(string: String, exactMatch: Boolean = true) = Likeness(if (exactMatch) NOT_LIKE else NOT_LIKE_IGNORE_CASE, string) + fun <R> isNull() = ColumnPredicate.NullExpression<R>(NullOperator.IS_NULL) fun <R> isNotNull() = ColumnPredicate.NullExpression<R>(NullOperator.NOT_NULL) + @JvmOverloads + fun <O> KProperty1<O, String?>.like(string: String, exactMatch: Boolean = true) = predicate(Builder.like(string, exactMatch)) - fun <O> KProperty1<O, String?>.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string)) @JvmStatic + @JvmOverloads @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") - fun Field.like(string: String) = info().like(string) - @JvmStatic - fun FieldInfo.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string)) + fun Field.like(string: String, exactMatch: Boolean = true) = info().like(string, exactMatch) - fun <O> KProperty1<O, String?>.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string)) @JvmStatic + @JvmOverloads + fun FieldInfo.like(string: String, exactMatch: Boolean = true) = predicate(Builder.like(string, exactMatch)) + + @JvmOverloads + fun <O> KProperty1<O, String?>.notLike(string: String, exactMatch: Boolean = true) = predicate(Builder.notLike(string, exactMatch)) + + @JvmStatic + @JvmOverloads @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") - fun Field.notLike(string: String) = info().notLike(string) + fun Field.notLike(string: String, exactMatch: Boolean = true) = info().notLike(string, exactMatch) + @JvmStatic - fun FieldInfo.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string)) + @JvmOverloads + fun FieldInfo.notLike(string: String, exactMatch: Boolean = true) = predicate(Builder.notLike(string, exactMatch)) fun <O, R> KProperty1<O, R?>.isNull() = predicate(ColumnPredicate.NullExpression(NullOperator.IS_NULL)) @JvmStatic diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt index c5df7f7069..3a0ee16ce0 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt @@ -207,7 +207,13 @@ interface SerializationContext { * The use case that we are serializing for, since it influences the implementations chosen. */ @KeepForDJVM - enum class UseCase { P2P, RPCServer, RPCClient, Storage, Checkpoint, Testing } + enum class UseCase { + P2P, + RPCServer, + RPCClient, + Storage, + Testing + } } /** @@ -230,7 +236,6 @@ object SerializationDefaults { @DeleteForDJVM val RPC_SERVER_CONTEXT get() = effectiveSerializationEnv.rpcServerContext @DeleteForDJVM val RPC_CLIENT_CONTEXT get() = effectiveSerializationEnv.rpcClientContext @DeleteForDJVM val STORAGE_CONTEXT get() = effectiveSerializationEnv.storageContext - @DeleteForDJVM val CHECKPOINT_CONTEXT get() = effectiveSerializationEnv.checkpointContext } /** diff --git a/core/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationAPI.kt b/core/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationAPI.kt new file mode 100644 index 0000000000..448d1ab25f --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationAPI.kt @@ -0,0 +1,198 @@ +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 +import java.io.NotSerializableException + + +object CheckpointSerializationDefaults { + @DeleteForDJVM + val CHECKPOINT_CONTEXT get() = effectiveSerializationEnv.checkpointContext + val CHECKPOINT_SERIALIZATION_FACTORY get() = effectiveSerializationEnv.checkpointSerializationFactory +} + +/** + * A class for serializing and deserializing objects at checkpoints, using Kryo serialization. + */ +@KeepForDJVM +class CheckpointSerializationFactory( + private val scheme: CheckpointSerializationScheme +) { + + val defaultContext: CheckpointSerializationContext get() = _currentContext.get() ?: effectiveSerializationEnv.checkpointContext + + private val creator: List<StackTraceElement> = Exception().stackTrace.asList() + + /** + * Deserialize the bytes in to an object, using the prefixed bytes to determine the format. + * + * @param byteSequence The bytes to deserialize, including a format header prefix. + * @param clazz The class or superclass or the object to be deserialized, or [Any] or [Object] if unknown. + * @param context A context that configures various parameters to deserialization. + */ + fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: CheckpointSerializationContext): T { + return withCurrentContext(context) { scheme.deserialize(byteSequence, clazz, context) } + } + + /** + * Serialize an object to bytes using the preferred serialization format version from the context. + * + * @param obj The object to be serialized. + * @param context A context that configures various parameters to serialization, including the serialization format version. + */ + fun <T : Any> serialize(obj: T, context: CheckpointSerializationContext): SerializedBytes<T> { + return withCurrentContext(context) { scheme.serialize(obj, context) } + } + + override fun toString(): String { + return "${this.javaClass.name} scheme=$scheme ${creator.joinToString("\n")}" + } + + override fun equals(other: Any?): Boolean { + return other is CheckpointSerializationFactory && other.scheme == this.scheme + } + + override fun hashCode(): Int = scheme.hashCode() + + private val _currentContext = ThreadLocal<CheckpointSerializationContext?>() + + /** + * Change the current context inside the block to that supplied. + */ + fun <T> withCurrentContext(context: CheckpointSerializationContext?, block: () -> T): T { + val priorContext = _currentContext.get() + if (context != null) _currentContext.set(context) + try { + return block() + } finally { + if (context != null) _currentContext.set(priorContext) + } + } + + companion object { + val defaultFactory: CheckpointSerializationFactory get() = effectiveSerializationEnv.checkpointSerializationFactory + } +} + +@KeepForDJVM +@DoNotImplement +interface CheckpointSerializationScheme { + @Throws(NotSerializableException::class) + fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: CheckpointSerializationContext): T + + @Throws(NotSerializableException::class) + fun <T : Any> serialize(obj: T, context: CheckpointSerializationContext): SerializedBytes<T> +} + +/** + * Parameters to checkpoint serialization and deserialization. + */ +@KeepForDJVM +@DoNotImplement +interface CheckpointSerializationContext { + /** + * If non-null, apply this encoding (typically compression) when serializing. + */ + val encoding: SerializationEncoding? + /** + * The class loader to use for deserialization. + */ + val deserializationClassLoader: ClassLoader + /** + * A whitelist that contains (mostly for security purposes) which classes can be serialized and deserialized. + */ + val whitelist: ClassWhitelist + /** + * A whitelist that determines (mostly for security purposes) whether a particular encoding may be used when deserializing. + */ + val encodingWhitelist: EncodingWhitelist + /** + * A map of any addition properties specific to the particular use case. + */ + val properties: Map<Any, Any> + /** + * Duplicate references to the same object preserved in the wire format and when deserialized when this is true, + * otherwise they appear as new copies of the object. + */ + val objectReferencesEnabled: Boolean + + /** + * Helper method to return a new context based on this context with the property added. + */ + fun withProperty(property: Any, value: Any): CheckpointSerializationContext + + /** + * Helper method to return a new context based on this context with object references disabled. + */ + fun withoutReferences(): CheckpointSerializationContext + + /** + * Helper method to return a new context based on this context with the deserialization class loader changed. + */ + fun withClassLoader(classLoader: ClassLoader): CheckpointSerializationContext + + /** + * Helper method to return a new context based on this context with the appropriate class loader constructed from the passed attachment identifiers. + * (Requires the attachment storage to have been enabled). + */ + @Throws(MissingAttachmentsException::class) + fun withAttachmentsClassLoader(attachmentHashes: List<SecureHash>): CheckpointSerializationContext + + /** + * Helper method to return a new context based on this context with the given class specifically whitelisted. + */ + fun withWhitelisted(clazz: Class<*>): CheckpointSerializationContext + + /** + * A shallow copy of this context but with the given (possibly null) encoding. + */ + fun withEncoding(encoding: SerializationEncoding?): CheckpointSerializationContext + + /** + * A shallow copy of this context but with the given encoding whitelist. + */ + fun withEncodingWhitelist(encodingWhitelist: EncodingWhitelist): CheckpointSerializationContext +} + +/* + * The following extension methods are disambiguated from the AMQP-serialization methods by requiring that an + * explicit [CheckpointSerializationContext] parameter be provided. + */ + +/* + * Convenience extension method for deserializing a ByteSequence, utilising the default factory. + */ +inline fun <reified T : Any> ByteSequence.checkpointDeserialize(serializationFactory: CheckpointSerializationFactory = CheckpointSerializationFactory.defaultFactory, + context: CheckpointSerializationContext): T { + return serializationFactory.deserialize(this, T::class.java, context) +} + +/** + * Convenience extension method for deserializing SerializedBytes with type matching, utilising the default factory. + */ +inline fun <reified T : Any> SerializedBytes<T>.checkpointDeserialize(serializationFactory: CheckpointSerializationFactory = CheckpointSerializationFactory.defaultFactory, + context: CheckpointSerializationContext): T { + return serializationFactory.deserialize(this, T::class.java, context) +} + +/** + * Convenience extension method for deserializing a ByteArray, utilising the default factory. + */ +inline fun <reified T : Any> ByteArray.checkpointDeserialize(serializationFactory: CheckpointSerializationFactory = CheckpointSerializationFactory.defaultFactory, + context: CheckpointSerializationContext): T { + require(isNotEmpty()) { "Empty bytes" } + return this.sequence().checkpointDeserialize(serializationFactory, context) +} + +/** + * Convenience extension method for serializing an object of type T, utilising the default factory. + */ +fun <T : Any> T.checkpointSerialize(serializationFactory: CheckpointSerializationFactory = CheckpointSerializationFactory.defaultFactory, + context: CheckpointSerializationContext): SerializedBytes<T> { + return serializationFactory.serialize(this, context) +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/internal/SerializationEnvironment.kt b/core/src/main/kotlin/net/corda/core/serialization/internal/SerializationEnvironment.kt index 28c6ad7900..441cd52be4 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/internal/SerializationEnvironment.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/internal/SerializationEnvironment.kt @@ -12,11 +12,12 @@ import net.corda.core.serialization.SerializationFactory @KeepForDJVM interface SerializationEnvironment { val serializationFactory: SerializationFactory + val checkpointSerializationFactory: CheckpointSerializationFactory val p2pContext: SerializationContext val rpcServerContext: SerializationContext val rpcClientContext: SerializationContext val storageContext: SerializationContext - val checkpointContext: SerializationContext + val checkpointContext: CheckpointSerializationContext } @KeepForDJVM @@ -26,18 +27,21 @@ open class SerializationEnvironmentImpl( rpcServerContext: SerializationContext? = null, rpcClientContext: SerializationContext? = null, storageContext: SerializationContext? = null, - checkpointContext: SerializationContext? = null) : SerializationEnvironment { + checkpointContext: CheckpointSerializationContext? = null, + checkpointSerializationFactory: CheckpointSerializationFactory? = null) : SerializationEnvironment { // Those that are passed in as null are never inited: override lateinit var rpcServerContext: SerializationContext override lateinit var rpcClientContext: SerializationContext override lateinit var storageContext: SerializationContext - override lateinit var checkpointContext: SerializationContext + override lateinit var checkpointContext: CheckpointSerializationContext + override lateinit var checkpointSerializationFactory: CheckpointSerializationFactory init { rpcServerContext?.let { this.rpcServerContext = it } rpcClientContext?.let { this.rpcClientContext = it } storageContext?.let { this.storageContext = it } checkpointContext?.let { this.checkpointContext = it } + checkpointSerializationFactory?.let { this.checkpointSerializationFactory = it } } } diff --git a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt index dbc5ef95fb..aef380e153 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt @@ -3,12 +3,15 @@ package net.corda.core.transactions import net.corda.core.CordaException import net.corda.core.KeepForDJVM import net.corda.core.contracts.* +import net.corda.core.contracts.ComponentGroupEnum.* import net.corda.core.crypto.* import net.corda.core.identity.Party +import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.* import net.corda.core.utilities.OpaqueBytes import java.security.PublicKey import java.util.function.Predicate +import kotlin.reflect.KClass /** * Implemented by [WireTransaction] and [FilteredTransaction]. A TraversableTransaction allows you to iterate @@ -18,29 +21,29 @@ import java.util.function.Predicate */ abstract class TraversableTransaction(open val componentGroups: List<ComponentGroup>) : CoreTransaction() { /** Hashes of the ZIP/JAR files that are needed to interpret the contents of this wire transaction. */ - val attachments: List<SecureHash> = deserialiseComponentGroup(ComponentGroupEnum.ATTACHMENTS_GROUP, { SerializedBytes<SecureHash>(it).deserialize() }) + val attachments: List<SecureHash> = deserialiseComponentGroup(SecureHash::class, ATTACHMENTS_GROUP) /** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */ - override val inputs: List<StateRef> = deserialiseComponentGroup(ComponentGroupEnum.INPUTS_GROUP, { SerializedBytes<StateRef>(it).deserialize() }) + override val inputs: List<StateRef> = deserialiseComponentGroup(StateRef::class, INPUTS_GROUP) /** Pointers to reference states, identified by (tx identity hash, output index). */ - override val references: List<StateRef> = deserialiseComponentGroup(ComponentGroupEnum.REFERENCES_GROUP, { SerializedBytes<StateRef>(it).deserialize() }) + override val references: List<StateRef> = deserialiseComponentGroup(StateRef::class, REFERENCES_GROUP) - override val outputs: List<TransactionState<ContractState>> = deserialiseComponentGroup(ComponentGroupEnum.OUTPUTS_GROUP, { SerializedBytes<TransactionState<ContractState>>(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) }) + override val outputs: List<TransactionState<ContractState>> = deserialiseComponentGroup(TransactionState::class, OUTPUTS_GROUP, attachmentsContext = true) /** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */ val commands: List<Command<*>> = deserialiseCommands() override val notary: Party? = let { - val notaries: List<Party> = deserialiseComponentGroup(ComponentGroupEnum.NOTARY_GROUP, { SerializedBytes<Party>(it).deserialize() }) + val notaries: List<Party> = deserialiseComponentGroup(Party::class, NOTARY_GROUP) check(notaries.size <= 1) { "Invalid Transaction. More than 1 notary party detected." } - if (notaries.isNotEmpty()) notaries[0] else null + notaries.firstOrNull() } val timeWindow: TimeWindow? = let { - val timeWindows: List<TimeWindow> = deserialiseComponentGroup(ComponentGroupEnum.TIMEWINDOW_GROUP, { SerializedBytes<TimeWindow>(it).deserialize() }) + val timeWindows: List<TimeWindow> = deserialiseComponentGroup(TimeWindow::class, TIMEWINDOW_GROUP) check(timeWindows.size <= 1) { "Invalid Transaction. More than 1 time-window detected." } - if (timeWindows.isNotEmpty()) timeWindows[0] else null + timeWindows.firstOrNull() } /** @@ -63,12 +66,16 @@ abstract class TraversableTransaction(open val componentGroups: List<ComponentGr } // Helper function to return a meaningful exception if deserialisation of a component fails. - private fun <T> deserialiseComponentGroup(groupEnum: ComponentGroupEnum, deserialiseBody: (ByteArray) -> T): List<T> { + private fun <T : Any> deserialiseComponentGroup(clazz: KClass<T>, + groupEnum: ComponentGroupEnum, + attachmentsContext: Boolean = false): List<T> { + val factory = SerializationFactory.defaultFactory + val context = factory.defaultContext.let { if (attachmentsContext) it.withAttachmentsClassLoader(attachments) else it } val group = componentGroups.firstOrNull { it.groupIndex == groupEnum.ordinal } return if (group != null && group.components.isNotEmpty()) { group.components.mapIndexed { internalIndex, component -> try { - deserialiseBody(component.bytes) + factory.deserialize(component, clazz.java, context) } catch (e: MissingAttachmentsException) { throw e } catch (e: Exception) { @@ -87,11 +94,13 @@ abstract class TraversableTransaction(open val componentGroups: List<ComponentGr // TODO: we could avoid deserialising unrelated signers. // However, current approach ensures the transaction is not malformed // and it will throw if any of the signers objects is not List of public keys). - val signersList = deserialiseComponentGroup(ComponentGroupEnum.SIGNERS_GROUP, { SerializedBytes<List<PublicKey>>(it).deserialize() }) - val commandDataList = deserialiseComponentGroup(ComponentGroupEnum.COMMANDS_GROUP, { SerializedBytes<CommandData>(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) }) - val group = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal } + val signersList: List<List<PublicKey>> = uncheckedCast(deserialiseComponentGroup(List::class, SIGNERS_GROUP)) + val commandDataList: List<CommandData> = deserialiseComponentGroup(CommandData::class, COMMANDS_GROUP, attachmentsContext = true) + val group = componentGroups.firstOrNull { it.groupIndex == COMMANDS_GROUP.ordinal } return if (group is FilteredComponentGroup) { - check(commandDataList.size <= signersList.size) { "Invalid Transaction. Less Signers (${signersList.size}) than CommandData (${commandDataList.size}) objects" } + check(commandDataList.size <= signersList.size) { + "Invalid Transaction. Less Signers (${signersList.size}) than CommandData (${commandDataList.size}) objects" + } val componentHashes = group.components.mapIndexed { index, component -> componentHash(group.nonces[index], component) } val leafIndices = componentHashes.map { group.partialMerkleTree.leafIndex(it) } if (leafIndices.isNotEmpty()) @@ -100,7 +109,9 @@ abstract class TraversableTransaction(open val componentGroups: List<ComponentGr } else { // It is a WireTransaction // or a FilteredTransaction with no Commands (in which case group is null). - check(commandDataList.size == signersList.size) { "Invalid Transaction. Sizes of CommandData (${commandDataList.size}) and Signers (${signersList.size}) do not match" } + check(commandDataList.size == signersList.size) { + "Invalid Transaction. Sizes of CommandData (${commandDataList.size}) and Signers (${signersList.size}) do not match" + } commandDataList.mapIndexed { index, commandData -> Command(commandData, signersList[index]) } } } @@ -145,47 +156,47 @@ class FilteredTransaction internal constructor( var signersIncluded = false fun <T : Any> filter(t: T, componentGroupIndex: Int, internalIndex: Int) { - if (filtering.test(t)) { - val group = filteredSerialisedComponents[componentGroupIndex] - // Because the filter passed, we know there is a match. We also use first Vs single as the init function - // of WireTransaction ensures there are no duplicated groups. - val serialisedComponent = wtx.componentGroups.first { it.groupIndex == componentGroupIndex }.components[internalIndex] - if (group == null) { - // As all of the helper Map structures, like availableComponentNonces, availableComponentHashes - // and groupsMerkleRoots, are computed lazily via componentGroups.forEach, there should always be - // a match on Map.get ensuring it will never return null. - filteredSerialisedComponents[componentGroupIndex] = mutableListOf(serialisedComponent) - filteredComponentNonces[componentGroupIndex] = mutableListOf(wtx.availableComponentNonces[componentGroupIndex]!![internalIndex]) - filteredComponentHashes[componentGroupIndex] = mutableListOf(wtx.availableComponentHashes[componentGroupIndex]!![internalIndex]) - } else { - group.add(serialisedComponent) - // If the group[componentGroupIndex] existed, then we guarantee that - // filteredComponentNonces[componentGroupIndex] and filteredComponentHashes[componentGroupIndex] are not null. - filteredComponentNonces[componentGroupIndex]!!.add(wtx.availableComponentNonces[componentGroupIndex]!![internalIndex]) - filteredComponentHashes[componentGroupIndex]!!.add(wtx.availableComponentHashes[componentGroupIndex]!![internalIndex]) - } - // If at least one command is visible, then all command-signers should be visible as well. - // This is required for visibility purposes, see FilteredTransaction.checkAllCommandsVisible() for more details. - if (componentGroupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal && !signersIncluded) { - signersIncluded = true - val signersGroupIndex = ComponentGroupEnum.SIGNERS_GROUP.ordinal - // There exist commands, thus the signers group is not empty. - val signersGroupComponents = wtx.componentGroups.first { it.groupIndex == signersGroupIndex } - filteredSerialisedComponents[signersGroupIndex] = signersGroupComponents.components.toMutableList() - filteredComponentNonces[signersGroupIndex] = wtx.availableComponentNonces[signersGroupIndex]!!.toMutableList() - filteredComponentHashes[signersGroupIndex] = wtx.availableComponentHashes[signersGroupIndex]!!.toMutableList() - } + if (!filtering.test(t)) return + + val group = filteredSerialisedComponents[componentGroupIndex] + // Because the filter passed, we know there is a match. We also use first Vs single as the init function + // of WireTransaction ensures there are no duplicated groups. + val serialisedComponent = wtx.componentGroups.first { it.groupIndex == componentGroupIndex }.components[internalIndex] + if (group == null) { + // As all of the helper Map structures, like availableComponentNonces, availableComponentHashes + // and groupsMerkleRoots, are computed lazily via componentGroups.forEach, there should always be + // a match on Map.get ensuring it will never return null. + filteredSerialisedComponents[componentGroupIndex] = mutableListOf(serialisedComponent) + filteredComponentNonces[componentGroupIndex] = mutableListOf(wtx.availableComponentNonces[componentGroupIndex]!![internalIndex]) + filteredComponentHashes[componentGroupIndex] = mutableListOf(wtx.availableComponentHashes[componentGroupIndex]!![internalIndex]) + } else { + group.add(serialisedComponent) + // If the group[componentGroupIndex] existed, then we guarantee that + // filteredComponentNonces[componentGroupIndex] and filteredComponentHashes[componentGroupIndex] are not null. + filteredComponentNonces[componentGroupIndex]!!.add(wtx.availableComponentNonces[componentGroupIndex]!![internalIndex]) + filteredComponentHashes[componentGroupIndex]!!.add(wtx.availableComponentHashes[componentGroupIndex]!![internalIndex]) + } + // If at least one command is visible, then all command-signers should be visible as well. + // This is required for visibility purposes, see FilteredTransaction.checkAllCommandsVisible() for more details. + if (componentGroupIndex == COMMANDS_GROUP.ordinal && !signersIncluded) { + signersIncluded = true + val signersGroupIndex = SIGNERS_GROUP.ordinal + // There exist commands, thus the signers group is not empty. + val signersGroupComponents = wtx.componentGroups.first { it.groupIndex == signersGroupIndex } + filteredSerialisedComponents[signersGroupIndex] = signersGroupComponents.components.toMutableList() + filteredComponentNonces[signersGroupIndex] = wtx.availableComponentNonces[signersGroupIndex]!!.toMutableList() + filteredComponentHashes[signersGroupIndex] = wtx.availableComponentHashes[signersGroupIndex]!!.toMutableList() } } fun updateFilteredComponents() { - wtx.inputs.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.INPUTS_GROUP.ordinal, internalIndex) } - wtx.outputs.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.OUTPUTS_GROUP.ordinal, internalIndex) } - wtx.commands.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.COMMANDS_GROUP.ordinal, internalIndex) } - wtx.attachments.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.ATTACHMENTS_GROUP.ordinal, internalIndex) } - if (wtx.notary != null) filter(wtx.notary, ComponentGroupEnum.NOTARY_GROUP.ordinal, 0) - if (wtx.timeWindow != null) filter(wtx.timeWindow, ComponentGroupEnum.TIMEWINDOW_GROUP.ordinal, 0) - wtx.references.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.REFERENCES_GROUP.ordinal, internalIndex) } + wtx.inputs.forEachIndexed { internalIndex, it -> filter(it, INPUTS_GROUP.ordinal, internalIndex) } + wtx.outputs.forEachIndexed { internalIndex, it -> filter(it, OUTPUTS_GROUP.ordinal, internalIndex) } + wtx.commands.forEachIndexed { internalIndex, it -> filter(it, COMMANDS_GROUP.ordinal, internalIndex) } + wtx.attachments.forEachIndexed { internalIndex, it -> filter(it, ATTACHMENTS_GROUP.ordinal, internalIndex) } + if (wtx.notary != null) filter(wtx.notary, NOTARY_GROUP.ordinal, 0) + if (wtx.timeWindow != null) filter(wtx.timeWindow, TIMEWINDOW_GROUP.ordinal, 0) + wtx.references.forEachIndexed { internalIndex, it -> filter(it, REFERENCES_GROUP.ordinal, internalIndex) } // It is highlighted that because there is no a signers property in TraversableTransaction, // one cannot specifically filter them in or out. // The above is very important to ensure someone won't filter out the signers component group if at least one @@ -195,10 +206,17 @@ class FilteredTransaction internal constructor( // we decide to filter and attach this field to a FilteredTransaction. // An example would be to redact certain contract state types, but otherwise leave a transaction alone, // including the unknown new components. - wtx.componentGroups.filter { it.groupIndex >= ComponentGroupEnum.values().size }.forEach { componentGroup -> componentGroup.components.forEachIndexed { internalIndex, component -> filter(component, componentGroup.groupIndex, internalIndex) } } + wtx.componentGroups + .filter { it.groupIndex >= values().size } + .forEach { componentGroup -> componentGroup.components.forEachIndexed { internalIndex, component -> filter(component, componentGroup.groupIndex, internalIndex) } } } - fun createPartialMerkleTree(componentGroupIndex: Int) = PartialMerkleTree.build(MerkleTree.getMerkleTree(wtx.availableComponentHashes[componentGroupIndex]!!), filteredComponentHashes[componentGroupIndex]!!) + fun createPartialMerkleTree(componentGroupIndex: Int): PartialMerkleTree { + return PartialMerkleTree.build( + MerkleTree.getMerkleTree(wtx.availableComponentHashes[componentGroupIndex]!!), + filteredComponentHashes[componentGroupIndex]!! + ) + } fun createFilteredComponentGroups(): List<FilteredComponentGroup> { updateFilteredComponents() @@ -223,8 +241,11 @@ class FilteredTransaction internal constructor( @Throws(FilteredTransactionVerificationException::class) fun verify() { verificationCheck(groupHashes.isNotEmpty()) { "At least one component group hash is required" } - // Verify the top level Merkle tree (group hashes are its leaves, including allOnesHash for empty list or null components in WireTransaction). - verificationCheck(MerkleTree.getMerkleTree(groupHashes).hash == id) { "Top level Merkle tree cannot be verified against transaction's id" } + // Verify the top level Merkle tree (group hashes are its leaves, including allOnesHash for empty list or null + // components in WireTransaction). + verificationCheck(MerkleTree.getMerkleTree(groupHashes).hash == id) { + "Top level Merkle tree cannot be verified against transaction's id" + } // For completely blind verification (no components are included). if (filteredComponentGroups.isEmpty()) return @@ -233,8 +254,12 @@ class FilteredTransaction internal constructor( filteredComponentGroups.forEach { (groupIndex, components, nonces, groupPartialTree) -> verificationCheck(groupIndex < groupHashes.size) { "There is no matching component group hash for group $groupIndex" } val groupMerkleRoot = groupHashes[groupIndex] - verificationCheck(groupMerkleRoot == PartialMerkleTree.rootAndUsedHashes(groupPartialTree.root, mutableListOf())) { "Partial Merkle tree root and advertised full Merkle tree root for component group $groupIndex do not match" } - verificationCheck(groupPartialTree.verify(groupMerkleRoot, components.mapIndexed { index, component -> componentHash(nonces[index], component) })) { "Visible components in group $groupIndex cannot be verified against their partial Merkle tree" } + verificationCheck(groupMerkleRoot == PartialMerkleTree.rootAndUsedHashes(groupPartialTree.root, mutableListOf())) { + "Partial Merkle tree root and advertised full Merkle tree root for component group $groupIndex do not match" + } + verificationCheck(groupPartialTree.verify(groupMerkleRoot, components.mapIndexed { index, component -> componentHash(nonces[index], component) })) { + "Visible components in group $groupIndex cannot be verified against their partial Merkle tree" + } } } @@ -281,7 +306,9 @@ class FilteredTransaction internal constructor( val groupFullRoot = MerkleTree.getMerkleTree(group.components.mapIndexed { index, component -> componentHash(group.nonces[index], component) }).hash visibilityCheck(groupPartialRoot == groupFullRoot) { "Some components for group ${group.groupIndex} are not visible" } // Verify the top level Merkle tree from groupHashes. - visibilityCheck(MerkleTree.getMerkleTree(groupHashes).hash == id) { "Transaction is malformed. Top level Merkle tree cannot be verified against transaction's id" } + visibilityCheck(MerkleTree.getMerkleTree(groupHashes).hash == id) { + "Transaction is malformed. Top level Merkle tree cannot be verified against transaction's id" + } } } @@ -296,15 +323,17 @@ class FilteredTransaction internal constructor( */ @Throws(ComponentVisibilityException::class) fun checkCommandVisibility(publicKey: PublicKey) { - val commandSigners = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.SIGNERS_GROUP.ordinal } + val commandSigners = componentGroups.firstOrNull { it.groupIndex == SIGNERS_GROUP.ordinal } val expectedNumOfCommands = expectedNumOfCommands(publicKey, commandSigners) val receivedForThisKeyNumOfCommands = commands.filter { publicKey in it.signers }.size - visibilityCheck(expectedNumOfCommands == receivedForThisKeyNumOfCommands) { "$expectedNumOfCommands commands were expected, but received $receivedForThisKeyNumOfCommands" } + visibilityCheck(expectedNumOfCommands == receivedForThisKeyNumOfCommands) { + "$expectedNumOfCommands commands were expected, but received $receivedForThisKeyNumOfCommands" + } } // Function to return number of expected commands to sign. private fun expectedNumOfCommands(publicKey: PublicKey, commandSigners: ComponentGroup?): Int { - checkAllComponentsVisible(ComponentGroupEnum.SIGNERS_GROUP) + checkAllComponentsVisible(SIGNERS_GROUP) if (commandSigners == null) return 0 fun signersKeys (internalIndex: Int, opaqueBytes: OpaqueBytes): List<PublicKey> { try { @@ -340,7 +369,10 @@ class FilteredTransaction internal constructor( */ @KeepForDJVM @CordaSerializable -data class FilteredComponentGroup(override val groupIndex: Int, override val components: List<OpaqueBytes>, val nonces: List<SecureHash>, val partialMerkleTree: PartialMerkleTree) : ComponentGroup(groupIndex, components) { +data class FilteredComponentGroup(override val groupIndex: Int, + override val components: List<OpaqueBytes>, + val nonces: List<SecureHash>, + val partialMerkleTree: PartialMerkleTree) : ComponentGroup(groupIndex, components) { init { check(components.size == nonces.size) { "Size of transaction components and nonces do not match" } } diff --git a/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt index 1d5ceb3f73..0407bd95bb 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt @@ -33,9 +33,10 @@ sealed class ByteSequence(private val _bytes: ByteArray, val offset: Int, val si fun open() = ByteArrayInputStream(_bytes, offset, size) /** - * Create a sub-sequence, that may be backed by a new byte array. + * Create a sub-sequence of this sequence. A copy of the underlying array may be made, if a subclass overrides + * [bytes] to do so, as [OpaqueBytes] does. * - * @param offset The offset within this sequence to start the new sequence. Note: not the offset within the backing array. + * @param offset The offset within this sequence to start the new sequence. Note: not the offset within the backing array. * @param size The size of the intended sub sequence. */ @Suppress("MemberVisibilityCanBePrivate") @@ -43,7 +44,7 @@ sealed class ByteSequence(private val _bytes: ByteArray, val offset: Int, val si require(offset >= 0) require(offset + size <= this.size) // Intentionally use bytes rather than _bytes, to mirror the copy-or-not behaviour of that property. - return if (offset == 0 && size == this.size) this else OpaqueBytesSubSequence(bytes, this.offset + offset, size) + return if (offset == 0 && size == this.size) this else of(bytes, this.offset + offset, size) } companion object { diff --git a/core/src/smoke-test/kotlin/net/corda/core/cordapp/CordappSmokeTest.kt b/core/src/smoke-test/kotlin/net/corda/core/cordapp/CordappSmokeTest.kt index 44195477d0..69eef61ffd 100644 --- a/core/src/smoke-test/kotlin/net/corda/core/cordapp/CordappSmokeTest.kt +++ b/core/src/smoke-test/kotlin/net/corda/core/cordapp/CordappSmokeTest.kt @@ -106,7 +106,7 @@ class CordappSmokeTest { class SendBackInitiatorFlowContext(private val otherPartySession: FlowSession) : FlowLogic<Unit>() { @Suspendable override fun call() { - // An initiated flow calling getFlowContext on its initiator will get the context from the session-init + // An initiated flow calling getFlowInfo on its initiator will get the context from the session-init val sessionInitContext = otherPartySession.getCounterpartyFlowInfo() otherPartySession.send(sessionInitContext) } diff --git a/core/src/test/java/net/corda/core/flows/SerializationApiInJavaTest.java b/core/src/test/java/net/corda/core/flows/SerializationApiInJavaTest.java index 297adeff8f..55e66c1766 100644 --- a/core/src/test/java/net/corda/core/flows/SerializationApiInJavaTest.java +++ b/core/src/test/java/net/corda/core/flows/SerializationApiInJavaTest.java @@ -1,11 +1,14 @@ package net.corda.core.flows; +import net.corda.core.serialization.internal.CheckpointSerializationDefaults; +import net.corda.core.serialization.internal.CheckpointSerializationFactory; import net.corda.core.serialization.SerializationDefaults; import net.corda.core.serialization.SerializationFactory; import net.corda.testing.core.SerializationEnvironmentRule; import org.junit.Rule; import org.junit.Test; +import static net.corda.core.serialization.internal.CheckpointSerializationAPIKt.checkpointSerialize; import static net.corda.core.serialization.SerializationAPIKt.serialize; import static org.junit.Assert.assertNull; @@ -28,10 +31,13 @@ public class SerializationApiInJavaTest { public void enforceSerializationDefaultsApi() { SerializationDefaults defaults = SerializationDefaults.INSTANCE; SerializationFactory factory = defaults.getSERIALIZATION_FACTORY(); + + CheckpointSerializationDefaults checkpointDefaults = CheckpointSerializationDefaults.INSTANCE; + CheckpointSerializationFactory checkpointSerializationFactory = checkpointDefaults.getCHECKPOINT_SERIALIZATION_FACTORY(); serialize("hello", factory, defaults.getP2P_CONTEXT()); serialize("hello", factory, defaults.getRPC_SERVER_CONTEXT()); serialize("hello", factory, defaults.getRPC_CLIENT_CONTEXT()); serialize("hello", factory, defaults.getSTORAGE_CONTEXT()); - serialize("hello", factory, defaults.getCHECKPOINT_CONTEXT()); + checkpointSerialize("hello", checkpointSerializationFactory, checkpointDefaults.getCHECKPOINT_CONTEXT()); } } diff --git a/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt index e96eb9cd76..2263f85b39 100644 --- a/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt @@ -105,7 +105,7 @@ internal class UseRefState(val linearId: UniqueIdentifier) : FlowLogic<SignedTra val notary = serviceHub.networkMapCache.notaryIdentities.first() val query = QueryCriteria.LinearStateQueryCriteria( linearId = listOf(linearId), - isRelevant = Vault.RelevancyStatus.ALL + relevancyStatus = Vault.RelevancyStatus.ALL ) val referenceState = serviceHub.vaultService.queryBy<ContractState>(query).states.single() return subFlow(FinalityFlow( diff --git a/core/src/test/kotlin/net/corda/core/internal/CertRoleTests.kt b/core/src/test/kotlin/net/corda/core/internal/CertRoleTests.kt index 71fba5a51e..60f81927c8 100644 --- a/core/src/test/kotlin/net/corda/core/internal/CertRoleTests.kt +++ b/core/src/test/kotlin/net/corda/core/internal/CertRoleTests.kt @@ -8,7 +8,7 @@ import kotlin.test.assertFailsWith class CertRoleTests { @Test fun `should deserialize valid value`() { - val expected = CertRole.INTERMEDIATE_CA + val expected = CertRole.DOORMAN_CA val actual = CertRole.getInstance(ASN1Integer(1L)) assertEquals(expected, actual) } diff --git a/core/src/test/kotlin/net/corda/core/internal/cordapp/CordappInfoResolverTest.kt b/core/src/test/kotlin/net/corda/core/internal/cordapp/CordappInfoResolverTest.kt new file mode 100644 index 0000000000..2ae7403e1a --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/internal/cordapp/CordappInfoResolverTest.kt @@ -0,0 +1,42 @@ +package net.corda.core.internal.cordapp + +import org.junit.After +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals + +class CordappInfoResolverTest { + + @Before + @After + fun clearCordappInfoResolver() { + CordappInfoResolver.clear() + } + + @Test() + fun `The correct cordapp resolver is used after calling withCordappResolution`() { + val defaultTargetVersion = 222 + + CordappInfoResolver.register(listOf(javaClass.name), CordappImpl.Info("test", "test", "2", 3, defaultTargetVersion)) + assertEquals(defaultTargetVersion, returnCallingTargetVersion()) + + val expectedTargetVersion = 555 + CordappInfoResolver.withCordappInfoResolution( { CordappImpl.Info("foo", "bar", "1", 2, expectedTargetVersion) }) + { + val actualTargetVersion = returnCallingTargetVersion() + assertEquals(expectedTargetVersion, actualTargetVersion) + } + assertEquals(defaultTargetVersion, returnCallingTargetVersion()) + } + + @Test() + fun `When more than one cordapp is registered for the same class, the resolver returns null`() { + CordappInfoResolver.register(listOf(javaClass.name), CordappImpl.Info("test", "test", "2", 3, 222)) + CordappInfoResolver.register(listOf(javaClass.name), CordappImpl.Info("test1", "test1", "1", 2, 456)) + assertEquals(0, returnCallingTargetVersion()) + } + + private fun returnCallingTargetVersion(): Int { + return CordappInfoResolver.getCorDappInfo()?.targetPlatformVersion ?: 0 + } +} \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/utilities/KotlinUtilsTest.kt b/core/src/test/kotlin/net/corda/core/utilities/KotlinUtilsTest.kt index fefb890213..debb6307f0 100644 --- a/core/src/test/kotlin/net/corda/core/utilities/KotlinUtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/utilities/KotlinUtilsTest.kt @@ -3,9 +3,10 @@ package net.corda.core.utilities import com.esotericsoftware.kryo.KryoException import net.corda.core.crypto.random63BitValue import net.corda.core.serialization.* +import net.corda.core.serialization.internal.checkpointDeserialize +import net.corda.core.serialization.internal.checkpointSerialize import net.corda.node.serialization.kryo.KRYO_CHECKPOINT_CONTEXT -import net.corda.node.serialization.kryo.kryoMagic -import net.corda.serialization.internal.SerializationContextImpl +import net.corda.serialization.internal.CheckpointSerializationContextImpl import net.corda.testing.core.SerializationEnvironmentRule import org.assertj.core.api.Assertions.assertThat import org.junit.Rule @@ -24,12 +25,11 @@ class KotlinUtilsTest { @Rule val expectedEx: ExpectedException = ExpectedException.none() - private val KRYO_CHECKPOINT_NOWHITELIST_CONTEXT = SerializationContextImpl(kryoMagic, + private val KRYO_CHECKPOINT_NOWHITELIST_CONTEXT = CheckpointSerializationContextImpl( javaClass.classLoader, EmptyWhitelist, emptyMap(), true, - SerializationContext.UseCase.Checkpoint, null) @Test @@ -44,7 +44,7 @@ class KotlinUtilsTest { fun `checkpointing a transient property with non-capturing lambda`() { val original = NonCapturingTransientProperty() val originalVal = original.transientVal - val copy = original.serialize(context = KRYO_CHECKPOINT_CONTEXT).deserialize(context = KRYO_CHECKPOINT_CONTEXT) + val copy = original.checkpointSerialize(context = KRYO_CHECKPOINT_CONTEXT).checkpointDeserialize(context = KRYO_CHECKPOINT_CONTEXT) val copyVal = copy.transientVal assertThat(copyVal).isNotEqualTo(originalVal) assertThat(copy.transientVal).isEqualTo(copyVal) @@ -55,14 +55,14 @@ class KotlinUtilsTest { expectedEx.expect(KryoException::class.java) expectedEx.expectMessage("is not annotated or on the whitelist, so cannot be used in serialization") val original = NonCapturingTransientProperty() - original.serialize(context = KRYO_CHECKPOINT_CONTEXT).deserialize(context = KRYO_CHECKPOINT_NOWHITELIST_CONTEXT) + original.checkpointSerialize(context = KRYO_CHECKPOINT_CONTEXT).checkpointDeserialize(context = KRYO_CHECKPOINT_NOWHITELIST_CONTEXT) } @Test fun `checkpointing a transient property with capturing lambda`() { val original = CapturingTransientProperty("Hello") val originalVal = original.transientVal - val copy = original.serialize(context = KRYO_CHECKPOINT_CONTEXT).deserialize(context = KRYO_CHECKPOINT_CONTEXT) + val copy = original.checkpointSerialize(context = KRYO_CHECKPOINT_CONTEXT).checkpointDeserialize(context = KRYO_CHECKPOINT_CONTEXT) val copyVal = copy.transientVal assertThat(copyVal).isNotEqualTo(originalVal) assertThat(copy.transientVal).isEqualTo(copyVal) @@ -76,7 +76,7 @@ class KotlinUtilsTest { val original = CapturingTransientProperty("Hello") - original.serialize(context = KRYO_CHECKPOINT_CONTEXT).deserialize(context = KRYO_CHECKPOINT_NOWHITELIST_CONTEXT) + original.checkpointSerialize(context = KRYO_CHECKPOINT_CONTEXT).checkpointDeserialize(context = KRYO_CHECKPOINT_NOWHITELIST_CONTEXT) } private class NullTransientProperty { diff --git a/djvm/build.gradle b/djvm/build.gradle index 270bfc2375..db88e8c4c5 100644 --- a/djvm/build.gradle +++ b/djvm/build.gradle @@ -1,18 +1,35 @@ plugins { - id 'com.github.johnrengelman.shadow' version '2.0.4' + id 'com.github.johnrengelman.shadow' } +apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' +apply plugin: 'idea' + +description 'Corda deterministic JVM sandbox' ext { // Shaded version of ASM to avoid conflict with root project. - asm_version = '6.1.1' + asm_version = '6.2.1' +} + +repositories { + maven { + url "$artifactory_contextUrl/corda-dev" + } +} + +configurations { + testCompile.extendsFrom shadow + jdkRt.resolutionStrategy { + // Always check the repository for a newer SNAPSHOT. + cacheChangingModulesFor 0, 'seconds' + } } dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - compile "org.slf4j:jul-to-slf4j:$slf4j_version" - compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" - compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version" + shadow "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + shadow "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + shadow "org.slf4j:slf4j-api:$slf4j_version" // ASM: byte code manipulation library compile "org.ow2.asm:asm:$asm_version" @@ -20,30 +37,40 @@ dependencies { compile "org.ow2.asm:asm-commons:$asm_version" // Classpath scanner - compile "io.github.lukehutch:fast-classpath-scanner:$fast_classpath_scanner_version" + shadow "io.github.lukehutch:fast-classpath-scanner:$fast_classpath_scanner_version" // Test utilities testCompile "junit:junit:$junit_version" testCompile "org.assertj:assertj-core:$assertj_version" + testCompile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" + jdkRt "net.corda:deterministic-rt:latest.integration" } jar.enabled = false shadowJar { - baseName = "djvm" - classifier = "" - dependencies { - exclude(dependency('com.jcabi:.*:.*')) - exclude(dependency('org.apache.*:.*:.*')) - exclude(dependency('org.jetbrains.*:.*:.*')) - exclude(dependency('org.slf4j:.*:.*')) - exclude(dependency('io.github.lukehutch:.*:.*')) - } + baseName 'corda-djvm' + classifier '' relocate 'org.objectweb.asm', 'djvm.org.objectweb.asm' - artifacts { - shadow(tasks.shadowJar.archivePath) { - builtBy shadowJar - } - } } assemble.dependsOn shadowJar + +tasks.withType(Test) { + systemProperty 'deterministic-rt.path', configurations.jdkRt.asPath +} + +artifacts { + publish shadowJar +} + +publish { + dependenciesFrom configurations.shadow + name shadowJar.baseName +} + +idea { + module { + downloadJavadoc = true + downloadSources = true + } +} diff --git a/djvm/cli/build.gradle b/djvm/cli/build.gradle index a3543bcc54..d72a4a74c0 100644 --- a/djvm/cli/build.gradle +++ b/djvm/cli/build.gradle @@ -15,12 +15,10 @@ configurations { dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - compile "org.slf4j:jul-to-slf4j:$slf4j_version" compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version" compile "info.picocli:picocli:$picocli_version" - compile "io.github.lukehutch:fast-classpath-scanner:$fast_classpath_scanner_version" compile project(path: ":djvm", configuration: "shadow") // Deterministic runtime - used in whitelist generation diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/BuildCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/BuildCommand.kt index 298ebcb1ce..7fafd5d743 100644 --- a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/BuildCommand.kt +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/BuildCommand.kt @@ -1,8 +1,5 @@ package net.corda.djvm.tools.cli -import net.corda.djvm.tools.Utilities.createCodePath -import net.corda.djvm.tools.Utilities.getFileNames -import net.corda.djvm.tools.Utilities.jarPath import picocli.CommandLine.Command import picocli.CommandLine.Parameters import java.nio.file.Path diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/ClassCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/ClassCommand.kt index e3538535d0..e22d9c084f 100644 --- a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/ClassCommand.kt +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/ClassCommand.kt @@ -7,9 +7,6 @@ import net.corda.djvm.execution.* import net.corda.djvm.references.ClassModule import net.corda.djvm.source.ClassSource import net.corda.djvm.source.SourceClassLoader -import net.corda.djvm.tools.Utilities.find -import net.corda.djvm.tools.Utilities.onEmpty -import net.corda.djvm.tools.Utilities.userClassPath import net.corda.djvm.utilities.Discovery import djvm.org.objectweb.asm.ClassReader import picocli.CommandLine.Option @@ -66,7 +63,7 @@ abstract class ClassCommand : CommandBase() { private lateinit var classLoader: ClassLoader - protected var executor = SandboxExecutor<Any, Any>() + protected var executor = SandboxExecutor<Any, Any>(SandboxConfiguration.DEFAULT) private var derivedWhitelist: Whitelist = Whitelist.MINIMAL @@ -117,7 +114,7 @@ abstract class ClassCommand : CommandBase() { } private fun findDiscoverableRunnables(filters: Array<String>): List<Class<*>> { - val classes = find<DiscoverableRunnable>() + val classes = find<java.util.function.Function<*,*>>() val applicableFilters = filters .filter { !isJarFile(it) && !isFullClassName(it) } val filteredClasses = applicableFilters @@ -128,7 +125,7 @@ abstract class ClassCommand : CommandBase() { } if (applicableFilters.isNotEmpty() && filteredClasses.isEmpty()) { - throw Exception("Could not find any classes implementing ${SandboxedRunnable::class.java.simpleName} " + + throw Exception("Could not find any classes implementing ${java.util.function.Function::class.java.simpleName} " + "whose name matches '${applicableFilters.joinToString(" ")}'") } @@ -192,7 +189,7 @@ abstract class ClassCommand : CommandBase() { profile = profile, rules = if (ignoreRules) { emptyList() } else { Discovery.find() }, emitters = ignoreEmitters.emptyListIfTrueOtherwiseNull(), - definitionProviders = if(ignoreDefinitionProviders) { emptyList() } else { Discovery.find() }, + definitionProviders = if (ignoreDefinitionProviders) { emptyList() } else { Discovery.find() }, enableTracing = !disableTracing, analysisConfiguration = AnalysisConfiguration( whitelist = whitelist, diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/InspectionCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/InspectionCommand.kt index f6d779ceb2..32ce08ec6e 100644 --- a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/InspectionCommand.kt +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/InspectionCommand.kt @@ -1,7 +1,6 @@ package net.corda.djvm.tools.cli import net.corda.djvm.source.ClassSource -import net.corda.djvm.tools.Utilities.createCodePath import picocli.CommandLine.Command import picocli.CommandLine.Parameters import java.nio.file.Files diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/NewCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/NewCommand.kt index e2df2f4d0f..8e6302de4e 100644 --- a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/NewCommand.kt +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/NewCommand.kt @@ -1,9 +1,5 @@ package net.corda.djvm.tools.cli -import net.corda.djvm.tools.Utilities.baseName -import net.corda.djvm.tools.Utilities.createCodePath -import net.corda.djvm.tools.Utilities.getFiles -import net.corda.djvm.tools.Utilities.openOptions import picocli.CommandLine.* import java.nio.file.Files import java.nio.file.Path diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/RunCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/RunCommand.kt index 3f4fd93108..62fab057ba 100644 --- a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/RunCommand.kt +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/RunCommand.kt @@ -1,6 +1,5 @@ package net.corda.djvm.tools.cli -import net.corda.djvm.execution.SandboxedRunnable import net.corda.djvm.source.ClassSource import picocli.CommandLine.Command import picocli.CommandLine.Parameters @@ -20,7 +19,7 @@ class RunCommand : ClassCommand() { var classes: Array<String> = emptyArray() override fun processClasses(classes: List<Class<*>>) { - val interfaceName = SandboxedRunnable::class.java.simpleName + val interfaceName = java.util.function.Function::class.java.simpleName for (clazz in classes) { if (!clazz.interfaces.any { it.simpleName == interfaceName }) { printError("Class is not an instance of $interfaceName; ${clazz.name}") diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/TreeCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/TreeCommand.kt index 47b24ffa44..26b5dbc7d8 100644 --- a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/TreeCommand.kt +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/TreeCommand.kt @@ -1,6 +1,5 @@ package net.corda.djvm.tools.cli -import net.corda.djvm.tools.Utilities.workingDirectory import picocli.CommandLine.Command import java.nio.file.Files diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Utilities.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Utilities.kt new file mode 100644 index 0000000000..66d5d4d918 --- /dev/null +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Utilities.kt @@ -0,0 +1,104 @@ +@file:JvmName("Utilities") +package net.corda.djvm.tools.cli + +import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner +import java.lang.reflect.Modifier +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardOpenOption + +/** + * Get the expanded file name of each path in the provided array. + */ +fun Array<Path>?.getFiles(map: (Path) -> Path = { it }) = (this ?: emptyArray()).map { + val pathString = it.toString() + val path = map(it) + when { + '/' in pathString || '\\' in pathString -> + throw Exception("Please provide a pathless file name") + pathString.endsWith(".java", true) -> path + else -> Paths.get("$path.java") + } +} + +/** + * Get the string representation of each expanded file name in the provided array. + */ +fun Array<Path>?.getFileNames(map: (Path) -> Path = { it }) = this.getFiles(map).map { + it.toString() +}.toTypedArray() + +/** + * Execute inlined action if the collection is empty. + */ +inline fun <T> List<T>.onEmpty(action: () -> Unit): List<T> { + if (!this.any()) { + action() + } + return this +} + +/** + * Execute inlined action if the array is empty. + */ +inline fun <reified T> Array<T>?.onEmpty(action: () -> Unit): Array<T> { + return (this ?: emptyArray()).toList().onEmpty(action).toTypedArray() +} + +/** + * Derive the set of [StandardOpenOption]'s to use for a file operation. + */ +fun openOptions(force: Boolean) = if (force) { + arrayOf(StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING) +} else { + arrayOf(StandardOpenOption.CREATE_NEW) +} + +/** + * Get the path of where any generated code will be placed. Create the directory if it does not exist. + */ +fun createCodePath(): Path { + return Paths.get("tmp", "net", "corda", "djvm").let { + Files.createDirectories(it) + } +} + +/** + * Return the base name of a file (i.e., its name without extension) + */ +val Path.baseName: String + get() = this.fileName.toString() + .replaceAfterLast('.', "") + .removeSuffix(".") + +/** + * The path of the executing JAR. + */ +val jarPath: String = object {}.javaClass.protectionDomain.codeSource.location.toURI().path + + +/** + * The path of the current working directory. + */ +val workingDirectory: Path = Paths.get(System.getProperty("user.dir")) + +/** + * The class path for the current execution context. + */ +val userClassPath: String = System.getProperty("java.class.path") + +/** + * Get a reference of each concrete class that implements interface or class [T]. + */ +inline fun <reified T> find(scanSpec: String = "net/corda/djvm"): List<Class<*>> { + val references = mutableListOf<Class<*>>() + FastClasspathScanner(scanSpec) + .matchClassesImplementing(T::class.java) { clazz -> + if (!Modifier.isAbstract(clazz.modifiers) && !Modifier.isStatic(clazz.modifiers)) { + references.add(clazz) + } + } + .scan() + return references +} diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/WhitelistGenerateCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/WhitelistGenerateCommand.kt index 4882c9355f..b578dbffb3 100644 --- a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/WhitelistGenerateCommand.kt +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/WhitelistGenerateCommand.kt @@ -33,52 +33,53 @@ class WhitelistGenerateCommand : CommandBase() { override fun validateArguments() = paths.isNotEmpty() override fun handleCommand(): Boolean { - val entries = mutableListOf<String>() - val visitor = object : ClassAndMemberVisitor() { - override fun visitClass(clazz: ClassRepresentation): ClassRepresentation { - entries.add(clazz.name) - return super.visitClass(clazz) - } + val entries = AnalysisConfiguration().use { configuration -> + val entries = mutableListOf<String>() + val visitor = object : ClassAndMemberVisitor(configuration, null) { + override fun visitClass(clazz: ClassRepresentation): ClassRepresentation { + entries.add(clazz.name) + return super.visitClass(clazz) + } - override fun visitMethod(clazz: ClassRepresentation, method: Member): Member { - visitMember(clazz, method) - return super.visitMethod(clazz, method) - } + override fun visitMethod(clazz: ClassRepresentation, method: Member): Member { + visitMember(clazz, method) + return super.visitMethod(clazz, method) + } - override fun visitField(clazz: ClassRepresentation, field: Member): Member { - visitMember(clazz, field) - return super.visitField(clazz, field) - } + override fun visitField(clazz: ClassRepresentation, field: Member): Member { + visitMember(clazz, field) + return super.visitField(clazz, field) + } - private fun visitMember(clazz: ClassRepresentation, member: Member) { - entries.add("${clazz.name}.${member.memberName}:${member.signature}") + private fun visitMember(clazz: ClassRepresentation, member: Member) { + entries.add("${clazz.name}.${member.memberName}:${member.signature}") + } } + val context = AnalysisContext.fromConfiguration(configuration) + for (path in paths) { + ClassSource.fromPath(path).getStreamIterator().forEach { + visitor.analyze(it, context) + } + } + entries } - val context = AnalysisContext.fromConfiguration(AnalysisConfiguration(), emptyList()) - for (path in paths) { - ClassSource.fromPath(path).getStreamIterator().forEach { - visitor.analyze(it, context) - } - } - val output = output - if (output != null) { - Files.newOutputStream(output, StandardOpenOption.CREATE).use { - GZIPOutputStream(it).use { - PrintStream(it).use { - it.println(""" + output?.also { + Files.newOutputStream(it, StandardOpenOption.CREATE).use { out -> + GZIPOutputStream(out).use { gzip -> + PrintStream(gzip).use { pout -> + pout.println(""" |java/.* |javax/.* |jdk/.* + |com/sun/.* |sun/.* |--- """.trimMargin().trim()) - printEntries(it, entries) + printEntries(pout, entries) } } } - } else { - printEntries(System.out, entries) - } + } ?: printEntries(System.out, entries) return true } diff --git a/djvm/src/main/resources/log4j2.xml b/djvm/cli/src/main/resources/log4j2.xml similarity index 100% rename from djvm/src/main/resources/log4j2.xml rename to djvm/cli/src/main/resources/log4j2.xml diff --git a/djvm/src/main/kotlin/net/corda/djvm/SandboxRuntimeContext.kt b/djvm/src/main/kotlin/net/corda/djvm/SandboxRuntimeContext.kt index 49013b9ba3..d717c9074e 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/SandboxRuntimeContext.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/SandboxRuntimeContext.kt @@ -3,25 +3,20 @@ package net.corda.djvm import net.corda.djvm.analysis.AnalysisContext import net.corda.djvm.costing.RuntimeCostSummary import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.djvm.source.ClassSource /** * The context in which a sandboxed operation is run. * * @property configuration The configuration of the sandbox. - * @property inputClasses The classes passed in for analysis. */ -class SandboxRuntimeContext( - val configuration: SandboxConfiguration, - private val inputClasses: List<ClassSource> -) { +class SandboxRuntimeContext(val configuration: SandboxConfiguration) { /** * The class loader to use inside the sandbox. */ val classLoader: SandboxClassLoader = SandboxClassLoader( configuration, - AnalysisContext.fromConfiguration(configuration.analysisConfiguration, inputClasses) + AnalysisContext.fromConfiguration(configuration.analysisConfiguration) ) /** @@ -35,7 +30,7 @@ class SandboxRuntimeContext( fun use(action: SandboxRuntimeContext.() -> Unit) { SandboxRuntimeContext.instance = this try { - this.action() + action(this) } finally { threadLocalContext.remove() } @@ -43,9 +38,7 @@ class SandboxRuntimeContext( companion object { - private val threadLocalContext = object : ThreadLocal<SandboxRuntimeContext?>() { - override fun initialValue(): SandboxRuntimeContext? = null - } + private val threadLocalContext = ThreadLocal<SandboxRuntimeContext?>() /** * When called from within a sandbox, this returns the context for the current sandbox thread. diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt index 4114aa32af..2a1e7d63cf 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt @@ -1,9 +1,15 @@ package net.corda.djvm.analysis +import net.corda.djvm.code.ruleViolationError +import net.corda.djvm.code.thresholdViolationError import net.corda.djvm.messages.Severity import net.corda.djvm.references.ClassModule import net.corda.djvm.references.MemberModule +import net.corda.djvm.source.BootstrapClassLoader +import net.corda.djvm.source.SourceClassLoader import sandbox.net.corda.djvm.costing.RuntimeCostAccounter +import java.io.Closeable +import java.io.IOException import java.nio.file.Path /** @@ -13,7 +19,8 @@ import java.nio.file.Path * @param additionalPinnedClasses Classes that have already been declared in the sandbox namespace and that should be * made available inside the sandboxed environment. * @property minimumSeverityLevel The minimum severity level to log and report. - * @property classPath The extended class path to use for the analysis. + * @param classPath The extended class path to use for the analysis. + * @param bootstrapJar The location of a jar containing the Java APIs. * @property analyzeAnnotations Analyze annotations despite not being explicitly referenced. * @property prefixFilters Only record messages where the originating class name matches one of the provided prefixes. * If none are provided, all messages will be reported. @@ -24,32 +31,47 @@ class AnalysisConfiguration( val whitelist: Whitelist = Whitelist.MINIMAL, additionalPinnedClasses: Set<String> = emptySet(), val minimumSeverityLevel: Severity = Severity.WARNING, - val classPath: List<Path> = emptyList(), + classPath: List<Path> = emptyList(), + bootstrapJar: Path? = null, val analyzeAnnotations: Boolean = false, val prefixFilters: List<String> = emptyList(), val classModule: ClassModule = ClassModule(), val memberModule: MemberModule = MemberModule() -) { +) : Closeable { /** * Classes that have already been declared in the sandbox namespace and that should be made * available inside the sandboxed environment. */ - val pinnedClasses: Set<String> = setOf(SANDBOXED_OBJECT, RUNTIME_COST_ACCOUNTER) + additionalPinnedClasses + val pinnedClasses: Set<String> = setOf( + SANDBOXED_OBJECT, + RuntimeCostAccounter.TYPE_NAME, + ruleViolationError, + thresholdViolationError + ) + additionalPinnedClasses /** * Functionality used to resolve the qualified name and relevant information about a class. */ val classResolver: ClassResolver = ClassResolver(pinnedClasses, whitelist, SANDBOX_PREFIX) + private val bootstrapClassLoader = bootstrapJar?.let { BootstrapClassLoader(it, classResolver) } + val supportingClassLoader = SourceClassLoader(classPath, classResolver, bootstrapClassLoader) + + @Throws(IOException::class) + override fun close() { + supportingClassLoader.use { + bootstrapClassLoader?.close() + } + } + companion object { /** * The package name prefix to use for classes loaded into a sandbox. */ private const val SANDBOX_PREFIX: String = "sandbox/" - private const val SANDBOXED_OBJECT = "sandbox/java/lang/Object" - private const val RUNTIME_COST_ACCOUNTER = RuntimeCostAccounter.TYPE_NAME + private const val SANDBOXED_OBJECT = SANDBOX_PREFIX + "java/lang/Object" } } diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisContext.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisContext.kt index c37beab616..3ea9b55b1b 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisContext.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisContext.kt @@ -1,10 +1,10 @@ package net.corda.djvm.analysis +import net.corda.djvm.code.asPackagePath import net.corda.djvm.messages.MessageCollection import net.corda.djvm.references.ClassHierarchy import net.corda.djvm.references.EntityReference import net.corda.djvm.references.ReferenceMap -import net.corda.djvm.source.ClassSource /** * The context in which one or more classes are analysed. @@ -13,13 +13,11 @@ import net.corda.djvm.source.ClassSource * @property classes List of class definitions that have been analyzed. * @property references A collection of all referenced members found during analysis together with the locations from * where each member has been accessed or invoked. - * @property inputClasses The classes passed in for analysis. */ class AnalysisContext private constructor( val messages: MessageCollection, val classes: ClassHierarchy, - val references: ReferenceMap, - val inputClasses: List<ClassSource> + val references: ReferenceMap ) { private val origins = mutableMapOf<String, MutableSet<EntityReference>>() @@ -28,7 +26,7 @@ class AnalysisContext private constructor( * Record a class origin in the current analysis context. */ fun recordClassOrigin(name: String, origin: EntityReference) { - origins.getOrPut(name.normalize()) { mutableSetOf() }.add(origin) + origins.getOrPut(name.asPackagePath) { mutableSetOf() }.add(origin) } /** @@ -42,20 +40,14 @@ class AnalysisContext private constructor( /** * Create a new analysis context from provided configuration. */ - fun fromConfiguration(configuration: AnalysisConfiguration, classes: List<ClassSource>): AnalysisContext { + fun fromConfiguration(configuration: AnalysisConfiguration): AnalysisContext { return AnalysisContext( MessageCollection(configuration.minimumSeverityLevel, configuration.prefixFilters), ClassHierarchy(configuration.classModule, configuration.memberModule), - ReferenceMap(configuration.classModule), - classes + ReferenceMap(configuration.classModule) ) } - /** - * Local extension method for normalizing a class name. - */ - private fun String.normalize() = this.replace("/", ".") - } } \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt index 53a934cd80..d0d9cb4e8c 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt @@ -4,30 +4,25 @@ import net.corda.djvm.code.EmitterModule import net.corda.djvm.code.Instruction import net.corda.djvm.code.instructions.* import net.corda.djvm.messages.Message -import net.corda.djvm.references.ClassReference -import net.corda.djvm.references.ClassRepresentation -import net.corda.djvm.references.Member -import net.corda.djvm.references.MemberReference -import net.corda.djvm.source.SourceClassLoader +import net.corda.djvm.references.* import org.objectweb.asm.* import java.io.InputStream /** * Functionality for traversing a class and its members. * - * @property classVisitor Class visitor to use when traversing the structure of classes. * @property configuration The configuration to use for the analysis + * @property classVisitor Class visitor to use when traversing the structure of classes. */ open class ClassAndMemberVisitor( - private val classVisitor: ClassVisitor? = null, - private val configuration: AnalysisConfiguration = AnalysisConfiguration() + private val configuration: AnalysisConfiguration, + private val classVisitor: ClassVisitor? ) { /** * Holds a reference to the currently used analysis context. */ - protected var analysisContext: AnalysisContext = - AnalysisContext.fromConfiguration(configuration, emptyList()) + protected var analysisContext: AnalysisContext = AnalysisContext.fromConfiguration(configuration) /** * Holds a link to the class currently being traversed. @@ -44,12 +39,6 @@ open class ClassAndMemberVisitor( */ private var sourceLocation = SourceLocation() - /** - * The class loader used to find classes on the extended class path. - */ - private val supportingClassLoader = - SourceClassLoader(configuration.classPath, configuration.classResolver) - /** * Analyze class by using the provided qualified name of the class. */ @@ -63,7 +52,7 @@ open class ClassAndMemberVisitor( * @param origin The originating class for the analysis. */ fun analyze(className: String, context: AnalysisContext, origin: String? = null) { - supportingClassLoader.classReader(className, context, origin).apply { + configuration.supportingClassLoader.classReader(className, context, origin).apply { analyze(this, context) } } @@ -167,7 +156,8 @@ open class ClassAndMemberVisitor( } /** - * Run action with a guard that populates [messages] based on the output. + * Run action with a guard that populates [AnalysisRuntimeContext.messages] + * based on the output. */ private inline fun captureExceptions(action: () -> Unit): Boolean { return try { @@ -229,9 +219,7 @@ open class ClassAndMemberVisitor( ClassRepresentation(version, access, name, superClassName, interfaceNames, genericsDetails = signature ?: "").also { currentClass = it currentMember = null - sourceLocation = SourceLocation( - className = name - ) + sourceLocation = SourceLocation(className = name) } captureExceptions { currentClass = visitClass(currentClass!!) @@ -251,7 +239,7 @@ open class ClassAndMemberVisitor( override fun visitEnd() { configuration.classModule .getClassReferencesFromClass(currentClass!!, configuration.analyzeAnnotations) - .forEach { recordTypeReference(it) } + .forEach(::recordTypeReference) captureExceptions { visitClassEnd(currentClass!!) } @@ -306,14 +294,15 @@ open class ClassAndMemberVisitor( configuration.memberModule.addToClass(clazz, visitedMember ?: member) return if (processMember) { val derivedMember = visitedMember ?: member - val targetVisitor = super.visitMethod( - derivedMember.access, - derivedMember.memberName, - derivedMember.signature, - signature, - derivedMember.exceptions.toTypedArray() - ) - MethodVisitorImpl(targetVisitor) + super.visitMethod( + derivedMember.access, + derivedMember.memberName, + derivedMember.signature, + signature, + derivedMember.exceptions.toTypedArray() + )?.let { targetVisitor -> + MethodVisitorImpl(targetVisitor, derivedMember) + } } else { null } @@ -340,14 +329,15 @@ open class ClassAndMemberVisitor( configuration.memberModule.addToClass(clazz, visitedMember ?: member) return if (processMember) { val derivedMember = visitedMember ?: member - val targetVisitor = super.visitField( - derivedMember.access, - derivedMember.memberName, - derivedMember.signature, - signature, - derivedMember.value - ) - FieldVisitorImpl(targetVisitor) + super.visitField( + derivedMember.access, + derivedMember.memberName, + derivedMember.signature, + signature, + derivedMember.value + )?.let { targetVisitor -> + FieldVisitorImpl(targetVisitor) + } } else { null } @@ -359,7 +349,8 @@ open class ClassAndMemberVisitor( * Visitor used to traverse and analyze a method. */ private inner class MethodVisitorImpl( - targetVisitor: MethodVisitor? + targetVisitor: MethodVisitor, + private val method: Member ) : MethodVisitor(API_VERSION, targetVisitor) { /** @@ -387,6 +378,16 @@ open class ClassAndMemberVisitor( return super.visitAnnotation(desc, visible) } + /** + * Write any new method body code, assuming the definition providers + * have provided any. This handler will not be visited if this method + * has no existing code. + */ + override fun visitCode() { + tryReplaceMethodBody() + super.visitCode() + } + /** * Extract information about provided field access instruction. */ @@ -493,6 +494,29 @@ open class ClassAndMemberVisitor( } } + /** + * Finish visiting this method, writing any new method body byte-code + * if we haven't written it already. This would (presumably) only happen + * for methods that previously had no body, e.g. native methods. + */ + override fun visitEnd() { + tryReplaceMethodBody() + super.visitEnd() + } + + private fun tryReplaceMethodBody() { + if (method.body.isNotEmpty() && (mv != null)) { + EmitterModule(mv).apply { + for (body in method.body) { + body(this) + } + } + mv.visitMaxs(-1, -1) + mv.visitEnd() + mv = null + } + } + /** * Helper function used to streamline the access to an instruction and to catch any related processing errors. */ @@ -517,7 +541,7 @@ open class ClassAndMemberVisitor( * Visitor used to traverse and analyze a field. */ private inner class FieldVisitorImpl( - targetVisitor: FieldVisitor? + targetVisitor: FieldVisitor ) : FieldVisitor(API_VERSION, targetVisitor) { /** diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassResolver.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassResolver.kt index e2d23b3f70..b1aa3ae541 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassResolver.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassResolver.kt @@ -1,5 +1,8 @@ package net.corda.djvm.analysis +import net.corda.djvm.code.asPackagePath +import net.corda.djvm.code.asResourcePath + /** * Functionality for resolving the class name of a sandboxable class. * @@ -32,12 +35,12 @@ class ClassResolver( */ fun resolve(name: String): String { return when { - name.startsWith("[") && name.endsWith(";") -> { + name.startsWith('[') && name.endsWith(';') -> { complexArrayTypeRegex.replace(name) { "${it.groupValues[1]}L${resolveName(it.groupValues[2])};" } } - name.startsWith("[") && !name.endsWith(";") -> name + name.startsWith('[') && !name.endsWith(';') -> name else -> resolveName(name) } } @@ -46,7 +49,7 @@ class ClassResolver( * Resolve the class name from a fully qualified normalized name. */ fun resolveNormalized(name: String): String { - return resolve(name.replace('.', '/')).replace('/', '.') + return resolve(name.asResourcePath).asPackagePath } /** @@ -96,7 +99,7 @@ class ClassResolver( * Reverse the resolution of a class name from a fully qualified normalized name. */ fun reverseNormalized(name: String): String { - return reverse(name.replace('.', '/')).replace('/', '.') + return reverse(name.asResourcePath).asPackagePath } /** diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt index 95d8c2ff39..3cbbfe8223 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt @@ -117,7 +117,8 @@ open class Whitelist private constructor( "^java/lang/Throwable(\\..*)?$".toRegex(), "^java/lang/Void(\\..*)?$".toRegex(), "^java/lang/.*Error(\\..*)?$".toRegex(), - "^java/lang/.*Exception(\\..*)?$".toRegex() + "^java/lang/.*Exception(\\..*)?$".toRegex(), + "^java/lang/reflect/Array(\\..*)?$".toRegex() ) /** diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt b/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt index b8d2fa8a93..777e69f9fe 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt @@ -20,7 +20,7 @@ class ClassMutator( private val configuration: AnalysisConfiguration, private val definitionProviders: List<DefinitionProvider> = emptyList(), private val emitters: List<Emitter> = emptyList() -) : ClassAndMemberVisitor(classVisitor, configuration = configuration) { +) : ClassAndMemberVisitor(configuration, classVisitor) { /** * Tracks whether any modifications have been applied to any of the processed class(es) and pertinent members. @@ -82,7 +82,8 @@ class ClassMutator( */ override fun visitInstruction(method: Member, emitter: EmitterModule, instruction: Instruction) { val context = EmitterContext(currentAnalysisContext(), configuration, emitter) - Processor.processEntriesOfType<Emitter>(emitters, analysisContext.messages) { + // We need to apply the tracing emitters before the non-tracing ones. + Processor.processEntriesOfType<Emitter>(emitters.sortedByDescending(Emitter::isTracer), analysisContext.messages) { it.emit(context, instruction) } if (!emitter.emitDefaultInstruction || emitter.hasEmittedCustomCode) { diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt b/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt index 7d953a28e1..f904d276b7 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt @@ -20,6 +20,7 @@ interface Emitter { /** * Indication of whether or not the emitter performs instrumentation for tracing inside the sandbox. */ + @JvmDefault val isTracer: Boolean get() = false diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt b/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt index 8d0f25bd02..afe9b5165d 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt @@ -1,7 +1,9 @@ package net.corda.djvm.code +import org.objectweb.asm.Label import org.objectweb.asm.MethodVisitor -import org.objectweb.asm.Opcodes +import org.objectweb.asm.Opcodes.* +import org.objectweb.asm.Type import sandbox.net.corda.djvm.costing.RuntimeCostAccounter /** @@ -29,7 +31,7 @@ class EmitterModule( /** * Emit instruction for creating a new object of type [typeName]. */ - fun new(typeName: String, opcode: Int = Opcodes.NEW) { + fun new(typeName: String, opcode: Int = NEW) { hasEmittedCustomCode = true methodVisitor.visitTypeInsn(opcode, typeName) } @@ -38,7 +40,7 @@ class EmitterModule( * Emit instruction for creating a new object of type [T]. */ inline fun <reified T> new() { - new(T::class.java.name) + new(Type.getInternalName(T::class.java)) } /** @@ -62,7 +64,7 @@ class EmitterModule( */ fun invokeStatic(owner: String, name: String, descriptor: String, isInterface: Boolean = false) { hasEmittedCustomCode = true - methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, owner, name, descriptor, isInterface) + methodVisitor.visitMethodInsn(INVOKESTATIC, owner, name, descriptor, isInterface) } /** @@ -70,14 +72,14 @@ class EmitterModule( */ fun invokeSpecial(owner: String, name: String, descriptor: String, isInterface: Boolean = false) { hasEmittedCustomCode = true - methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, owner, name, descriptor, isInterface) + methodVisitor.visitMethodInsn(INVOKESPECIAL, owner, name, descriptor, isInterface) } /** * Emit instruction for invoking a special method on class [T], e.g. a constructor or a method on a super-type. */ inline fun <reified T> invokeSpecial(name: String, descriptor: String, isInterface: Boolean = false) { - invokeSpecial(T::class.java.name, name, descriptor, isInterface) + invokeSpecial(Type.getInternalName(T::class.java), name, descriptor, isInterface) } /** @@ -85,7 +87,7 @@ class EmitterModule( */ fun pop() { hasEmittedCustomCode = true - methodVisitor.visitInsn(Opcodes.POP) + methodVisitor.visitInsn(POP) } /** @@ -93,19 +95,40 @@ class EmitterModule( */ fun duplicate() { hasEmittedCustomCode = true - methodVisitor.visitInsn(Opcodes.DUP) + methodVisitor.visitInsn(DUP) } /** * Emit a sequence of instructions for instantiating and throwing an exception based on the provided message. */ - fun throwError(message: String) { + fun <T : Throwable> throwException(exceptionType: Class<T>, message: String) { hasEmittedCustomCode = true - new<java.lang.Exception>() - methodVisitor.visitInsn(Opcodes.DUP) + val exceptionName = Type.getInternalName(exceptionType) + new(exceptionName) + methodVisitor.visitInsn(DUP) methodVisitor.visitLdcInsn(message) - invokeSpecial<java.lang.Exception>("<init>", "(Ljava/lang/String;)V") - methodVisitor.visitInsn(Opcodes.ATHROW) + invokeSpecial(exceptionName, "<init>", "(Ljava/lang/String;)V") + methodVisitor.visitInsn(ATHROW) + } + + inline fun <reified T : Throwable> throwException(message: String) = throwException(T::class.java, message) + + /** + * Emit instruction for returning from "void" method. + */ + fun returnVoid() { + methodVisitor.visitInsn(RETURN) + hasEmittedCustomCode = true + } + + /** + * Emit instructions for a new line number. + */ + fun lineNumber(line: Int) { + val label = Label() + methodVisitor.visitLabel(label) + methodVisitor.visitLineNumber(line, label) + hasEmittedCustomCode = true } /** diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/Types.kt b/djvm/src/main/kotlin/net/corda/djvm/code/Types.kt new file mode 100644 index 0000000000..e137f196d5 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/code/Types.kt @@ -0,0 +1,15 @@ +@file:JvmName("Types") +package net.corda.djvm.code + +import org.objectweb.asm.Type +import sandbox.net.corda.djvm.costing.ThresholdViolationError +import sandbox.net.corda.djvm.rules.RuleViolationError + +val ruleViolationError: String = Type.getInternalName(RuleViolationError::class.java) +val thresholdViolationError: String = Type.getInternalName(ThresholdViolationError::class.java) + +/** + * Local extension method for normalizing a class name. + */ +val String.asPackagePath: String get() = this.replace('/', '.') +val String.asResourcePath: String get() = this.replace('.', '/') \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/costing/TypedRuntimeCost.kt b/djvm/src/main/kotlin/net/corda/djvm/costing/TypedRuntimeCost.kt index e184ebe674..b3ca604701 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/costing/TypedRuntimeCost.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/costing/TypedRuntimeCost.kt @@ -1,6 +1,7 @@ package net.corda.djvm.costing import net.corda.djvm.utilities.loggerFor +import sandbox.net.corda.djvm.costing.ThresholdViolationError /** * Cost metric to be used in a sandbox environment. The metric has a threshold and a mechanism for reporting violations. @@ -41,7 +42,7 @@ open class TypedRuntimeCost<T>( if (thresholdPredicate(newValue)) { val message = errorMessage(currentThread) logger.error("Threshold breached; {}", message) - throw ThresholdViolationException(message) + throw ThresholdViolationError(message) } } diff --git a/djvm/src/main/kotlin/net/corda/djvm/execution/DeterministicSandboxExecutor.kt b/djvm/src/main/kotlin/net/corda/djvm/execution/DeterministicSandboxExecutor.kt index 305dab489c..28e7b7226d 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/execution/DeterministicSandboxExecutor.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/execution/DeterministicSandboxExecutor.kt @@ -2,6 +2,7 @@ package net.corda.djvm.execution import net.corda.djvm.SandboxConfiguration import net.corda.djvm.source.ClassSource +import java.util.function.Function /** * The executor is responsible for spinning up a deterministic, sandboxed environment and launching the referenced code @@ -12,14 +13,14 @@ import net.corda.djvm.source.ClassSource * @param configuration The configuration of the sandbox. */ class DeterministicSandboxExecutor<TInput, TOutput>( - configuration: SandboxConfiguration = SandboxConfiguration.DEFAULT + configuration: SandboxConfiguration ) : SandboxExecutor<TInput, TOutput>(configuration) { /** - * Short-hand for running a [SandboxedRunnable] in a sandbox by its type reference. + * Short-hand for running a [Function] in a sandbox by its type reference. */ - inline fun <reified TRunnable : SandboxedRunnable<TInput, TOutput>> run(input: TInput): - ExecutionSummaryWithResult<TOutput?> { + inline fun <reified TRunnable : Function<in TInput, out TOutput>> run(input: TInput): + ExecutionSummaryWithResult<TOutput> { return run(ClassSource.fromClassName(TRunnable::class.java.name), input) } diff --git a/djvm/src/main/kotlin/net/corda/djvm/execution/DiscoverableRunnable.kt b/djvm/src/main/kotlin/net/corda/djvm/execution/DiscoverableRunnable.kt deleted file mode 100644 index 7447d10bb4..0000000000 --- a/djvm/src/main/kotlin/net/corda/djvm/execution/DiscoverableRunnable.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.corda.djvm.execution - -/** - * Functionality runnable by a sandbox executor, marked for discoverability. - */ -interface DiscoverableRunnable \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionProfile.kt b/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionProfile.kt index a9273d44b8..409455bc46 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionProfile.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionProfile.kt @@ -1,7 +1,7 @@ package net.corda.djvm.execution /** - * The execution profile of a [SandboxedRunnable] when run in a sandbox. + * The execution profile of a [java.util.function.Function] when run in a sandbox. * * @property allocationCostThreshold The threshold placed on allocations. * @property invocationCostThreshold The threshold placed on invocations. diff --git a/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionSummary.kt b/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionSummary.kt index 95ed43700f..651440731e 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionSummary.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionSummary.kt @@ -1,9 +1,9 @@ package net.corda.djvm.execution /** - * The summary of the execution of a [SandboxedRunnable] in a sandbox. This class has no representation of the outcome, - * and is typically used when there has been a pre-mature exit from the sandbox, for instance, if an exception was - * thrown. + * The summary of the execution of a [java.util.function.Function] in a sandbox. This class has no representation of the + * outcome, and is typically used when there has been a pre-mature exit from the sandbox, for instance, if an exception + * was thrown. * * @property costs The costs accumulated when running the sandboxed code. */ diff --git a/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionSummaryWithResult.kt b/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionSummaryWithResult.kt index 509c12c702..c32f7c7df0 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionSummaryWithResult.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionSummaryWithResult.kt @@ -1,7 +1,7 @@ package net.corda.djvm.execution /** - * The summary of the execution of a [SandboxedRunnable] in a sandbox. + * The summary of the execution of a [java.util.function.Function] in a sandbox. * * @property result The outcome of the sandboxed operation. * @see ExecutionSummary diff --git a/djvm/src/main/kotlin/net/corda/djvm/execution/IsolatedTask.kt b/djvm/src/main/kotlin/net/corda/djvm/execution/IsolatedTask.kt index 6d1a9d7521..7d2ae05153 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/execution/IsolatedTask.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/execution/IsolatedTask.kt @@ -2,7 +2,6 @@ package net.corda.djvm.execution import net.corda.djvm.SandboxConfiguration import net.corda.djvm.SandboxRuntimeContext -import net.corda.djvm.analysis.AnalysisContext import net.corda.djvm.messages.MessageCollection import net.corda.djvm.rewiring.SandboxClassLoader import net.corda.djvm.rewiring.SandboxClassLoadingException @@ -16,8 +15,7 @@ import kotlin.concurrent.thread */ class IsolatedTask( private val identifier: String, - private val configuration: SandboxConfiguration, - private val context: AnalysisContext + private val configuration: SandboxConfiguration ) { /** @@ -32,12 +30,12 @@ class IsolatedTask( var exception: Throwable? = null thread(name = threadName, isDaemon = true) { logger.trace("Entering isolated runtime environment...") - SandboxRuntimeContext(configuration, context.inputClasses).use { + SandboxRuntimeContext(configuration).use { output = try { action(runnable) } catch (ex: Throwable) { logger.error("Exception caught in isolated runtime environment", ex) - exception = ex + exception = (ex as? LinkageError)?.cause ?: ex null } costs = CostSummary( @@ -84,7 +82,7 @@ class IsolatedTask( ) /** - * The class loader to use for loading the [SandboxedRunnable] and any referenced code in [SandboxExecutor.run]. + * The class loader to use for loading the [java.util.function.Function] and any referenced code in [SandboxExecutor.run]. */ val classLoader: SandboxClassLoader get() = SandboxRuntimeContext.instance.classLoader diff --git a/djvm/src/main/kotlin/net/corda/djvm/execution/SandboxExecutor.kt b/djvm/src/main/kotlin/net/corda/djvm/execution/SandboxExecutor.kt index b671348370..b69585538f 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/execution/SandboxExecutor.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/execution/SandboxExecutor.kt @@ -11,7 +11,6 @@ import net.corda.djvm.rewiring.SandboxClassLoadingException import net.corda.djvm.source.ClassSource import net.corda.djvm.utilities.loggerFor import net.corda.djvm.validation.ReferenceValidationSummary -import net.corda.djvm.validation.ReferenceValidator import java.lang.reflect.InvocationTargetException /** @@ -22,7 +21,7 @@ import java.lang.reflect.InvocationTargetException * @property configuration The configuration of sandbox. */ open class SandboxExecutor<in TInput, out TOutput>( - protected val configuration: SandboxConfiguration = SandboxConfiguration.DEFAULT + protected val configuration: SandboxConfiguration ) { private val classModule = configuration.analysisConfiguration.classModule @@ -32,12 +31,7 @@ open class SandboxExecutor<in TInput, out TOutput>( private val whitelist = configuration.analysisConfiguration.whitelist /** - * Module used to validate all traversable references before instantiating and executing a [SandboxedRunnable]. - */ - private val referenceValidator = ReferenceValidator(configuration.analysisConfiguration) - - /** - * Executes a [SandboxedRunnable] implementation. + * Executes a [java.util.function.Function] implementation. * * @param runnableClass The entry point of the sandboxed code to run. * @param input The input to provide to the sandboxed environment. @@ -50,7 +44,7 @@ open class SandboxExecutor<in TInput, out TOutput>( open fun run( runnableClass: ClassSource, input: TInput - ): ExecutionSummaryWithResult<TOutput?> { + ): ExecutionSummaryWithResult<TOutput> { // 1. We first do a breath first traversal of the class hierarchy, starting from the requested class. // The branching is defined by class references from referencesFromLocation. // 2. For each class we run validation against defined rules. @@ -63,22 +57,22 @@ open class SandboxExecutor<in TInput, out TOutput>( // 6. For execution, we then load the top-level class, implementing the SandboxedRunnable interface, again and // and consequently hit the cache. Once loaded, we can execute the code on the spawned thread, i.e., in an // isolated environment. - logger.trace("Executing {} with input {}...", runnableClass, input) + logger.debug("Executing {} with input {}...", runnableClass, input) // TODO Class sources can be analyzed in parallel, although this require making the analysis context thread-safe // To do so, one could start by batching the first X classes from the class sources and analyse each one in // parallel, caching any intermediate state and subsequently process enqueued sources in parallel batches as well. // Note that this would require some rework of the [IsolatedTask] and the class loader to bypass the limitation // of caching and state preserved in thread-local contexts. val classSources = listOf(runnableClass) - val context = AnalysisContext.fromConfiguration(configuration.analysisConfiguration, classSources) - val result = IsolatedTask(runnableClass.qualifiedClassName, configuration, context).run { + val context = AnalysisContext.fromConfiguration(configuration.analysisConfiguration) + val result = IsolatedTask(runnableClass.qualifiedClassName, configuration).run { validate(context, classLoader, classSources) val loadedClass = classLoader.loadClassAndBytes(runnableClass, context) val instance = loadedClass.type.newInstance() - val method = loadedClass.type.getMethod("run", Any::class.java) + val method = loadedClass.type.getMethod("apply", Any::class.java) try { @Suppress("UNCHECKED_CAST") - method.invoke(instance, input) as? TOutput? + method.invoke(instance, input) as? TOutput } catch (ex: InvocationTargetException) { throw ex.targetException } @@ -105,8 +99,8 @@ open class SandboxExecutor<in TInput, out TOutput>( * @return A [LoadedClass] with the class' byte code, type and name. */ fun load(classSource: ClassSource): LoadedClass { - val context = AnalysisContext.fromConfiguration(configuration.analysisConfiguration, listOf(classSource)) - val result = IsolatedTask("LoadClass", configuration, context).run { + val context = AnalysisContext.fromConfiguration(configuration.analysisConfiguration) + val result = IsolatedTask("LoadClass", configuration).run { classLoader.loadClassAndBytes(classSource, context) } return result.output ?: throw ClassNotFoundException(classSource.qualifiedClassName) @@ -125,8 +119,8 @@ open class SandboxExecutor<in TInput, out TOutput>( @Throws(SandboxClassLoadingException::class) fun validate(vararg classSources: ClassSource): ReferenceValidationSummary { logger.trace("Validating {}...", classSources) - val context = AnalysisContext.fromConfiguration(configuration.analysisConfiguration, classSources.toList()) - val result = IsolatedTask("Validation", configuration, context).run { + val context = AnalysisContext.fromConfiguration(configuration.analysisConfiguration) + val result = IsolatedTask("Validation", configuration).run { validate(context, classLoader, classSources.toList()) } logger.trace("Validation of {} resulted in {}", classSources, result) @@ -172,10 +166,6 @@ open class SandboxExecutor<in TInput, out TOutput>( } failOnReportedErrorsInContext(context) - // Validate all references in class hierarchy before proceeding. - referenceValidator.validate(context, classLoader.analyzer) - failOnReportedErrorsInContext(context) - return ReferenceValidationSummary(context.classes, context.messages, context.classOrigins) } @@ -185,7 +175,7 @@ open class SandboxExecutor<in TInput, out TOutput>( private inline fun processClassQueue( vararg elements: ClassSource, action: QueueProcessor<ClassSource>.(ClassSource, String) -> Unit ) { - QueueProcessor({ it.qualifiedClassName }, *elements).process { classSource -> + QueueProcessor(ClassSource::qualifiedClassName, *elements).process { classSource -> val className = classResolver.reverse(classModule.getBinaryClassName(classSource.qualifiedClassName)) if (!whitelist.matches(className)) { action(classSource, className) diff --git a/djvm/src/main/kotlin/net/corda/djvm/execution/SandboxedRunnable.kt b/djvm/src/main/kotlin/net/corda/djvm/execution/SandboxedRunnable.kt deleted file mode 100644 index d3cbcde31b..0000000000 --- a/djvm/src/main/kotlin/net/corda/djvm/execution/SandboxedRunnable.kt +++ /dev/null @@ -1,19 +0,0 @@ -package net.corda.djvm.execution - -/** - * Functionality runnable by a sandbox executor. - */ -interface SandboxedRunnable<in TInput, out TOutput> : DiscoverableRunnable { - - /** - * The entry point of the sandboxed functionality to be run. - * - * @param input The input to pass in to the entry point. - * - * @returns The output to pass back to the caller after the sandboxed code has finished running. - * @throws Exception The function can throw an exception, in which case the exception gets passed to the caller. - */ - @Throws(Exception::class) - fun run(input: TInput): TOutput? - -} diff --git a/djvm/src/main/kotlin/net/corda/djvm/formatting/MemberFormatter.kt b/djvm/src/main/kotlin/net/corda/djvm/formatting/MemberFormatter.kt index c03f81006f..090b6acbdf 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/formatting/MemberFormatter.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/formatting/MemberFormatter.kt @@ -53,7 +53,7 @@ class MemberFormatter( * Check whether or not a signature is for a method. */ fun isMethod(abbreviatedSignature: String): Boolean { - return abbreviatedSignature.startsWith("(") + return abbreviatedSignature.startsWith('(') } /** diff --git a/djvm/src/main/kotlin/net/corda/djvm/references/ClassHierarchy.kt b/djvm/src/main/kotlin/net/corda/djvm/references/ClassHierarchy.kt index 7929a97eb7..a68c40b4b3 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/references/ClassHierarchy.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/references/ClassHierarchy.kt @@ -82,8 +82,8 @@ class ClassHierarchy( return findAncestors(get(className)).plus(get(OBJECT_NAME)) .asSequence() .filterNotNull() - .map { memberModule.getFromClass(it, memberName, signature) } - .firstOrNull { it != null } + .mapNotNull { memberModule.getFromClass(it, memberName, signature) } + .firstOrNull() .apply { logger.trace("Getting rooted member for {}.{}:{} yields {}", className, memberName, signature, this) } diff --git a/djvm/src/main/kotlin/net/corda/djvm/references/ClassModule.kt b/djvm/src/main/kotlin/net/corda/djvm/references/ClassModule.kt index f3fa1bf62b..62673d3780 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/references/ClassModule.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/references/ClassModule.kt @@ -1,5 +1,8 @@ package net.corda.djvm.references +import net.corda.djvm.code.asPackagePath +import net.corda.djvm.code.asResourcePath + /** * Class-specific functionality. */ @@ -42,14 +45,12 @@ class ClassModule : AnnotationModule() { /** * Get the binary version of a class name. */ - fun getBinaryClassName(name: String) = - normalizeClassName(name).replace('.', '/') + fun getBinaryClassName(name: String) = normalizeClassName(name).asResourcePath /** * Get the formatted version of a class name. */ - fun getFormattedClassName(name: String) = - normalizeClassName(name).replace('/', '.') + fun getFormattedClassName(name: String) = normalizeClassName(name).asPackagePath /** * Get the short name of a class. diff --git a/djvm/src/main/kotlin/net/corda/djvm/references/Member.kt b/djvm/src/main/kotlin/net/corda/djvm/references/Member.kt index 14bc723a4c..56a4b78982 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/references/Member.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/references/Member.kt @@ -1,5 +1,13 @@ package net.corda.djvm.references +import net.corda.djvm.code.EmitterModule + +/** + * Alias for a handler which will replace an entire + * method body with a block of byte-code. + */ +typealias MethodBody = (EmitterModule) -> Unit + /** * Representation of a class member. * @@ -11,6 +19,7 @@ package net.corda.djvm.references * @property annotations The names of the annotations the member is attributed. * @property exceptions The names of the exceptions that the member can throw. * @property value The default value of a field. + * @property body One or more handlers to replace the method body with new byte-code. */ data class Member( override val access: Int, @@ -20,5 +29,6 @@ data class Member( val genericsDetails: String, val annotations: MutableSet<String> = mutableSetOf(), val exceptions: MutableSet<String> = mutableSetOf(), - val value: Any? = null + val value: Any? = null, + val body: List<MethodBody> = emptyList() ) : MemberInformation, EntityWithAccessFlag diff --git a/djvm/src/main/kotlin/net/corda/djvm/references/MemberInformation.kt b/djvm/src/main/kotlin/net/corda/djvm/references/MemberInformation.kt index f890fe7c89..09ccb21836 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/references/MemberInformation.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/references/MemberInformation.kt @@ -12,5 +12,6 @@ interface MemberInformation { val className: String val memberName: String val signature: String + @JvmDefault val reference: String get() = "$className.$memberName:$signature" } diff --git a/djvm/src/main/kotlin/net/corda/djvm/references/MemberModule.kt b/djvm/src/main/kotlin/net/corda/djvm/references/MemberModule.kt index 88ad3880a7..952d787656 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/references/MemberModule.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/references/MemberModule.kt @@ -33,14 +33,14 @@ class MemberModule : AnnotationModule() { * Check if member is a field. */ fun isField(member: MemberInformation): Boolean { - return !member.signature.startsWith("(") + return !member.signature.startsWith('(') } /** * Check if member is a method. */ fun isMethod(member: MemberInformation): Boolean { - return member.signature.startsWith("(") + return member.signature.startsWith('(') } /** diff --git a/djvm/src/main/kotlin/net/corda/djvm/references/ReferenceMap.kt b/djvm/src/main/kotlin/net/corda/djvm/references/ReferenceMap.kt index a84b816775..995bd6f84f 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/references/ReferenceMap.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/references/ReferenceMap.kt @@ -16,7 +16,11 @@ class ReferenceMap( private val referencesPerLocation: MutableMap<String, MutableSet<ReferenceWithLocation>> = hashMapOf() - private var numberOfReferences = 0 + /** + * The number of references in the map. + */ + var numberOfReferences = 0 + private set /** * Add source location association to a target member. @@ -50,12 +54,6 @@ class ReferenceMap( return referencesPerLocation.getOrElse(key(className, memberName, signature)) { emptySet() } } - /** - * The number of member references in the map. - */ - val size: Int - get() = numberOfReferences - /** * Get iterator for all the references in the map. */ diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt index 9fe6d83e59..473718512a 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt @@ -27,7 +27,7 @@ open class ClassRewriter( * @param context The context in which the class is being analyzed and processed. */ fun rewrite(reader: ClassReader, context: AnalysisContext): ByteCode { - logger.trace("Rewriting class {}...", reader.className) + logger.debug("Rewriting class {}...", reader.className) val writer = SandboxClassWriter(reader, classLoader) val classRemapper = ClassRemapper(writer, remapper) val visitor = ClassMutator( diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/LoadedClass.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/LoadedClass.kt index 8db11e85d1..fdbeed7161 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rewiring/LoadedClass.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/LoadedClass.kt @@ -1,5 +1,7 @@ package net.corda.djvm.rewiring +import org.objectweb.asm.Type + /** * A class or interface running in a Java application, together with its raw byte code representation and all references * made from within the type. @@ -16,7 +18,7 @@ class LoadedClass( * The name of the loaded type. */ val name: String - get() = type.name.replace('.', '/') + get() = Type.getInternalName(type) override fun toString(): String { return "Class(type=$name, size=${byteCode.bytes.size}, isModified=${byteCode.isModified})" diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt index cb17fc4aba..5740534526 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt @@ -3,29 +3,31 @@ package net.corda.djvm.rewiring import net.corda.djvm.SandboxConfiguration import net.corda.djvm.analysis.AnalysisContext import net.corda.djvm.analysis.ClassAndMemberVisitor +import net.corda.djvm.code.asResourcePath import net.corda.djvm.references.ClassReference import net.corda.djvm.source.ClassSource -import net.corda.djvm.source.SourceClassLoader import net.corda.djvm.utilities.loggerFor import net.corda.djvm.validation.RuleValidator /** * Class loader that enables registration of rewired classes. * - * @property configuration The configuration to use for the sandbox. + * @param configuration The configuration to use for the sandbox. * @property context The context in which analysis and processing is performed. */ class SandboxClassLoader( - val configuration: SandboxConfiguration, - val context: AnalysisContext -) : ClassLoader() { + configuration: SandboxConfiguration, + private val context: AnalysisContext +) : ClassLoader(null) { + + private val analysisConfiguration = configuration.analysisConfiguration /** * The instance used to validate that any loaded class complies with the specified rules. */ private val ruleValidator: RuleValidator = RuleValidator( rules = configuration.rules, - configuration = configuration.analysisConfiguration + configuration = analysisConfiguration ) /** @@ -37,12 +39,12 @@ class SandboxClassLoader( /** * Set of classes that should be left untouched due to pinning. */ - private val pinnedClasses = configuration.analysisConfiguration.pinnedClasses + private val pinnedClasses = analysisConfiguration.pinnedClasses /** * Set of classes that should be left untouched due to whitelisting. */ - private val whitelistedClasses = configuration.analysisConfiguration.whitelist + private val whitelistedClasses = analysisConfiguration.whitelist /** * Cache of loaded classes. @@ -52,10 +54,7 @@ class SandboxClassLoader( /** * The class loader used to find classes on the extended class path. */ - private val supportingClassLoader = SourceClassLoader( - configuration.analysisConfiguration.classPath, - configuration.analysisConfiguration.classResolver - ) + private val supportingClassLoader = analysisConfiguration.supportingClassLoader /** * The re-writer to use for registered classes. @@ -83,9 +82,9 @@ class SandboxClassLoader( * @return The resulting <tt>Class</tt> object and its byte code representation. */ fun loadClassAndBytes(source: ClassSource, context: AnalysisContext): LoadedClass { - logger.trace("Loading class {}, origin={}...", source.qualifiedClassName, source.origin) - val name = configuration.analysisConfiguration.classResolver.reverseNormalized(source.qualifiedClassName) - val resolvedName = configuration.analysisConfiguration.classResolver.resolveNormalized(name) + logger.debug("Loading class {}, origin={}...", source.qualifiedClassName, source.origin) + val name = analysisConfiguration.classResolver.reverseNormalized(source.qualifiedClassName) + val resolvedName = analysisConfiguration.classResolver.resolveNormalized(name) // Check if the class has already been loaded. val loadedClass = loadedClasses[name] @@ -99,14 +98,14 @@ class SandboxClassLoader( // Analyse the class if not matching the whitelist. val readClassName = reader.className - if (!configuration.analysisConfiguration.whitelist.matches(readClassName)) { + if (!analysisConfiguration.whitelist.matches(readClassName)) { logger.trace("Class {} does not match with the whitelist", source.qualifiedClassName) logger.trace("Analyzing class {}...", source.qualifiedClassName) analyzer.analyze(reader, context) } // Check if the class should be left untouched. - val qualifiedName = name.replace('.', '/') + val qualifiedName = name.asResourcePath if (qualifiedName in pinnedClasses) { logger.trace("Class {} is marked as pinned", source.qualifiedClassName) val pinnedClasses = LoadedClass( @@ -146,7 +145,7 @@ class SandboxClassLoader( context.recordClassOrigin(name, ClassReference(source.origin)) } - logger.trace("Loaded class {}, bytes={}, isModified={}", + logger.debug("Loaded class {}, bytes={}, isModified={}", source.qualifiedClassName, byteCode.bytes.size, byteCode.isModified) return classWithByteCode diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassWriter.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassWriter.kt index 9704421cd5..e1a051d45c 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassWriter.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassWriter.kt @@ -1,9 +1,11 @@ package net.corda.djvm.rewiring +import net.corda.djvm.code.asPackagePath import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassWriter import org.objectweb.asm.ClassWriter.COMPUTE_FRAMES import org.objectweb.asm.ClassWriter.COMPUTE_MAXS +import org.objectweb.asm.Type /** * Class writer for sandbox execution, with configurable a [classLoader] to ensure correct deduction of the used class @@ -20,26 +22,28 @@ import org.objectweb.asm.ClassWriter.COMPUTE_MAXS */ open class SandboxClassWriter( classReader: ClassReader, - private val classLoader: ClassLoader, + private val cloader: ClassLoader, flags: Int = COMPUTE_FRAMES or COMPUTE_MAXS ) : ClassWriter(classReader, flags) { + override fun getClassLoader(): ClassLoader = cloader + /** * Get the common super type of [type1] and [type2]. */ override fun getCommonSuperClass(type1: String, type2: String): String { - // Need to override [getCommonSuperClass] to ensure that the correct class loader is used. + // Need to override [getCommonSuperClass] to ensure that we use ClassLoader.loadClass(). when { type1 == OBJECT_NAME -> return type1 type2 == OBJECT_NAME -> return type2 } val class1 = try { - classLoader.loadClass(type1.replace('/', '.')) + classLoader.loadClass(type1.asPackagePath) } catch (exception: Exception) { throw TypeNotPresentException(type1, exception) } val class2 = try { - classLoader.loadClass(type2.replace('/', '.')) + classLoader.loadClass(type2.asPackagePath) } catch (exception: Exception) { throw TypeNotPresentException(type2, exception) } @@ -52,7 +56,7 @@ open class SandboxClassWriter( do { clazz = clazz.superclass } while (!clazz.isAssignableFrom(class2)) - clazz.name.replace('.', '/') + Type.getInternalName(clazz) } } } diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/ClassRule.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/ClassRule.kt index 396acc30e7..d4004ee72d 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/ClassRule.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/ClassRule.kt @@ -18,7 +18,7 @@ abstract class ClassRule : Rule { */ abstract fun validate(context: RuleContext, clazz: ClassRepresentation) - override fun validate(context: RuleContext, clazz: ClassRepresentation?, member: Member?, instruction: Instruction?) { + final override fun validate(context: RuleContext, clazz: ClassRepresentation?, member: Member?, instruction: Instruction?) { // Only run validation step if applied to the class itself. if (clazz != null && member == null && instruction == null) { validate(context, clazz) diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/InstructionRule.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/InstructionRule.kt index b8f6aa5305..202f6ab435 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/InstructionRule.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/InstructionRule.kt @@ -18,7 +18,7 @@ abstract class InstructionRule : Rule { */ abstract fun validate(context: RuleContext, instruction: Instruction) - override fun validate(context: RuleContext, clazz: ClassRepresentation?, member: Member?, instruction: Instruction?) { + final override fun validate(context: RuleContext, clazz: ClassRepresentation?, member: Member?, instruction: Instruction?) { // Only run validation step if applied to the class member itself. if (clazz != null && member != null && instruction != null) { validate(context, instruction) diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/MemberRule.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/MemberRule.kt index f94c9f6a8c..a2c626851e 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/MemberRule.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/MemberRule.kt @@ -18,7 +18,7 @@ abstract class MemberRule : Rule { */ abstract fun validate(context: RuleContext, member: Member) - override fun validate(context: RuleContext, clazz: ClassRepresentation?, member: Member?, instruction: Instruction?) { + final override fun validate(context: RuleContext, clazz: ClassRepresentation?, member: Member?, instruction: Instruction?) { // Only run validation step if applied to the class member itself. if (clazz != null && member != null && instruction == null) { validate(context, member) diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowBreakpoints.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowBreakpoints.kt deleted file mode 100644 index 603e9bab21..0000000000 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowBreakpoints.kt +++ /dev/null @@ -1,17 +0,0 @@ -package net.corda.djvm.rules.implementation - -import net.corda.djvm.code.Instruction -import net.corda.djvm.code.Instruction.Companion.OP_BREAKPOINT -import net.corda.djvm.rules.InstructionRule -import net.corda.djvm.validation.RuleContext - -/** - * Rule that checks for invalid breakpoint instructions. - */ -class DisallowBreakpoints : InstructionRule() { - - override fun validate(context: RuleContext, instruction: Instruction) = context.validate { - fail("Disallowed breakpoint in method") given (instruction.operation == OP_BREAKPOINT) - } - -} diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowCatchingBlacklistedExceptions.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowCatchingBlacklistedExceptions.kt index 8d27c84c71..a5524ec12b 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowCatchingBlacklistedExceptions.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowCatchingBlacklistedExceptions.kt @@ -1,36 +1,16 @@ package net.corda.djvm.rules.implementation -import net.corda.djvm.code.Emitter -import net.corda.djvm.code.EmitterContext -import net.corda.djvm.code.Instruction +import net.corda.djvm.code.* import net.corda.djvm.code.instructions.CodeLabel import net.corda.djvm.code.instructions.TryCatchBlock -import net.corda.djvm.costing.ThresholdViolationException -import net.corda.djvm.rules.InstructionRule -import net.corda.djvm.validation.RuleContext import org.objectweb.asm.Label +import sandbox.net.corda.djvm.costing.ThresholdViolationError /** - * Rule that checks for attempted catches of [ThreadDeath], [ThresholdViolationException], [StackOverflowError], - * [OutOfMemoryError], [Error] or [Throwable]. + * Rule that checks for attempted catches of [ThreadDeath], [ThresholdViolationError], + * [StackOverflowError], [OutOfMemoryError], [Error] or [Throwable]. */ -class DisallowCatchingBlacklistedExceptions : InstructionRule(), Emitter { - - override fun validate(context: RuleContext, instruction: Instruction) = context.validate { - if (instruction is TryCatchBlock) { - val typeName = context.classModule.getFormattedClassName(instruction.typeName) - warn("Injected runtime check for catch-block for type $typeName") given - (instruction.typeName in disallowedExceptionTypes) - fail("Disallowed catch of ThreadDeath exception") given - (instruction.typeName == threadDeathException) - fail("Disallowed catch of stack overflow exception") given - (instruction.typeName == stackOverflowException) - fail("Disallowed catch of out of memory exception") given - (instruction.typeName == outOfMemoryException) - fail("Disallowed catch of threshold violation exception") given - (instruction.typeName.endsWith(ThresholdViolationException::class.java.simpleName)) - } - } +class DisallowCatchingBlacklistedExceptions : Emitter { override fun emit(context: EmitterContext, instruction: Instruction) = context.emit { if (instruction is TryCatchBlock && instruction.typeName in disallowedExceptionTypes) { @@ -46,13 +26,27 @@ class DisallowCatchingBlacklistedExceptions : InstructionRule(), Emitter { private fun isExceptionHandler(label: Label) = label in handlers companion object { - - private const val threadDeathException = "java/lang/ThreadDeath" - private const val stackOverflowException = "java/lang/StackOverflowError" - private const val outOfMemoryException = "java/lang/OutOfMemoryError" - - // Any of [ThreadDeath]'s throwable super-classes need explicit checking. private val disallowedExceptionTypes = setOf( + ruleViolationError, + thresholdViolationError, + + /** + * These errors indicate that the JVM is failing, + * so don't allow these to be caught either. + */ + "java/lang/StackOverflowError", + "java/lang/OutOfMemoryError", + + /** + * These are immediate super-classes for our explicit errors. + */ + "java/lang/VirtualMachineError", + "java/lang/ThreadDeath", + + /** + * Any of [ThreadDeath] and [VirtualMachineError]'s throwable + * super-classes also need explicit checking. + */ "java/lang/Throwable", "java/lang/Error" ) diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowFinalizerMethods.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowFinalizerMethods.kt deleted file mode 100644 index 43725aedf7..0000000000 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowFinalizerMethods.kt +++ /dev/null @@ -1,17 +0,0 @@ -package net.corda.djvm.rules.implementation - -import net.corda.djvm.references.Member -import net.corda.djvm.rules.MemberRule -import net.corda.djvm.validation.RuleContext - -/** - * Rule that checks for invalid use of finalizers. - */ -class DisallowFinalizerMethods : MemberRule() { - - override fun validate(context: RuleContext, member: Member) = context.validate { - fail("Disallowed finalizer method") given ("${member.memberName}:${member.signature}" == "finalize:()V") - // TODO Make this rule simply erase the finalize() method and continue execution. - } - -} diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowNativeMethods.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowNativeMethods.kt deleted file mode 100644 index 5b1fbfb392..0000000000 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowNativeMethods.kt +++ /dev/null @@ -1,17 +0,0 @@ -package net.corda.djvm.rules.implementation - -import net.corda.djvm.references.Member -import net.corda.djvm.rules.MemberRule -import net.corda.djvm.validation.RuleContext -import java.lang.reflect.Modifier - -/** - * Rule that checks for invalid use of native methods. - */ -class DisallowNativeMethods : MemberRule() { - - override fun validate(context: RuleContext, member: Member) = context.validate { - fail("Disallowed native method") given Modifier.isNative(member.access) - } - -} diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowNonDeterministicMethods.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowNonDeterministicMethods.kt new file mode 100644 index 0000000000..04ef9e3d5c --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowNonDeterministicMethods.kt @@ -0,0 +1,42 @@ +package net.corda.djvm.rules.implementation + +import net.corda.djvm.code.Emitter +import net.corda.djvm.code.EmitterContext +import net.corda.djvm.code.Instruction +import net.corda.djvm.code.instructions.MemberAccessInstruction +import net.corda.djvm.formatting.MemberFormatter +import org.objectweb.asm.Opcodes.* +import sandbox.net.corda.djvm.rules.RuleViolationError + +/** + * Some non-deterministic APIs belong to pinned classes and so cannot be stubbed out. + * Replace their invocations with exceptions instead. + */ +class DisallowNonDeterministicMethods : Emitter { + + override fun emit(context: EmitterContext, instruction: Instruction) = context.emit { + if (instruction is MemberAccessInstruction && isForbidden(instruction)) { + when (instruction.operation) { + INVOKEVIRTUAL -> { + throwException<RuleViolationError>("Disallowed reference to API; ${memberFormatter.format(instruction.member)}") + preventDefault() + } + } + } + } + + private fun isClassReflection(instruction: MemberAccessInstruction): Boolean = + (instruction.owner == "java/lang/Class") && ( + ((instruction.memberName == "newInstance" && instruction.signature == "()Ljava/lang/Object;") + || instruction.signature.contains("Ljava/lang/reflect/")) + ) + + private fun isObjectMonitor(instruction: MemberAccessInstruction): Boolean = + (instruction.signature == "()V" && (instruction.memberName == "notify" || instruction.memberName == "notifyAll" || instruction.memberName == "wait")) + || (instruction.memberName == "wait" && (instruction.signature == "(J)V" || instruction.signature == "(JI)V")) + + private fun isForbidden(instruction: MemberAccessInstruction): Boolean + = instruction.isMethod && (isClassReflection(instruction) || isObjectMonitor(instruction)) + + private val memberFormatter = MemberFormatter() +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowReflection.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowReflection.kt deleted file mode 100644 index cdc0b73f42..0000000000 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowReflection.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.corda.djvm.rules.implementation - -import net.corda.djvm.code.Instruction -import net.corda.djvm.code.instructions.MemberAccessInstruction -import net.corda.djvm.formatting.MemberFormatter -import net.corda.djvm.rules.InstructionRule -import net.corda.djvm.validation.RuleContext - -/** - * Rule that checks for illegal references to reflection APIs. - */ -class DisallowReflection : InstructionRule() { - - override fun validate(context: RuleContext, instruction: Instruction) = context.validate { - // TODO Enable controlled use of reflection APIs - if (instruction is MemberAccessInstruction) { - invalidReflectionUsage(instruction) given - ("java/lang/Class" in instruction.owner && instruction.memberName == "newInstance") - invalidReflectionUsage(instruction) given (instruction.owner.startsWith("java/lang/reflect/")) - invalidReflectionUsage(instruction) given (instruction.owner.startsWith("java/lang/invoke/")) - invalidReflectionUsage(instruction) given (instruction.owner.startsWith("sun/")) - } - } - - private fun RuleContext.invalidReflectionUsage(instruction: MemberAccessInstruction) = - this.fail("Disallowed reference to reflection API; ${memberFormatter.format(instruction.member)}") - - private val memberFormatter = MemberFormatter() - -} diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/IgnoreBreakpoints.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/IgnoreBreakpoints.kt new file mode 100644 index 0000000000..88c1a08e49 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/IgnoreBreakpoints.kt @@ -0,0 +1,19 @@ +package net.corda.djvm.rules.implementation + +import net.corda.djvm.code.Emitter +import net.corda.djvm.code.EmitterContext +import net.corda.djvm.code.Instruction +import net.corda.djvm.code.Instruction.Companion.OP_BREAKPOINT + +/** + * Rule that deletes invalid breakpoint instructions. + */ +class IgnoreBreakpoints : Emitter { + + override fun emit(context: EmitterContext, instruction: Instruction) = context.emit { + when (instruction.operation) { + OP_BREAKPOINT -> preventDefault() + } + } + +} diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/IgnoreSynchronizedBlocks.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/IgnoreSynchronizedBlocks.kt index 9430d94312..3b1cb80f91 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/IgnoreSynchronizedBlocks.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/IgnoreSynchronizedBlocks.kt @@ -3,20 +3,13 @@ package net.corda.djvm.rules.implementation import net.corda.djvm.code.Emitter import net.corda.djvm.code.EmitterContext import net.corda.djvm.code.Instruction -import net.corda.djvm.rules.InstructionRule -import net.corda.djvm.validation.RuleContext import org.objectweb.asm.Opcodes.* /** - * Rule that warns about the use of synchronized code blocks. This class also exposes an emitter that rewrites pertinent - * monitoring instructions to [POP]'s, as these replacements will remove the object references that [MONITORENTER] and - * [MONITOREXIT] anticipate to be on the stack. + * An emitter that rewrites monitoring instructions to [POP]s, as these replacements will remove + * the object references that [MONITORENTER] and [MONITOREXIT] anticipate to be on the stack. */ -class IgnoreSynchronizedBlocks : InstructionRule(), Emitter { - - override fun validate(context: RuleContext, instruction: Instruction) = context.validate { - inform("Stripped monitoring instruction") given (instruction.operation in setOf(MONITORENTER, MONITOREXIT)) - } +class IgnoreSynchronizedBlocks : Emitter { override fun emit(context: EmitterContext, instruction: Instruction) = context.emit { when (instruction.operation) { diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutFinalizerMethods.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutFinalizerMethods.kt new file mode 100644 index 0000000000..48c714b843 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutFinalizerMethods.kt @@ -0,0 +1,35 @@ +package net.corda.djvm.rules.implementation + +import net.corda.djvm.analysis.AnalysisRuntimeContext +import net.corda.djvm.code.EmitterModule +import net.corda.djvm.code.MemberDefinitionProvider +import net.corda.djvm.references.Member +import java.lang.reflect.Modifier + +/** + * Rule that replaces a finalize() method with a simple stub. + */ +class StubOutFinalizerMethods : MemberDefinitionProvider { + + override fun define(context: AnalysisRuntimeContext, member: Member) = when { + /** + * Discard any other method body and replace with stub that just returns. + * Other [MemberDefinitionProvider]s are expected to append to this list + * and not replace its contents! + */ + isFinalizer(member) -> member.copy(body = listOf(::writeMethodBody)) + else -> member + } + + private fun writeMethodBody(emitter: EmitterModule): Unit = with(emitter) { + returnVoid() + } + + /** + * No need to rewrite [Object.finalize] or [Enum.finalize]; ignore these. + */ + private fun isFinalizer(member: Member): Boolean + = member.memberName == "finalize" && member.signature == "()V" + && !member.className.startsWith("java/lang/") + && !Modifier.isAbstract(member.access) +} diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutNativeMethods.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutNativeMethods.kt new file mode 100644 index 0000000000..74a58f6c7f --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutNativeMethods.kt @@ -0,0 +1,36 @@ +package net.corda.djvm.rules.implementation + +import net.corda.djvm.analysis.AnalysisRuntimeContext +import net.corda.djvm.code.EmitterModule +import net.corda.djvm.code.MemberDefinitionProvider +import net.corda.djvm.references.Member +import org.objectweb.asm.Opcodes.* +import sandbox.net.corda.djvm.rules.RuleViolationError +import java.lang.reflect.Modifier + +/** + * Rule that replaces a native method with a stub that throws an exception. + */ +class StubOutNativeMethods : MemberDefinitionProvider { + + override fun define(context: AnalysisRuntimeContext, member: Member) = when { + isNative(member) -> member.copy( + access = member.access and ACC_NATIVE.inv(), + body = member.body + if (isForStubbing(member)) ::writeStubMethodBody else ::writeExceptionMethodBody + ) + else -> member + } + + private fun writeExceptionMethodBody(emitter: EmitterModule): Unit = with(emitter) { + lineNumber(0) + throwException(RuleViolationError::class.java, "Native method has been deleted") + } + + private fun writeStubMethodBody(emitter: EmitterModule): Unit = with(emitter) { + returnVoid() + } + + private fun isForStubbing(member: Member): Boolean = member.signature == "()V" && member.memberName == "registerNatives" + + private fun isNative(member: Member): Boolean = Modifier.isNative(member.access) +} diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutReflectionMethods.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutReflectionMethods.kt new file mode 100644 index 0000000000..4e486bf289 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutReflectionMethods.kt @@ -0,0 +1,35 @@ +package net.corda.djvm.rules.implementation + +import net.corda.djvm.analysis.AnalysisRuntimeContext +import net.corda.djvm.code.EmitterModule +import net.corda.djvm.code.MemberDefinitionProvider +import net.corda.djvm.references.Member +import org.objectweb.asm.Opcodes.* +import sandbox.net.corda.djvm.rules.RuleViolationError + +/** + * Replace reflection APIs with stubs that throw exceptions. Only for unpinned classes. + */ +class StubOutReflectionMethods : MemberDefinitionProvider { + + override fun define(context: AnalysisRuntimeContext, member: Member): Member = when { + isConcreteApi(member) && isReflection(member) -> member.copy(body = member.body + ::writeMethodBody) + else -> member + } + + private fun writeMethodBody(emitter: EmitterModule): Unit = with(emitter) { + lineNumber(0) + throwException(RuleViolationError::class.java, "Disallowed reference to reflection API") + } + + // The method must be public and with a Java implementation. + private fun isConcreteApi(member: Member): Boolean = member.access and (ACC_PUBLIC or ACC_ABSTRACT or ACC_NATIVE) == ACC_PUBLIC + + private fun isReflection(member: Member): Boolean { + return member.className.startsWith("java/lang/reflect/") + || member.className.startsWith("java/lang/invoke/") + || member.className.startsWith("sun/reflect/") + || member.className == "sun/misc/Unsafe" + || member.className == "sun/misc/VM" + } +} diff --git a/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt b/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt index 47d0544bcd..8b4789f8df 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt @@ -3,6 +3,7 @@ package net.corda.djvm.source import net.corda.djvm.analysis.AnalysisContext import net.corda.djvm.analysis.ClassResolver import net.corda.djvm.analysis.SourceLocation +import net.corda.djvm.code.asResourcePath import net.corda.djvm.messages.Message import net.corda.djvm.messages.Severity import net.corda.djvm.rewiring.SandboxClassLoadingException @@ -17,18 +18,11 @@ import java.nio.file.Path import java.nio.file.Paths import kotlin.streams.toList -/** - * Customizable class loader that allows the user to explicitly specify additional JARs and directories to scan. - * - * @param paths The directories and explicit JAR files to scan. - * @property classResolver The resolver to use to derive the original name of a requested class. - * @property resolvedUrls The resolved URLs that get passed to the underlying class loader. - */ -open class SourceClassLoader( - paths: List<Path>, - private val classResolver: ClassResolver, - val resolvedUrls: Array<URL> = resolvePaths(paths) -) : URLClassLoader(resolvedUrls, SourceClassLoader::class.java.classLoader) { +abstract class AbstractSourceClassLoader( + paths: List<Path>, + private val classResolver: ClassResolver, + parent: ClassLoader? +) : URLClassLoader(resolvePaths(paths), parent) { /** * Open a [ClassReader] for the provided class name. @@ -36,7 +30,7 @@ open class SourceClassLoader( fun classReader( className: String, context: AnalysisContext, origin: String? = null ): ClassReader { - val originalName = classResolver.reverse(className.replace('.', '/')) + val originalName = classResolver.reverse(className.asResourcePath) return try { logger.trace("Opening ClassReader for class {}...", originalName) getResourceAsStream("$originalName.class").use { @@ -71,16 +65,16 @@ open class SourceClassLoader( return super.loadClass(originalName, resolve) } - private companion object { - - private val logger = loggerFor<SourceClassLoader>() + protected companion object { + @JvmStatic + protected val logger = loggerFor<SourceClassLoader>() private fun resolvePaths(paths: List<Path>): Array<URL> { return paths.map(this::expandPath).flatMap { when { !Files.exists(it) -> throw FileNotFoundException("File not found; $it") Files.isDirectory(it) -> { - listOf(it.toURL()) + Files.list(it).filter { isJarFile(it) }.map { it.toURL() }.toList() + listOf(it.toURL()) + Files.list(it).filter(::isJarFile).map { jar -> jar.toURL() }.toList() } Files.isReadable(it) && isJarFile(it) -> listOf(it.toURL()) else -> throw IllegalArgumentException("Expected JAR or class file, but found $it") @@ -100,11 +94,76 @@ open class SourceClassLoader( private fun isJarFile(path: Path) = path.toString().endsWith(".jar", true) - private fun Path.toURL() = this.toUri().toURL() + private fun Path.toURL(): URL = this.toUri().toURL() private val homeDirectory: Path get() = Paths.get(System.getProperty("user.home")) } +} + +/** + * Class loader to manage an optional JAR of replacement Java APIs. + * @param bootstrapJar The location of the JAR containing the Java APIs. + * @param classResolver The resolver to use to derive the original name of a requested class. + */ +class BootstrapClassLoader( + bootstrapJar: Path, + classResolver: ClassResolver +) : AbstractSourceClassLoader(listOf(bootstrapJar), classResolver, null) { + + /** + * Only search our own jars for the given resource. + */ + override fun getResource(name: String): URL? = findResource(name) +} + +/** + * Customizable class loader that allows the user to explicitly specify additional JARs and directories to scan. + * + * @param paths The directories and explicit JAR files to scan. + * @property classResolver The resolver to use to derive the original name of a requested class. + * @property bootstrap The [BootstrapClassLoader] containing the Java APIs for the sandbox. + */ +class SourceClassLoader( + paths: List<Path>, + classResolver: ClassResolver, + private val bootstrap: BootstrapClassLoader? = null +) : AbstractSourceClassLoader(paths, classResolver, SourceClassLoader::class.java.classLoader) { + + /** + * First check the bootstrap classloader, if we have one. + * Otherwise check our parent classloader, followed by + * the user-supplied jars. + */ + override fun getResource(name: String): URL? { + if (bootstrap != null) { + val resource = bootstrap.findResource(name) + if (resource != null) { + return resource + } else if (isJvmInternal(name)) { + logger.error("Denying request for actual {}", name) + return null + } + } + + return parent?.getResource(name) ?: findResource(name) + } + + /** + * Deny all requests for DJVM classes from any user-supplied jars. + */ + override fun findResource(name: String): URL? { + return if (name.startsWith("net/corda/djvm/")) null else super.findResource(name) + } + + /** + * Does [name] exist within any of the packages reserved for Java itself? + */ + private fun isJvmInternal(name: String): Boolean = name.startsWith("java/") + || name.startsWith("javax/") + || name.startsWith("com/sun/") + || name.startsWith("sun/") + || name.startsWith("jdk/") } \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/tools/Utilities.kt b/djvm/src/main/kotlin/net/corda/djvm/tools/Utilities.kt deleted file mode 100644 index f66b14d1cd..0000000000 --- a/djvm/src/main/kotlin/net/corda/djvm/tools/Utilities.kt +++ /dev/null @@ -1,114 +0,0 @@ -package net.corda.djvm.tools - -import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner -import java.lang.reflect.Modifier -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.Paths -import java.nio.file.StandardOpenOption - -/** - * Various utility functions. - */ -@Suppress("unused") -object Utilities { - - /** - * Get the expanded file name of each path in the provided array. - */ - fun Array<Path>?.getFiles(map: (Path) -> Path = { it }) = (this ?: emptyArray()).map { - val pathString = it.toString() - val path = map(it) - when { - '/' in pathString || '\\' in pathString -> - throw Exception("Please provide a pathless file name") - pathString.endsWith(".java", true) -> path - else -> Paths.get("$path.java") - } - } - - /** - * Get the string representation of each expanded file name in the provided array. - */ - fun Array<Path>?.getFileNames(map: (Path) -> Path = { it }) = this.getFiles(map).map { - it.toString() - }.toTypedArray() - - /** - * Execute inlined action if the collection is empty. - */ - inline fun <T> List<T>.onEmpty(action: () -> Unit): List<T> { - if (!this.any()) { - action() - } - return this - } - - /** - * Execute inlined action if the array is empty. - */ - inline fun <reified T> Array<T>?.onEmpty(action: () -> Unit): Array<T> { - return (this ?: emptyArray()).toList().onEmpty(action).toTypedArray() - } - - /** - * Derive the set of [StandardOpenOption]'s to use for a file operation. - */ - fun openOptions(force: Boolean) = if (force) { - arrayOf(StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING) - } else { - arrayOf(StandardOpenOption.CREATE_NEW) - } - - /** - * Get the path of where any generated code will be placed. Create the directory if it does not exist. - */ - fun createCodePath(): Path { - val root = Paths.get("tmp") - .resolve("net") - .resolve("corda") - .resolve("djvm") - Files.createDirectories(root) - return root - } - - /** - * Return the base name of a file (i.e., its name without extension) - */ - val Path.baseName: String - get() = this.fileName.toString() - .replaceAfterLast('.', "") - .removeSuffix(".") - - /** - * The path of the executing JAR. - */ - val jarPath: String = Utilities::class.java.protectionDomain.codeSource.location.toURI().path - - - /** - * The path of the current working directory. - */ - val workingDirectory: Path = Paths.get(System.getProperty("user.dir")) - - /** - * The class path for the current execution context. - */ - val userClassPath: String = System.getProperty("java.class.path") - - /** - * Get a reference of each concrete class that implements interface or class [T]. - */ - inline fun <reified T> find(scanSpec: String = "net/corda/djvm"): List<Class<*>> { - val references = mutableListOf<Class<*>>() - FastClasspathScanner(scanSpec) - .matchClassesImplementing(T::class.java) { clazz -> - if (!Modifier.isAbstract(clazz.modifiers) && !Modifier.isStatic(clazz.modifiers)) { - references.add(clazz) - } - } - .scan() - return references - } - -} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/utilities/Discovery.kt b/djvm/src/main/kotlin/net/corda/djvm/utilities/Discovery.kt index 8deadf7d0b..9092e5c044 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/utilities/Discovery.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/utilities/Discovery.kt @@ -7,6 +7,7 @@ import java.lang.reflect.Modifier * Find and instantiate types that implement a certain interface. */ object Discovery { + const val FORBIDDEN_CLASS_MASK = (Modifier.STATIC or Modifier.ABSTRACT) /** * Get an instance of each concrete class that implements interface or class [T]. @@ -15,7 +16,7 @@ object Discovery { val instances = mutableListOf<T>() FastClasspathScanner("net/corda/djvm") .matchClassesImplementing(T::class.java) { clazz -> - if (!Modifier.isAbstract(clazz.modifiers) && !Modifier.isStatic(clazz.modifiers)) { + if (clazz.modifiers and FORBIDDEN_CLASS_MASK == 0) { try { instances.add(clazz.newInstance()) } catch (exception: Throwable) { diff --git a/djvm/src/main/kotlin/net/corda/djvm/validation/ReferenceValidator.kt b/djvm/src/main/kotlin/net/corda/djvm/validation/ReferenceValidator.kt deleted file mode 100644 index 89fa22c992..0000000000 --- a/djvm/src/main/kotlin/net/corda/djvm/validation/ReferenceValidator.kt +++ /dev/null @@ -1,219 +0,0 @@ -package net.corda.djvm.validation - -import net.corda.djvm.analysis.AnalysisConfiguration -import net.corda.djvm.analysis.AnalysisContext -import net.corda.djvm.analysis.ClassAndMemberVisitor -import net.corda.djvm.execution.SandboxedRunnable -import net.corda.djvm.formatting.MemberFormatter -import net.corda.djvm.messages.Message -import net.corda.djvm.messages.Severity -import net.corda.djvm.references.* -import net.corda.djvm.rewiring.SandboxClassLoadingException -import net.corda.djvm.utilities.loggerFor - -/** - * Module used to validate all traversable references before instantiating and executing a [SandboxedRunnable]. - * - * @param configuration The analysis configuration to use for the validation. - * @property memberFormatter Module with functionality for formatting class members. - */ -class ReferenceValidator( - private val configuration: AnalysisConfiguration, - private val memberFormatter: MemberFormatter = MemberFormatter() -) { - - /** - * Container holding the current state of the validation. - * - * @property context The context in which references are to be validated. - * @property analyzer Underlying analyzer used for processing classes. - */ - private class State( - val context: AnalysisContext, - val analyzer: ClassAndMemberVisitor - ) - - /** - * Validate whether or not the classes in a class hierarchy can be safely instantiated and run in a sandbox by - * checking that all references are rooted in deterministic code. - * - * @param context The context in which the check should be made. - * @param analyzer Underlying analyzer used for processing classes. - */ - fun validate(context: AnalysisContext, analyzer: ClassAndMemberVisitor): ReferenceValidationSummary = - State(context, analyzer).let { state -> - logger.trace("Validating {} references across {} class(es)...", - context.references.size, context.classes.size) - context.references.process { validateReference(state, it) } - logger.trace("Reference validation completed; {} class(es) and {} message(s)", - context.references.size, context.classes.size) - ReferenceValidationSummary(state.context.classes, state.context.messages, state.context.classOrigins) - } - - /** - * Construct a message from an invalid reference and its source location. - */ - private fun referenceToMessage(referenceWithLocation: ReferenceWithLocation): Message { - val (location, reference, description) = referenceWithLocation - val referenceMessage = when { - reference is ClassReference -> - "Invalid reference to class ${configuration.classModule.getFormattedClassName(reference.className)}" - reference is MemberReference && configuration.memberModule.isConstructor(reference) -> - "Invalid reference to constructor ${memberFormatter.format(reference)}" - reference is MemberReference && configuration.memberModule.isField(reference) -> - "Invalid reference to field ${memberFormatter.format(reference)}" - reference is MemberReference && configuration.memberModule.isMethod(reference) -> - "Invalid reference to method ${memberFormatter.format(reference)}" - else -> - "Invalid reference to $reference" - } - val message = if (description.isNotBlank()) { - "$referenceMessage, $description" - } else { - referenceMessage - } - return Message(message, Severity.ERROR, location) - } - - /** - * Validate a reference made from a class or class member. - */ - private fun validateReference(state: State, reference: EntityReference) { - if (configuration.whitelist.matches(reference.className)) { - // The referenced class has been whitelisted - no need to go any further. - return - } - when (reference) { - is ClassReference -> { - logger.trace("Validating class reference {}", reference) - val clazz = getClass(state, reference.className) - val reason = when (clazz) { - null -> Reason(Reason.Code.NON_EXISTENT_CLASS) - else -> getReasonFromEntity(clazz) - } - if (reason != null) { - logger.trace("Recorded invalid class reference to {}; reason = {}", reference, reason) - state.context.messages.addAll(state.context.references.locationsFromReference(reference).map { - referenceToMessage(ReferenceWithLocation(it, reference, reason.description)) - }) - } - } - is MemberReference -> { - logger.trace("Validating member reference {}", reference) - // Ensure that the dependent class is loaded and analyzed - val clazz = getClass(state, reference.className) - val member = state.context.classes.getMember( - reference.className, reference.memberName, reference.signature - ) - val reason = when { - clazz == null -> Reason(Reason.Code.NON_EXISTENT_CLASS) - member == null -> Reason(Reason.Code.NON_EXISTENT_MEMBER) - else -> getReasonFromEntity(state, member) - } - if (reason != null) { - logger.trace("Recorded invalid member reference to {}; reason = {}", reference, reason) - state.context.messages.addAll(state.context.references.locationsFromReference(reference).map { - referenceToMessage(ReferenceWithLocation(it, reference, reason.description)) - }) - } - } - } - } - - /** - * Get a class from the class hierarchy by its binary name. - */ - private fun getClass(state: State, className: String, originClass: String? = null): ClassRepresentation? { - val name = if (configuration.classModule.isArray(className)) { - val arrayType = arrayTypeExtractor.find(className)?.groupValues?.get(1) - when (arrayType) { - null -> "java/lang/Object" - else -> arrayType - } - } else { - className - } - var clazz = state.context.classes[name] - if (clazz == null) { - logger.trace("Loading and analyzing referenced class {}...", name) - val origin = state.context.references - .locationsFromReference(ClassReference(name)) - .map { it.className } - .firstOrNull() ?: originClass - state.analyzer.analyze(name, state.context, origin) - clazz = state.context.classes[name] - } - if (clazz == null) { - logger.warn("Failed to load class {}", name) - state.context.messages.add(Message("Referenced class not found; $name", Severity.ERROR)) - } - clazz?.apply { - val ancestors = listOf(superClass) + interfaces - for (ancestor in ancestors.filter(String::isNotBlank)) { - getClass(state, ancestor, clazz.name) - } - } - return clazz - } - - /** - * Check if a top-level class definition is considered safe or not. - */ - private fun isNonDeterministic(state: State, className: String): Boolean = when { - configuration.whitelist.matches(className) -> false - else -> { - try { - getClass(state, className)?.let { - isNonDeterministic(it) - } ?: true - } catch (exception: SandboxClassLoadingException) { - true // Failed to load the class, which means the class is non-deterministic. - } - } - } - - /** - * Check if a top-level class definition is considered safe or not. - */ - private fun isNonDeterministic(clazz: ClassRepresentation) = - getReasonFromEntity(clazz) != null - - /** - * Derive what reason to give to the end-user for an invalid class. - */ - private fun getReasonFromEntity(clazz: ClassRepresentation): Reason? = when { - configuration.whitelist.matches(clazz.name) -> null - configuration.whitelist.inNamespace(clazz.name) -> Reason(Reason.Code.NOT_WHITELISTED) - configuration.classModule.isNonDeterministic(clazz) -> Reason(Reason.Code.ANNOTATED) - else -> null - } - - /** - * Derive what reason to give to the end-user for an invalid member. - */ - private fun getReasonFromEntity(state: State, member: Member): Reason? = when { - configuration.whitelist.matches(member.reference) -> null - configuration.whitelist.inNamespace(member.reference) -> Reason(Reason.Code.NOT_WHITELISTED) - configuration.memberModule.isNonDeterministic(member) -> Reason(Reason.Code.ANNOTATED) - else -> { - val invalidClasses = configuration.memberModule.findReferencedClasses(member) - .filter { isNonDeterministic(state, it) } - if (invalidClasses.isNotEmpty()) { - Reason(Reason.Code.INVALID_CLASS, invalidClasses.map { - configuration.classModule.getFormattedClassName(it) - }) - } else { - null - } - } - } - - private companion object { - - private val logger = loggerFor<ReferenceValidator>() - - private val arrayTypeExtractor = "^\\[*L([^;]+);$".toRegex() - - } - -} diff --git a/djvm/src/main/kotlin/net/corda/djvm/validation/RuleValidator.kt b/djvm/src/main/kotlin/net/corda/djvm/validation/RuleValidator.kt index 0580566bb6..1f4ead8cd1 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/validation/RuleValidator.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/validation/RuleValidator.kt @@ -21,9 +21,9 @@ import org.objectweb.asm.ClassVisitor */ class RuleValidator( private val rules: List<Rule> = emptyList(), - configuration: AnalysisConfiguration = AnalysisConfiguration(), + configuration: AnalysisConfiguration, classVisitor: ClassVisitor? = null -) : ClassAndMemberVisitor(classVisitor, configuration = configuration) { +) : ClassAndMemberVisitor(configuration, classVisitor) { /** * Apply the set of rules to the traversed class and record any violations. diff --git a/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/RuntimeCostAccounter.kt b/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/RuntimeCostAccounter.kt index 94487b1c91..3445e31ea0 100644 --- a/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/RuntimeCostAccounter.kt +++ b/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/RuntimeCostAccounter.kt @@ -2,7 +2,7 @@ package sandbox.net.corda.djvm.costing import net.corda.djvm.SandboxRuntimeContext import net.corda.djvm.costing.RuntimeCostSummary -import net.corda.djvm.costing.ThresholdViolationException +import org.objectweb.asm.Type /** * Class for keeping a tally on various runtime metrics, like number of jumps, allocations, invocations, etc. The @@ -24,7 +24,8 @@ object RuntimeCostAccounter { /** * The type name of the [RuntimeCostAccounter] class; referenced from instrumentors. */ - const val TYPE_NAME: String = "sandbox/net/corda/djvm/costing/RuntimeCostAccounter" + @JvmField + val TYPE_NAME: String = Type.getInternalName(this::class.java) /** * Known / estimated allocation costs. @@ -35,14 +36,12 @@ object RuntimeCostAccounter { ) /** - * Re-throw exception if it is of type [ThreadDeath] or [ThresholdViolationException]. + * Re-throw exception if it is of type [ThreadDeath] or [VirtualMachineError]. */ @JvmStatic fun checkCatch(exception: Throwable) { - if (exception is ThreadDeath) { - throw exception - } else if (exception is ThresholdViolationException) { - throw exception + when (exception) { + is ThreadDeath, is VirtualMachineError -> throw exception } } diff --git a/djvm/src/main/kotlin/net/corda/djvm/costing/ThresholdViolationException.kt b/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/ThresholdViolationError.kt similarity index 64% rename from djvm/src/main/kotlin/net/corda/djvm/costing/ThresholdViolationException.kt rename to djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/ThresholdViolationError.kt index 4e8635ba8d..0fe4283caf 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/costing/ThresholdViolationException.kt +++ b/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/ThresholdViolationError.kt @@ -1,4 +1,4 @@ -package net.corda.djvm.costing +package sandbox.net.corda.djvm.costing /** * Exception thrown when a sandbox threshold is violated. This will kill the current thread and consequently exit the @@ -6,6 +6,4 @@ package net.corda.djvm.costing * * @property message The description of the condition causing the problem. */ -class ThresholdViolationException( - override val message: String -) : ThreadDeath() +class ThresholdViolationError(override val message: String) : ThreadDeath() diff --git a/djvm/src/main/kotlin/sandbox/net/corda/djvm/rules/RuleViolationError.kt b/djvm/src/main/kotlin/sandbox/net/corda/djvm/rules/RuleViolationError.kt new file mode 100644 index 0000000000..24b0e73775 --- /dev/null +++ b/djvm/src/main/kotlin/sandbox/net/corda/djvm/rules/RuleViolationError.kt @@ -0,0 +1,10 @@ +package sandbox.net.corda.djvm.rules + +/** + * Exception thrown when a sandbox rule is violated at runtime. + * This will kill the current thread and consequently exit the + * sandbox. + * + * @property message The description of the condition causing the problem. + */ +class RuleViolationError(override val message: String) : ThreadDeath() \ No newline at end of file diff --git a/djvm/src/test/kotlin/foo/bar/sandbox/StrictFloat.kt b/djvm/src/test/kotlin/foo/bar/sandbox/StrictFloat.kt index 0d30565a98..82f721ab9a 100644 --- a/djvm/src/test/kotlin/foo/bar/sandbox/StrictFloat.kt +++ b/djvm/src/test/kotlin/foo/bar/sandbox/StrictFloat.kt @@ -4,7 +4,7 @@ class StrictFloat : Callable { override fun call() { val d = java.lang.Double.MIN_VALUE val x = d / 2 * 2 - assert(x.toString() == "0.0") + require(x.toString() == "0.0") } } diff --git a/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt b/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt index 333516c06a..b54d92b16e 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt @@ -16,11 +16,18 @@ import net.corda.djvm.rules.Rule import net.corda.djvm.source.ClassSource import net.corda.djvm.utilities.Discovery import net.corda.djvm.validation.RuleValidator +import org.junit.After +import org.junit.Assert.assertEquals import org.objectweb.asm.ClassReader -import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Type import java.lang.reflect.InvocationTargetException +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.concurrent.thread +import kotlin.reflect.jvm.jvmName -open class TestBase { +abstract class TestBase { companion object { @@ -32,27 +39,33 @@ open class TestBase { val BLANK = emptySet<Any>() - val DEFAULT = (ALL_RULES + ALL_EMITTERS + ALL_DEFINITION_PROVIDERS) - .toSet().distinctBy { it.javaClass } + val DEFAULT = (ALL_RULES + ALL_EMITTERS + ALL_DEFINITION_PROVIDERS).distinctBy(Any::javaClass) + + val DETERMINISTIC_RT: Path = Paths.get( + System.getProperty("deterministic-rt.path") ?: throw AssertionError("deterministic-rt.path property not set")) /** * Get the full name of type [T]. */ - inline fun <reified T> nameOf(prefix: String = "") = - "$prefix${T::class.java.name.replace('.', '/')}" + inline fun <reified T> nameOf(prefix: String = "") = "$prefix${Type.getInternalName(T::class.java)}" } /** * Default analysis configuration. */ - val configuration = AnalysisConfiguration(Whitelist.MINIMAL) + val configuration = AnalysisConfiguration(Whitelist.MINIMAL, bootstrapJar = DETERMINISTIC_RT) /** * Default analysis context */ val context: AnalysisContext - get() = AnalysisContext.fromConfiguration(configuration, emptyList()) + get() = AnalysisContext.fromConfiguration(configuration) + + @After + fun destroy() { + configuration.close() + } /** * Short-hand for analysing and validating a class. @@ -62,14 +75,15 @@ open class TestBase { noinline block: (RuleValidator.(AnalysisContext) -> Unit) ) { val reader = ClassReader(T::class.java.name) - val configuration = AnalysisConfiguration(minimumSeverityLevel = minimumSeverityLevel) - val validator = RuleValidator(ALL_RULES, configuration) - val context = AnalysisContext.fromConfiguration( - configuration, - listOf(ClassSource.fromClassName(reader.className)) - ) - validator.analyze(reader, context) - block(validator, context) + AnalysisConfiguration( + minimumSeverityLevel = minimumSeverityLevel, + classPath = listOf(DETERMINISTIC_RT) + ).use { analysisConfiguration -> + val validator = RuleValidator(ALL_RULES, analysisConfiguration) + val context = AnalysisContext.fromConfiguration(analysisConfiguration) + validator.analyze(reader, context) + block(validator, context) + } } /** @@ -113,27 +127,26 @@ open class TestBase { } } var thrownException: Throwable? = null - Thread { + thread { try { - val pinnedTestClasses = pinnedClasses.map { it.name.replace('.', '/') }.toSet() - val analysisConfiguration = AnalysisConfiguration( - whitelist = whitelist, - additionalPinnedClasses = pinnedTestClasses, - minimumSeverityLevel = minimumSeverityLevel - ) - SandboxRuntimeContext(SandboxConfiguration.of( - executionProfile, rules, emitters, definitionProviders, enableTracing, analysisConfiguration - ), classSources).use { - assertThat(runtimeCosts).areZero() - action(this) + val pinnedTestClasses = pinnedClasses.map(Type::getInternalName).toSet() + AnalysisConfiguration( + whitelist = whitelist, + bootstrapJar = DETERMINISTIC_RT, + additionalPinnedClasses = pinnedTestClasses, + minimumSeverityLevel = minimumSeverityLevel + ).use { analysisConfiguration -> + SandboxRuntimeContext(SandboxConfiguration.of( + executionProfile, rules, emitters, definitionProviders, enableTracing, analysisConfiguration + )).use { + assertThat(runtimeCosts).areZero() + action(this) + } } } catch (exception: Throwable) { thrownException = exception } - }.apply { - start() - join() - } + }.join() throw thrownException ?: return } @@ -145,8 +158,12 @@ open class TestBase { /** * Create a new instance of a class using the sandbox class loader. */ - inline fun <reified T : Callable> SandboxRuntimeContext.newCallable() = - classLoader.loadClassAndBytes(ClassSource.fromClassName(T::class.java.name), context) + inline fun <reified T : Callable> SandboxRuntimeContext.newCallable(): LoadedClass = loadClass<T>() + + inline fun <reified T : Any> SandboxRuntimeContext.loadClass(): LoadedClass = loadClass(T::class.jvmName) + + fun SandboxRuntimeContext.loadClass(className: String): LoadedClass = + classLoader.loadClassAndBytes(ClassSource.fromClassName(className), context) /** * Run the entry-point of the loaded [Callable] class. @@ -164,6 +181,10 @@ open class TestBase { /** * Stub visitor. */ - protected class Visitor : ClassVisitor(ClassAndMemberVisitor.API_VERSION) + protected class Writer : ClassWriter(COMPUTE_FRAMES or COMPUTE_MAXS) { + init { + assertEquals(ClassAndMemberVisitor.API_VERSION, api) + } + } } diff --git a/djvm/src/test/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitorTest.kt b/djvm/src/test/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitorTest.kt index fa5d9ffbf0..4bd8295927 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitorTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitorTest.kt @@ -21,7 +21,7 @@ class ClassAndMemberVisitorTest : TestBase() { @Test fun `can traverse classes`() { val classesVisited = mutableSetOf<ClassRepresentation>() - val visitor = object : ClassAndMemberVisitor() { + val visitor = object : ClassAndMemberVisitor(configuration, null) { override fun visitClass(clazz: ClassRepresentation): ClassRepresentation { classesVisited.add(clazz) return clazz @@ -47,7 +47,7 @@ class ClassAndMemberVisitorTest : TestBase() { @Test fun `can traverse fields`() { val membersVisited = mutableSetOf<Member>() - val visitor = object : ClassAndMemberVisitor() { + val visitor = object : ClassAndMemberVisitor(configuration, null) { override fun visitField(clazz: ClassRepresentation, field: Member): Member { membersVisited.add(field) return field @@ -77,7 +77,7 @@ class ClassAndMemberVisitorTest : TestBase() { @Test fun `can traverse methods`() { val membersVisited = mutableSetOf<Member>() - val visitor = object : ClassAndMemberVisitor() { + val visitor = object : ClassAndMemberVisitor(configuration, null) { override fun visitMethod(clazz: ClassRepresentation, method: Member): Member { membersVisited.add(method) return method @@ -102,7 +102,7 @@ class ClassAndMemberVisitorTest : TestBase() { @Test fun `can traverse class annotations`() { val annotations = mutableSetOf<String>() - val visitor = object : ClassAndMemberVisitor() { + val visitor = object : ClassAndMemberVisitor(configuration, null) { override fun visitClassAnnotation(clazz: ClassRepresentation, descriptor: String) { annotations.add(descriptor) } @@ -118,9 +118,21 @@ class ClassAndMemberVisitorTest : TestBase() { private class TestClassWithAnnotations @Test - fun `can traverse member annotations`() { + fun `cannot traverse member annotations when reading`() { val annotations = mutableSetOf<String>() - val visitor = object : ClassAndMemberVisitor() { + val visitor = object : ClassAndMemberVisitor(configuration, null) { + override fun visitMemberAnnotation(clazz: ClassRepresentation, member: Member, descriptor: String) { + annotations.add("${member.memberName}:$descriptor") + } + } + visitor.analyze<TestClassWithMemberAnnotations>(context) + assertThat(annotations).isEmpty() + } + + @Test + fun `can traverse member annotations when writing`() { + val annotations = mutableSetOf<String>() + val visitor = object : ClassAndMemberVisitor(configuration, Writer()) { override fun visitMemberAnnotation(clazz: ClassRepresentation, member: Member, descriptor: String) { annotations.add("${member.memberName}:$descriptor") } @@ -146,7 +158,7 @@ class ClassAndMemberVisitorTest : TestBase() { @Test fun `can traverse class sources`() { val sources = mutableSetOf<String>() - val visitor = object : ClassAndMemberVisitor() { + val visitor = object : ClassAndMemberVisitor(configuration, null) { override fun visitSource(clazz: ClassRepresentation, source: String) { sources.add(source) } @@ -160,9 +172,21 @@ class ClassAndMemberVisitorTest : TestBase() { } @Test - fun `can traverse instructions`() { + fun `does not traverse instructions when reading`() { val instructions = mutableSetOf<Pair<Member, Instruction>>() - val visitor = object : ClassAndMemberVisitor() { + val visitor = object : ClassAndMemberVisitor(configuration, null) { + override fun visitInstruction(method: Member, emitter: EmitterModule, instruction: Instruction) { + instructions.add(Pair(method, instruction)) + } + } + visitor.analyze<TestClassWithCode>(context) + assertThat(instructions).isEmpty() + } + + @Test + fun `can traverse instructions when writing`() { + val instructions = mutableSetOf<Pair<Member, Instruction>>() + val visitor = object : ClassAndMemberVisitor(configuration, Writer()) { override fun visitInstruction(method: Member, emitter: EmitterModule, instruction: Instruction) { instructions.add(Pair(method, instruction)) } diff --git a/djvm/src/test/kotlin/net/corda/djvm/analysis/ReferenceValidatorTest.kt b/djvm/src/test/kotlin/net/corda/djvm/analysis/ReferenceValidatorTest.kt deleted file mode 100644 index 816b232864..0000000000 --- a/djvm/src/test/kotlin/net/corda/djvm/analysis/ReferenceValidatorTest.kt +++ /dev/null @@ -1,66 +0,0 @@ -package net.corda.djvm.analysis - -import net.corda.djvm.TestBase -import net.corda.djvm.execution.SandboxedRunnable -import net.corda.djvm.validation.ReferenceValidator -import org.assertj.core.api.Assertions.assertThat -import org.junit.Test - -class ReferenceValidatorTest : TestBase() { - - private fun validator(whitelist: Whitelist = Whitelist.MINIMAL) = - ReferenceValidator(AnalysisConfiguration(whitelist)) - - @Test - fun `can validate when there are no references`() = analyze { context -> - analyze<EmptyRunnable>(context) - val (_, messages) = validator().validate(context, this) - assertThat(messages.count).isEqualTo(0) - } - - private class EmptyRunnable : SandboxedRunnable<Int, Int> { - override fun run(input: Int): Int? { - return null - } - } - - @Test - fun `can validate when there are references`() = analyze { context -> - analyze<RunnableWithReferences>(context) - analyze<TestRandom>(context) - val (_, messages) = validator().validate(context, this) - assertThat(messages.count).isEqualTo(0) - } - - private class RunnableWithReferences : SandboxedRunnable<Int, Int> { - override fun run(input: Int): Int? { - return TestRandom().nextInt() - } - } - - private class TestRandom { - external fun nextInt(): Int - } - - @Test - fun `can validate when there are transient references`() = analyze { context -> - analyze<RunnableWithTransientReferences>(context) - analyze<ReferencedClass>(context) - analyze<TestRandom>(context) - val (_, messages) = validator().validate(context, this) - assertThat(messages.count).isEqualTo(0) - } - - private class RunnableWithTransientReferences : SandboxedRunnable<Int, Int> { - override fun run(input: Int): Int? { - return ReferencedClass().test() - } - } - - private class ReferencedClass { - fun test(): Int { - return TestRandom().nextInt() - } - } - -} diff --git a/djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveClassWithByteCode.kt b/djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveClassWithByteCode.kt index 457b71c96f..cc122b7a8f 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveClassWithByteCode.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveClassWithByteCode.kt @@ -1,27 +1,31 @@ package net.corda.djvm.assertions import net.corda.djvm.rewiring.LoadedClass -import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.* class AssertiveClassWithByteCode(private val loadedClass: LoadedClass) { fun isSandboxed(): AssertiveClassWithByteCode { - Assertions.assertThat(loadedClass.type.name).startsWith("sandbox.") + assertThat(loadedClass.type.name).startsWith("sandbox.") return this } fun hasNotBeenModified(): AssertiveClassWithByteCode { - Assertions.assertThat(loadedClass.byteCode.isModified) + assertThat(loadedClass.byteCode.isModified) .`as`("Byte code has been modified") .isEqualTo(false) return this } fun hasBeenModified(): AssertiveClassWithByteCode { - Assertions.assertThat(loadedClass.byteCode.isModified) - .`as`("Byte code has been modified") + assertThat(loadedClass.byteCode.isModified) + .`as`("Byte code has not been modified") .isEqualTo(true) return this } + fun hasClassName(className: String): AssertiveClassWithByteCode { + assertThat(loadedClass.type.name).isEqualTo(className) + return this + } } diff --git a/djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveReferenceMap.kt b/djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveReferenceMap.kt index 4168e318db..5b349edbe3 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveReferenceMap.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveReferenceMap.kt @@ -10,7 +10,7 @@ open class AssertiveReferenceMap(private val references: ReferenceMap) { fun hasCount(count: Int): AssertiveReferenceMap { val allReferences = references.joinToString("\n") { " - $it" } - Assertions.assertThat(references.size) + Assertions.assertThat(references.numberOfReferences) .overridingErrorMessage("Expected $count reference(s), found:\n$allReferences") .isEqualTo(count) return this diff --git a/djvm/src/test/kotlin/net/corda/djvm/code/ClassMutatorTest.kt b/djvm/src/test/kotlin/net/corda/djvm/code/ClassMutatorTest.kt index c5dc170b9c..d729d9de2f 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/code/ClassMutatorTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/code/ClassMutatorTest.kt @@ -21,7 +21,7 @@ class ClassMutatorTest : TestBase() { } } val context = context - val mutator = ClassMutator(Visitor(), configuration, listOf(definitionProvider)) + val mutator = ClassMutator(Writer(), configuration, listOf(definitionProvider)) mutator.analyze<TestClass>(context) assertThat(hasProvidedDefinition).isTrue() assertThat(context.classes.get<TestClass>().access or ACC_STRICT).isNotEqualTo(0) @@ -39,7 +39,7 @@ class ClassMutatorTest : TestBase() { } } val context = context - val mutator = ClassMutator(Visitor(), configuration, listOf(definitionProvider)) + val mutator = ClassMutator(Writer(), configuration, listOf(definitionProvider)) mutator.analyze<TestClassWithMembers>(context) assertThat(hasProvidedDefinition).isTrue() for (member in context.classes.get<TestClassWithMembers>().members.values) { @@ -67,7 +67,7 @@ class ClassMutatorTest : TestBase() { } } val context = context - val mutator = ClassMutator(Visitor(), configuration, emitters = listOf(emitter)) + val mutator = ClassMutator(Writer(), configuration, emitters = listOf(emitter)) mutator.analyze<TestClassWithMembers>(context) assertThat(hasEmittedCode).isTrue() assertThat(shouldPreventDefault).isTrue() diff --git a/djvm/src/test/kotlin/net/corda/djvm/code/EmitterModuleTest.kt b/djvm/src/test/kotlin/net/corda/djvm/code/EmitterModuleTest.kt index 0289fbd4d9..fb37676669 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/code/EmitterModuleTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/code/EmitterModuleTest.kt @@ -7,6 +7,7 @@ import org.junit.Test import org.objectweb.asm.ClassVisitor import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Opcodes.NEW +import org.objectweb.asm.Type class EmitterModuleTest : TestBase() { @@ -14,15 +15,15 @@ class EmitterModuleTest : TestBase() { fun `can emit code to method body`() { var hasEmittedTypeInstruction = false val methodVisitor = object : MethodVisitor(ClassAndMemberVisitor.API_VERSION) { - override fun visitTypeInsn(opcode: Int, type: String?) { - if (opcode == NEW && type == java.lang.String::class.java.name) { + override fun visitTypeInsn(opcode: Int, type: String) { + if (opcode == NEW && type == Type.getInternalName(java.lang.String::class.java)) { hasEmittedTypeInstruction = true } } } val visitor = object : ClassVisitor(ClassAndMemberVisitor.API_VERSION) { override fun visitMethod( - access: Int, name: String?, descriptor: String?, signature: String?, exceptions: Array<out String>? + access: Int, name: String, descriptor: String, signature: String?, exceptions: Array<out String>? ): MethodVisitor { return methodVisitor } diff --git a/djvm/src/test/kotlin/net/corda/djvm/costing/RuntimeCostTest.kt b/djvm/src/test/kotlin/net/corda/djvm/costing/RuntimeCostTest.kt index e44fb883db..0e68806d33 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/costing/RuntimeCostTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/costing/RuntimeCostTest.kt @@ -3,12 +3,13 @@ package net.corda.djvm.costing import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.Test +import sandbox.net.corda.djvm.costing.ThresholdViolationError class RuntimeCostTest { @Test fun `can increment cost`() { - val cost = RuntimeCost(10, { "failed" }) + val cost = RuntimeCost(10) { "failed" } cost.increment(1) assertThat(cost.value).isEqualTo(1) } @@ -16,8 +17,8 @@ class RuntimeCostTest { @Test fun `cannot increment cost beyond threshold`() { Thread { - val cost = RuntimeCost(10, { "failed in ${it.name}" }) - assertThatExceptionOfType(ThresholdViolationException::class.java) + val cost = RuntimeCost(10) { "failed in ${it.name}" } + assertThatExceptionOfType(ThresholdViolationError::class.java) .isThrownBy { cost.increment(11) } .withMessage("failed in Foo") assertThat(cost.value).isEqualTo(11) diff --git a/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt b/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt index a677b4fd35..92fe59e159 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt @@ -6,13 +6,15 @@ import foo.bar.sandbox.toNumber import net.corda.djvm.TestBase import net.corda.djvm.analysis.Whitelist import net.corda.djvm.assertions.AssertionExtensions.withProblem -import net.corda.djvm.costing.ThresholdViolationException import net.corda.djvm.rewiring.SandboxClassLoadingException import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.Test +import sandbox.net.corda.djvm.costing.ThresholdViolationError +import sandbox.net.corda.djvm.rules.RuleViolationError import java.nio.file.Files import java.util.* +import java.util.function.Function class SandboxExecutorTest : TestBase() { @@ -24,8 +26,8 @@ class SandboxExecutorTest : TestBase() { assertThat(result).isEqualTo("sandbox") } - class TestSandboxedRunnable : SandboxedRunnable<Int, String> { - override fun run(input: Int): String? { + class TestSandboxedRunnable : Function<Int, String> { + override fun apply(input: Int): String { return "sandbox" } } @@ -42,8 +44,8 @@ class SandboxExecutorTest : TestBase() { .withMessageContaining("Contract constraint violated") } - class Contract : SandboxedRunnable<Transaction?, Unit> { - override fun run(input: Transaction?) { + class Contract : Function<Transaction?, Unit> { + override fun apply(input: Transaction?) { throw IllegalArgumentException("Contract constraint violated") } } @@ -58,12 +60,12 @@ class SandboxExecutorTest : TestBase() { assertThat(result).isEqualTo(0xfed_c0de + 2) } - class TestObjectHashCode : SandboxedRunnable<Int, Int> { - override fun run(input: Int): Int? { + class TestObjectHashCode : Function<Int, Int> { + override fun apply(input: Int): Int { val obj = Object() val hash1 = obj.hashCode() val hash2 = obj.hashCode() - assert(hash1 == hash2) + require(hash1 == hash2) return Object().hashCode() } } @@ -76,8 +78,8 @@ class SandboxExecutorTest : TestBase() { assertThat(result).isEqualTo(0xfed_c0de + 1) } - class TestObjectHashCodeWithHierarchy : SandboxedRunnable<Int, Int> { - override fun run(input: Int): Int? { + class TestObjectHashCodeWithHierarchy : Function<Int, Int> { + override fun apply(input: Int): Int { val obj = MyObject() return obj.hashCode() } @@ -91,9 +93,9 @@ class SandboxExecutorTest : TestBase() { .withMessageContaining("terminated due to excessive use of looping") } - class TestThresholdBreach : SandboxedRunnable<Int, Int> { + class TestThresholdBreach : Function<Int, Int> { private var x = 0 - override fun run(input: Int): Int? { + override fun apply(input: Int): Int { for (i in 0..1_000_000) { x += 1 } @@ -109,8 +111,8 @@ class SandboxExecutorTest : TestBase() { .withCauseInstanceOf(StackOverflowError::class.java) } - class TestStackOverflow : SandboxedRunnable<Int, Int> { - override fun run(input: Int): Int? { + class TestStackOverflow : Function<Int, Int> { + override fun apply(input: Int): Int { return a() } @@ -124,11 +126,12 @@ class SandboxExecutorTest : TestBase() { val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration) assertThatExceptionOfType(SandboxException::class.java) .isThrownBy { contractExecutor.run<TestKotlinMetaClasses>(0) } - .withMessageContaining("java/util/Random.<clinit>(): Disallowed reference to reflection API; sun.misc.Unsafe.getUnsafe()") + .withCauseInstanceOf(RuleViolationError::class.java) + .withMessageContaining("Disallowed reference to reflection API") } - class TestKotlinMetaClasses : SandboxedRunnable<Int, Int> { - override fun run(input: Int): Int? { + class TestKotlinMetaClasses : Function<Int, Int> { + override fun apply(input: Int): Int { val someNumber = testRandom() return "12345".toNumber() * someNumber } @@ -139,30 +142,32 @@ class SandboxExecutorTest : TestBase() { val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration) assertThatExceptionOfType(SandboxException::class.java) .isThrownBy { contractExecutor.run<TestNonDeterministicCode>(0) } - .withCauseInstanceOf(SandboxClassLoadingException::class.java) - .withProblem("java/util/Random.<clinit>(): Disallowed reference to reflection API; sun.misc.Unsafe.getUnsafe()") + .withCauseInstanceOf(RuleViolationError::class.java) + .withProblem("Disallowed reference to reflection API") } - class TestNonDeterministicCode : SandboxedRunnable<Int, Int> { - override fun run(input: Int): Int? { + class TestNonDeterministicCode : Function<Int, Int> { + override fun apply(input: Int): Int { return Random().nextInt() } } @Test fun `cannot execute runnable that catches ThreadDeath`() = sandbox(DEFAULT) { + TestCatchThreadDeath().apply { + assertThat(apply(0)).isEqualTo(1) + } + val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration) assertThatExceptionOfType(SandboxException::class.java) .isThrownBy { contractExecutor.run<TestCatchThreadDeath>(0) } - .withCauseInstanceOf(SandboxClassLoadingException::class.java) - .withMessageContaining("Disallowed catch of ThreadDeath exception") - .withMessageContaining(TestCatchThreadDeath::class.java.simpleName) + .withCauseExactlyInstanceOf(ThreadDeath::class.java) } - class TestCatchThreadDeath : SandboxedRunnable<Int, Int> { - override fun run(input: Int): Int? { + class TestCatchThreadDeath : Function<Int, Int> { + override fun apply(input: Int): Int { return try { - 0 + throw ThreadDeath() } catch (exception: ThreadDeath) { 1 } @@ -170,20 +175,46 @@ class SandboxExecutorTest : TestBase() { } @Test - fun `cannot execute runnable that catches ThresholdViolationException`() = sandbox(DEFAULT) { + fun `cannot execute runnable that catches ThresholdViolationError`() = sandbox(DEFAULT) { + TestCatchThresholdViolationError().apply { + assertThat(apply(0)).isEqualTo(1) + } + val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration) assertThatExceptionOfType(SandboxException::class.java) - .isThrownBy { contractExecutor.run<TestCatchThresholdViolationException>(0) } - .withCauseInstanceOf(SandboxClassLoadingException::class.java) - .withMessageContaining("Disallowed catch of threshold violation exception") - .withMessageContaining(TestCatchThresholdViolationException::class.java.simpleName) + .isThrownBy { contractExecutor.run<TestCatchThresholdViolationError>(0) } + .withCauseExactlyInstanceOf(ThresholdViolationError::class.java) + .withMessageContaining("Can't catch this!") } - class TestCatchThresholdViolationException : SandboxedRunnable<Int, Int> { - override fun run(input: Int): Int? { + class TestCatchThresholdViolationError : Function<Int, Int> { + override fun apply(input: Int): Int { return try { - 0 - } catch (exception: ThresholdViolationException) { + throw ThresholdViolationError("Can't catch this!") + } catch (exception: ThresholdViolationError) { + 1 + } + } + } + + @Test + fun `cannot execute runnable that catches RuleViolationError`() = sandbox(DEFAULT) { + TestCatchRuleViolationError().apply { + assertThat(apply(0)).isEqualTo(1) + } + + val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration) + assertThatExceptionOfType(SandboxException::class.java) + .isThrownBy { contractExecutor.run<TestCatchRuleViolationError>(0) } + .withCauseExactlyInstanceOf(RuleViolationError::class.java) + .withMessageContaining("Can't catch this!") + } + + class TestCatchRuleViolationError : Function<Int, Int> { + override fun apply(input: Int): Int { + return try { + throw RuleViolationError("Can't catch this!") + } catch (exception: RuleViolationError) { 1 } } @@ -209,12 +240,12 @@ class SandboxExecutorTest : TestBase() { fun `cannot catch ThreadDeath`() = sandbox(DEFAULT) { val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration) assertThatExceptionOfType(SandboxException::class.java) - .isThrownBy { contractExecutor.run<TestCatchThrowableErrorAndThreadDeath>(3) } + .isThrownBy { contractExecutor.run<TestCatchThrowableErrorsAndThreadDeath>(3) } .withCauseInstanceOf(ThreadDeath::class.java) } - class TestCatchThrowableAndError : SandboxedRunnable<Int, Int> { - override fun run(input: Int): Int? { + class TestCatchThrowableAndError : Function<Int, Int> { + override fun apply(input: Int): Int { return try { when (input) { 1 -> throw Throwable() @@ -229,13 +260,27 @@ class SandboxExecutorTest : TestBase() { } } - class TestCatchThrowableErrorAndThreadDeath : SandboxedRunnable<Int, Int> { - override fun run(input: Int): Int? { + class TestCatchThrowableErrorsAndThreadDeath : Function<Int, Int> { + override fun apply(input: Int): Int { return try { when (input) { 1 -> throw Throwable() 2 -> throw Error() - 3 -> throw ThreadDeath() + 3 -> try { + throw ThreadDeath() + } catch (ex: ThreadDeath) { + 3 + } + 4 -> try { + throw StackOverflowError("FAKE OVERFLOW!") + } catch (ex: StackOverflowError) { + 4 + } + 5 -> try { + throw OutOfMemoryError("FAKE OOM!") + } catch (ex: OutOfMemoryError) { + 5 + } else -> 0 } } catch (exception: Error) { @@ -246,6 +291,24 @@ class SandboxExecutorTest : TestBase() { } } + @Test + fun `cannot catch stack-overflow error`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration) + assertThatExceptionOfType(SandboxException::class.java) + .isThrownBy { contractExecutor.run<TestCatchThrowableErrorsAndThreadDeath>(4) } + .withCauseInstanceOf(StackOverflowError::class.java) + .withMessageContaining("FAKE OVERFLOW!") + } + + @Test + fun `cannot catch out-of-memory error`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration) + assertThatExceptionOfType(SandboxException::class.java) + .isThrownBy { contractExecutor.run<TestCatchThrowableErrorsAndThreadDeath>(5) } + .withCauseInstanceOf(OutOfMemoryError::class.java) + .withMessageContaining("FAKE OOM!") + } + @Test fun `cannot persist state across sessions`() = sandbox(DEFAULT) { val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration) @@ -256,8 +319,8 @@ class SandboxExecutorTest : TestBase() { .isEqualTo(1) } - class TestStatePersistence : SandboxedRunnable<Int, Int> { - override fun run(input: Int): Int? { + class TestStatePersistence : Function<Int, Int> { + override fun apply(input: Int): Int { ReferencedClass.value += 1 return ReferencedClass.value } @@ -274,11 +337,11 @@ class SandboxExecutorTest : TestBase() { assertThatExceptionOfType(SandboxException::class.java) .isThrownBy { contractExecutor.run<TestIO>(0) } .withCauseInstanceOf(SandboxClassLoadingException::class.java) - .withMessageContaining("Files.walk(Path, Integer, FileVisitOption[]): Disallowed dynamic invocation in method") + .withMessageContaining("Class file not found; java/nio/file/Files.class") } - class TestIO : SandboxedRunnable<Int, Int> { - override fun run(input: Int): Int? { + class TestIO : Function<Int, Int> { + override fun apply(input: Int): Int { val file = Files.createTempFile("test", ".dat") Files.newBufferedWriter(file).use { it.write("Hello world!") @@ -292,14 +355,13 @@ class SandboxExecutorTest : TestBase() { val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration) assertThatExceptionOfType(SandboxException::class.java) .isThrownBy { contractExecutor.run<TestReflection>(0) } - .withCauseInstanceOf(SandboxClassLoadingException::class.java) - .withMessageContaining("Disallowed reference to reflection API") + .withCauseInstanceOf(RuleViolationError::class.java) + .withMessageContaining("Disallowed reference to API;") .withMessageContaining("java.lang.Class.newInstance()") - .withMessageContaining("java.lang.reflect.Method.invoke(Object, Object[])") } - class TestReflection : SandboxedRunnable<Int, Int> { - override fun run(input: Int): Int? { + class TestReflection : Function<Int, Int> { + override fun apply(input: Int): Int { val clazz = Object::class.java val obj = clazz.newInstance() val result = clazz.methods.first().invoke(obj) @@ -307,4 +369,150 @@ class SandboxExecutorTest : TestBase() { } } + @Test + fun `can load and execute code that uses notify()`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor<Int, String>(configuration) + assertThatExceptionOfType(SandboxException::class.java) + .isThrownBy { contractExecutor.run<TestMonitors>(1) } + .withCauseInstanceOf(RuleViolationError::class.java) + .withMessageContaining("Disallowed reference to API;") + .withMessageContaining("java.lang.Object.notify()") + } + + @Test + fun `can load and execute code that uses notifyAll()`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor<Int, String>(configuration) + assertThatExceptionOfType(SandboxException::class.java) + .isThrownBy { contractExecutor.run<TestMonitors>(2) } + .withCauseInstanceOf(RuleViolationError::class.java) + .withMessageContaining("Disallowed reference to API;") + .withMessageContaining("java.lang.Object.notifyAll()") + } + + @Test + fun `can load and execute code that uses wait()`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor<Int, String>(configuration) + assertThatExceptionOfType(SandboxException::class.java) + .isThrownBy { contractExecutor.run<TestMonitors>(3) } + .withCauseInstanceOf(RuleViolationError::class.java) + .withMessageContaining("Disallowed reference to API;") + .withMessageContaining("java.lang.Object.wait()") + } + + @Test + fun `can load and execute code that uses wait(long)`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor<Int, String>(configuration) + assertThatExceptionOfType(SandboxException::class.java) + .isThrownBy { contractExecutor.run<TestMonitors>(4) } + .withCauseInstanceOf(RuleViolationError::class.java) + .withMessageContaining("Disallowed reference to API;") + .withMessageContaining("java.lang.Object.wait(Long)") + } + + @Test + fun `can load and execute code that uses wait(long,int)`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor<Int, String>(configuration) + assertThatExceptionOfType(SandboxException::class.java) + .isThrownBy { contractExecutor.run<TestMonitors>(5) } + .withCauseInstanceOf(RuleViolationError::class.java) + .withMessageContaining("Disallowed reference to API;") + .withMessageContaining("java.lang.Object.wait(Long, Integer)") + } + + @Test + fun `code after forbidden APIs is intact`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor<Int, String>(configuration) + assertThat(contractExecutor.run<TestMonitors>(0).result) + .isEqualTo("unknown") + } + + class TestMonitors : Function<Int, String> { + override fun apply(input: Int): String { + return synchronized(this) { + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + val javaObject = this as java.lang.Object + when(input) { + 1 -> { + javaObject.notify() + "notify" + } + 2 -> { + javaObject.notifyAll() + "notifyAll" + } + 3 -> { + javaObject.wait() + "wait" + } + 4 -> { + javaObject.wait(100) + "wait(100)" + } + 5 -> { + javaObject.wait(100, 10) + "wait(100, 10)" + } + else -> "unknown" + } + } + } + } + + @Test + fun `can load and execute code that has a native method`() = sandbox(DEFAULT) { + assertThatExceptionOfType(UnsatisfiedLinkError::class.java) + .isThrownBy { TestNativeMethod().apply(0) } + .withMessageContaining("TestNativeMethod.evilDeeds()I") + + val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration) + assertThatExceptionOfType(SandboxException::class.java) + .isThrownBy { contractExecutor.run<TestNativeMethod>(0) } + .withCauseInstanceOf(RuleViolationError::class.java) + .withMessageContaining("Native method has been deleted") + } + + class TestNativeMethod : Function<Int, Int> { + override fun apply(input: Int): Int { + return evilDeeds() + } + + private external fun evilDeeds(): Int + } + + @Test + fun `check arrays still work`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor<Int, Array<Int>>(configuration) + contractExecutor.run<TestArray>(100).apply { + assertThat(result).isEqualTo(arrayOf(100)) + } + } + + class TestArray : Function<Int, Array<Int>> { + override fun apply(input: Int): Array<Int> { + return listOf(input).toTypedArray() + } + } + + @Test + fun `can load and execute class that has finalize`() = sandbox(DEFAULT) { + assertThatExceptionOfType(UnsupportedOperationException::class.java) + .isThrownBy { TestFinalizeMethod().apply(100) } + .withMessageContaining("Very Bad Thing") + + val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration) + contractExecutor.run<TestFinalizeMethod>(100).apply { + assertThat(result).isEqualTo(100) + } + } + + class TestFinalizeMethod : Function<Int, Int> { + override fun apply(input: Int): Int { + finalize() + return input + } + + private fun finalize() { + throw UnsupportedOperationException("Very Bad Thing") + } + } } diff --git a/djvm/src/test/kotlin/net/corda/djvm/references/MemberModuleTest.kt b/djvm/src/test/kotlin/net/corda/djvm/references/MemberModuleTest.kt index 616a7ca7b6..71cd99cbf7 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/references/MemberModuleTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/references/MemberModuleTest.kt @@ -4,6 +4,7 @@ import net.corda.djvm.annotations.NonDeterministic import org.assertj.core.api.Assertions.assertThat import org.jetbrains.annotations.NotNull import org.junit.Test +import org.objectweb.asm.Type class MemberModuleTest { @@ -132,7 +133,7 @@ class MemberModuleTest { } private val java.lang.Class<*>.descriptor: String - get() = "L${name.replace('.', '/')};" + get() = Type.getDescriptor(this) private fun member(member: String) = MemberReference("", member, "") diff --git a/djvm/src/test/kotlin/net/corda/djvm/rewiring/ClassRewriterTest.kt b/djvm/src/test/kotlin/net/corda/djvm/rewiring/ClassRewriterTest.kt index 6abcca8e21..bd7e86dac0 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/rewiring/ClassRewriterTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/rewiring/ClassRewriterTest.kt @@ -6,10 +6,11 @@ import foo.bar.sandbox.Empty import foo.bar.sandbox.StrictFloat import net.corda.djvm.TestBase import net.corda.djvm.assertions.AssertionExtensions.assertThat -import net.corda.djvm.costing.ThresholdViolationException import net.corda.djvm.execution.ExecutionProfile import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.Test +import sandbox.net.corda.djvm.costing.ThresholdViolationError +import java.nio.file.Paths class ClassRewriterTest : TestBase() { @@ -45,7 +46,7 @@ class ClassRewriterTest : TestBase() { val callable = newCallable<B>() assertThat(callable).hasBeenModified() assertThat(callable).isSandboxed() - assertThatExceptionOfType(ThresholdViolationException::class.java).isThrownBy { + assertThatExceptionOfType(ThresholdViolationError::class.java).isThrownBy { callable.createAndInvoke() }.withMessageContaining("terminated due to excessive use of looping") assertThat(runtimeCosts) @@ -61,4 +62,44 @@ class ClassRewriterTest : TestBase() { callable.createAndInvoke() } + @Test + fun `can load a Java API that still exists in Java runtime`() = sandbox(DEFAULT) { + assertThat(loadClass<MutableList<*>>()) + .hasClassName("sandbox.java.util.List") + .hasBeenModified() + } + + @Test + fun `cannot load a Java API that was deleted from Java runtime`() = sandbox(DEFAULT) { + assertThatExceptionOfType(SandboxClassLoadingException::class.java) + .isThrownBy { loadClass<Paths>() } + .withMessageContaining("Class file not found; java/nio/file/Paths.class") + } + + @Test + fun `load internal Sun class that still exists in Java runtime`() = sandbox(DEFAULT) { + assertThat(loadClass<sun.misc.Unsafe>()) + .hasClassName("sandbox.sun.misc.Unsafe") + .hasBeenModified() + } + + @Test + fun `cannot load internal Sun class that was deleted from Java runtime`() = sandbox(DEFAULT) { + assertThatExceptionOfType(SandboxClassLoadingException::class.java) + .isThrownBy { loadClass<sun.misc.Timer>() } + .withMessageContaining("Class file not found; sun/misc/Timer.class") + } + + @Test + fun `can load local class`() = sandbox(DEFAULT) { + assertThat(loadClass<Example>()) + .hasClassName("sandbox.net.corda.djvm.rewiring.ClassRewriterTest\$Example") + .hasBeenModified() + } + + class Example : java.util.function.Function<Int, Int> { + override fun apply(input: Int): Int { + return input + } + } } diff --git a/djvm/src/test/kotlin/net/corda/djvm/rules/ReferenceExtractorTest.kt b/djvm/src/test/kotlin/net/corda/djvm/rules/ReferenceExtractorTest.kt index 3ea655c24f..bde75b7feb 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/rules/ReferenceExtractorTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/rules/ReferenceExtractorTest.kt @@ -4,23 +4,11 @@ import foo.bar.sandbox.Callable import net.corda.djvm.TestBase import net.corda.djvm.assertions.AssertionExtensions.assertThat import org.junit.Test +import org.objectweb.asm.Type import java.util.* class ReferenceExtractorTest : TestBase() { - @Test - fun `can find method references`() = validate<A> { context -> - assertThat(context.references) - .hasClass("java/util/Random") - .withLocationCount(1) - .hasMember("java/lang/Object", "<init>", "()V") - .withLocationCount(1) - .hasMember("java/util/Random", "<init>", "()V") - .withLocationCount(1) - .hasMember("java/util/Random", "nextInt", "()I") - .withLocationCount(1) - } - class A : Callable { override fun call() { synchronized(this) { @@ -29,25 +17,10 @@ class ReferenceExtractorTest : TestBase() { } } - @Test - fun `can find field references`() = validate<B> { context -> - assertThat(context.references) - .hasMember(B::class.java.name.replace('.', '/'), "foo", "Ljava/lang/String;") - } - - class B { - @JvmField - val foo: String = "" - - fun test(): String { - return foo - } - } - @Test fun `can find class references`() = validate<C> { context -> assertThat(context.references) - .hasClass(A::class.java.name.replace('.', '/')) + .hasClass(Type.getInternalName(A::class.java)) } class C { diff --git a/djvm/src/test/kotlin/net/corda/djvm/rules/RuleValidatorTest.kt b/djvm/src/test/kotlin/net/corda/djvm/rules/RuleValidatorTest.kt index 260cfc6488..cd37df166a 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/rules/RuleValidatorTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/rules/RuleValidatorTest.kt @@ -42,8 +42,7 @@ class RuleValidatorTest : TestBase() { assertThat(context.messages) .hasErrorCount(0) .hasWarningCount(0) - .hasInfoCount(1) - .withMessage("Stripped monitoring instruction") + .hasInfoCount(0) .hasTraceCount(4) .withMessage("Synchronization specifier will be ignored") .withMessage("Strict floating-point arithmetic will be applied") diff --git a/djvm/src/test/kotlin/net/corda/djvm/source/SourceClassLoaderTest.kt b/djvm/src/test/kotlin/net/corda/djvm/source/SourceClassLoaderTest.kt index 3837948678..85f4596f1f 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/source/SourceClassLoaderTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/source/SourceClassLoaderTest.kt @@ -67,7 +67,7 @@ class SourceClassLoaderTest { val (first, second) = this val directory = first.parent val classLoader = SourceClassLoader(listOf(directory), classResolver) - assertThat(classLoader.resolvedUrls).anySatisfy { + assertThat(classLoader.urLs).anySatisfy { assertThat(it).isEqualTo(first.toUri().toURL()) }.anySatisfy { assertThat(it).isEqualTo(second.toUri().toURL()) diff --git a/djvm/src/test/resources/log4j2-test.xml b/djvm/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000000..b12cea5b2d --- /dev/null +++ b/djvm/src/test/resources/log4j2-test.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Configuration status="info"> + + <ThresholdFilter level="info"/> + <Appenders> + <Console name="Console-Appender" target="SYSTEM_OUT"> + <PatternLayout pattern="%date %highlight{%level %c{1}.%method - %msg%n}{INFO=white,WARN=red,FATAL=bright red}"/> + </Console> + </Appenders> + + <Loggers> + <Root level="info"> + <AppenderRef ref="Console-Appender"/> + </Root> + </Loggers> + +</Configuration> \ No newline at end of file diff --git a/djvm/src/test/resources/log4j2.xml b/djvm/src/test/resources/log4j2.xml deleted file mode 100644 index 93e84b6252..0000000000 --- a/djvm/src/test/resources/log4j2.xml +++ /dev/null @@ -1,39 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<Configuration status="info"> - - <ThresholdFilter level="info"/> - <Appenders> - <!-- Will generate up to 10 log files for a given day. During every rollover it will delete - those that are older than 60 days, but keep the most recent 10 GB --> - <RollingFile name="RollingFile-Appender" - fileName="djvm.log" - filePattern="djvm.%date{yyyy-MM-dd}-%i.log.gz"> - - <PatternLayout pattern="%date{ISO8601}{UTC}Z [%-5level] %c - %msg%n"/> - - <Policies> - <TimeBasedTriggeringPolicy/> - <SizeBasedTriggeringPolicy size="10MB"/> - </Policies> - - <DefaultRolloverStrategy min="1" max="10"> - <Delete basePath="" maxDepth="1"> - <IfFileName glob="djvm*.log.gz"/> - <IfLastModified age="60d"> - <IfAny> - <IfAccumulatedFileSize exceeds="10 GB"/> - </IfAny> - </IfLastModified> - </Delete> - </DefaultRolloverStrategy> - - </RollingFile> - </Appenders> - - <Loggers> - <Root level="info"> - <AppenderRef ref="RollingFile-Appender"/> - </Root> - </Loggers> - -</Configuration> \ No newline at end of file diff --git a/docs/source/api-flows.rst b/docs/source/api-flows.rst index 5e15223144..d001529ecd 100644 --- a/docs/source/api-flows.rst +++ b/docs/source/api-flows.rst @@ -219,13 +219,13 @@ There are several ways to retrieve a notary from the network map: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 01 :end-before: DOCEND 01 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 01 :end-before: DOCEND 01 @@ -237,13 +237,13 @@ We can also use the network map to retrieve a specific counterparty: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 02 :end-before: DOCEND 02 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 02 :end-before: DOCEND 02 @@ -281,13 +281,13 @@ InitiateFlow .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART initiateFlow :end-before: DOCEND initiateFlow :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART initiateFlow :end-before: DOCEND initiateFlow @@ -306,13 +306,13 @@ Once we have a ``FlowSession`` object we can send arbitrary data to a counterpar .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 04 :end-before: DOCEND 04 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 04 :end-before: DOCEND 04 @@ -338,13 +338,13 @@ be what it appears to be! We must unwrap the ``UntrustworthyData`` using a lambd .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 05 :end-before: DOCEND 05 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 05 :end-before: DOCEND 05 @@ -355,13 +355,13 @@ as it likes, and each party can invoke a different response flow: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 06 :end-before: DOCEND 06 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 06 :end-before: DOCEND 06 @@ -380,13 +380,13 @@ type of data sent doesn't need to match the type of the data received back: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 07 :end-before: DOCEND 07 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 07 :end-before: DOCEND 07 @@ -405,13 +405,13 @@ Our side of the flow must mirror these calls. We could do this as follows: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 08 :end-before: DOCEND 08 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 08 :end-before: DOCEND 08 @@ -431,11 +431,16 @@ Consider the following contrived example using the old ``Party`` based API: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/LaunchSpaceshipFlow.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/LaunchSpaceshipFlow.kt :language: kotlin :start-after: DOCSTART LaunchSpaceshipFlow :end-before: DOCEND LaunchSpaceshipFlow + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/LaunchSpaceshipFlow.java + :language: java + :start-after: DOCSTART LaunchSpaceshipFlow + :end-before: DOCEND LaunchSpaceshipFlow + The intention of the flows is very clear: LaunchSpaceshipFlow asks the president whether a spaceship should be launched. It is expecting a boolean reply. The president in return first tells the secretary that they need coffee, which is also communicated with a boolean. Afterwards the president replies to the launcher that they don't want to launch. @@ -452,11 +457,16 @@ of flows would look like this: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/LaunchSpaceshipFlow.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/LaunchSpaceshipFlow.kt :language: kotlin :start-after: DOCSTART LaunchSpaceshipFlowCorrect :end-before: DOCEND LaunchSpaceshipFlowCorrect + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/LaunchSpaceshipFlow.java + :language: java + :start-after: DOCSTART LaunchSpaceshipFlowCorrect + :end-before: DOCEND LaunchSpaceshipFlowCorrect + Note how the president is now explicit about which session it wants to send to. Porting from the old Party-based API @@ -467,13 +477,13 @@ explicit in the ``initiateFlow`` function call. To port existing code: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART FlowSession porting :end-before: DOCEND FlowSession porting :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART FlowSession porting :end-before: DOCEND FlowSession porting @@ -550,13 +560,13 @@ the transaction's states: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 09 :end-before: DOCEND 09 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 09 :end-before: DOCEND 09 @@ -566,13 +576,13 @@ We can also choose to send the transaction to additional parties who aren't one .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 10 :end-before: DOCEND 10 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 10 :end-before: DOCEND 10 @@ -601,13 +611,13 @@ transaction ourselves, we can automatically gather the signatures of the other r .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 15 :end-before: DOCEND 15 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 15 :end-before: DOCEND 15 @@ -618,13 +628,13 @@ transaction (by implementing the ``checkTransaction`` method) and provide their .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 16 :end-before: DOCEND 16 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 16 :end-before: DOCEND 16 @@ -647,13 +657,13 @@ transaction data vending requests as the receiver walks the dependency chain usi .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 12 :end-before: DOCEND 12 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 12 :end-before: DOCEND 12 @@ -664,13 +674,13 @@ dependencies and verify the transaction: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 13 :end-before: DOCEND 13 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 13 :end-before: DOCEND 13 @@ -680,13 +690,13 @@ We can also send and receive a ``StateAndRef`` dependency chain and automaticall .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 14 :end-before: DOCEND 14 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 14 :end-before: DOCEND 14 @@ -738,13 +748,13 @@ To provide a progress tracker, we have to override ``FlowLogic.progressTracker`` .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 17 :end-before: DOCEND 17 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 17 :end-before: DOCEND 17 @@ -754,13 +764,13 @@ We then update the progress tracker's current step as we progress through the fl .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 18 :end-before: DOCEND 18 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 18 :end-before: DOCEND 18 diff --git a/docs/source/api-persistence.rst b/docs/source/api-persistence.rst index 67d1bd2767..8fabd37e17 100644 --- a/docs/source/api-persistence.rst +++ b/docs/source/api-persistence.rst @@ -136,14 +136,14 @@ JDBC session's can be used in Flows and Service Plugins (see ":doc:`flow-state-m The following example illustrates the creation of a custom corda service using a jdbcSession: -.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt +.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/CustomVaultQuery.kt :language: kotlin :start-after: DOCSTART CustomVaultQuery :end-before: DOCEND CustomVaultQuery which is then referenced within a custom flow: -.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt +.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/CustomVaultQuery.kt :language: kotlin :start-after: DOCSTART TopupIssuer :end-before: DOCEND TopupIssuer diff --git a/docs/source/api-testing.rst b/docs/source/api-testing.rst index 78bf5e828f..80b8134827 100644 --- a/docs/source/api-testing.rst +++ b/docs/source/api-testing.rst @@ -315,13 +315,13 @@ You can create dummy identities to use in test transactions using the ``TestIden .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 14 :end-before: DOCEND 14 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 14 :end-before: DOCEND 14 @@ -368,13 +368,13 @@ construct and check transactions. .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 11 :end-before: DOCEND 11 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 11 :end-before: DOCEND 11 @@ -388,13 +388,13 @@ for you, using all the given identities. .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 12 :end-before: DOCEND 12 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 12 :end-before: DOCEND 12 @@ -411,13 +411,13 @@ transaction has been executed, and any ``attachments``, as shown in this example .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 13 :end-before: DOCEND 13 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 13 :end-before: DOCEND 13 @@ -434,13 +434,13 @@ verifying the message, there is also a ``fails`` method. .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 4 :end-before: DOCEND 4 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 4 :end-before: DOCEND 4 @@ -459,13 +459,13 @@ add the relevant output state and check that the contract verifies successfully, .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 5 :end-before: DOCEND 5 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 5 :end-before: DOCEND 5 @@ -476,13 +476,13 @@ and then return to the original, unmodified transaction. As in the following exa .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 7 :end-before: DOCEND 7 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 7 :end-before: DOCEND 7 @@ -500,13 +500,13 @@ be verified separately by placing a ``verifies`` or ``fails`` statement within .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 9 :end-before: DOCEND 9 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 9 :end-before: DOCEND 9 diff --git a/docs/source/api-transactions.rst b/docs/source/api-transactions.rst index 0dfd5863a9..3c286cc831 100644 --- a/docs/source/api-transactions.rst +++ b/docs/source/api-transactions.rst @@ -56,13 +56,13 @@ An input state is added to a transaction as a ``StateAndRef``, which combines: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 21 :end-before: DOCEND 21 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 21 :end-before: DOCEND 21 @@ -75,13 +75,13 @@ A ``StateRef`` uniquely identifies an input state, allowing the notary to mark i .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 20 :end-before: DOCEND 20 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 20 :end-before: DOCEND 20 @@ -111,13 +111,13 @@ obtained from a ``StateAndRef`` by calling the ``StateAndRef.referenced()`` meth .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 55 :end-before: DOCEND 55 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 55 :end-before: DOCEND 55 @@ -146,13 +146,13 @@ add them to the transaction directly: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 22 :end-before: DOCEND 22 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 22 :end-before: DOCEND 22 @@ -163,13 +163,13 @@ it on the input state: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 23 :end-before: DOCEND 23 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 23 :end-before: DOCEND 23 @@ -183,13 +183,13 @@ wrapping the output state in a ``StateAndContract``, which combines: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 47 :end-before: DOCEND 47 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 47 :end-before: DOCEND 47 @@ -204,13 +204,13 @@ A command is added to the transaction as a ``Command``, which combines: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 24 :end-before: DOCEND 24 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 24 :end-before: DOCEND 24 @@ -222,13 +222,13 @@ Attachments are identified by their hash: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 25 :end-before: DOCEND 25 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 25 :end-before: DOCEND 25 @@ -243,13 +243,13 @@ time, or be open at either end: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 26 :end-before: DOCEND 26 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 26 :end-before: DOCEND 26 @@ -259,13 +259,13 @@ We can also define a time window as an ``Instant`` plus/minus a time tolerance ( .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 42 :end-before: DOCEND 42 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 42 :end-before: DOCEND 42 @@ -275,13 +275,13 @@ Or as a start-time plus a duration: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 43 :end-before: DOCEND 43 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 43 :end-before: DOCEND 43 @@ -299,13 +299,13 @@ that will notarise the inputs and verify the time-window: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 19 :end-before: DOCEND 19 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 19 :end-before: DOCEND 19 @@ -318,13 +318,13 @@ instantiated without one: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 46 :end-before: DOCEND 46 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 46 :end-before: DOCEND 46 @@ -362,13 +362,13 @@ Here's an example usage of ``TransactionBuilder.withItems``: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 27 :end-before: DOCEND 27 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 27 :end-before: DOCEND 27 @@ -380,13 +380,13 @@ Here are the methods for adding inputs and attachments: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 28 :end-before: DOCEND 28 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 28 :end-before: DOCEND 28 @@ -396,13 +396,13 @@ An output state can be added as a ``ContractState``, contract class name and not .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 49 :end-before: DOCEND 49 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 49 :end-before: DOCEND 49 @@ -412,13 +412,13 @@ We can also leave the notary field blank, in which case the transaction's defaul .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 50 :end-before: DOCEND 50 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 50 :end-before: DOCEND 50 @@ -428,13 +428,13 @@ Or we can add the output state as a ``TransactionState``, which already specifie .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 51 :end-before: DOCEND 51 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 51 :end-before: DOCEND 51 @@ -444,13 +444,13 @@ Commands can be added as a ``Command``: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 52 :end-before: DOCEND 52 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 52 :end-before: DOCEND 52 @@ -460,13 +460,13 @@ Or as ``CommandData`` and a ``vararg PublicKey``: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 53 :end-before: DOCEND 53 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 53 :end-before: DOCEND 53 @@ -476,13 +476,13 @@ For the time-window, we can set a time-window directly: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 44 :end-before: DOCEND 44 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 44 :end-before: DOCEND 44 @@ -492,13 +492,13 @@ Or define the time-window as a time plus a duration (e.g. 45 seconds): .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 45 :end-before: DOCEND 45 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 45 :end-before: DOCEND 45 @@ -512,13 +512,13 @@ We can either sign with our legal identity key: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 29 :end-before: DOCEND 29 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 29 :end-before: DOCEND 29 @@ -528,13 +528,13 @@ Or we can also choose to use another one of our public keys: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 30 :end-before: DOCEND 30 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 30 :end-before: DOCEND 30 @@ -572,13 +572,13 @@ and output states: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 33 :end-before: DOCEND 33 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 33 :end-before: DOCEND 33 @@ -597,13 +597,13 @@ We achieve this by using the ``ServiceHub`` to convert the ``SignedTransaction`` .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 32 :end-before: DOCEND 32 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 32 :end-before: DOCEND 32 @@ -613,13 +613,13 @@ We can now perform our additional verification. Here's a simple example: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 34 :end-before: DOCEND 34 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 34 :end-before: DOCEND 34 @@ -634,13 +634,13 @@ We can verify that all the transaction's required signatures are present and val .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 35 :end-before: DOCEND 35 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 35 :end-before: DOCEND 35 @@ -652,13 +652,13 @@ which the signatures are allowed to be missing: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 36 :end-before: DOCEND 36 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 36 :end-before: DOCEND 36 @@ -669,13 +669,13 @@ public keys for which the signatures are allowed to be missing: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 54 :end-before: DOCEND 54 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 54 :end-before: DOCEND 54 @@ -689,13 +689,13 @@ We can also choose to simply verify the signatures that are present: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 37 :end-before: DOCEND 37 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 37 :end-before: DOCEND 37 @@ -713,13 +713,13 @@ We can sign using our legal identity key, as follows: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 38 :end-before: DOCEND 38 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 38 :end-before: DOCEND 38 @@ -729,13 +729,13 @@ Or we can choose to sign using another one of our public keys: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 39 :end-before: DOCEND 39 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 39 :end-before: DOCEND 39 @@ -747,13 +747,13 @@ We can do this with our legal identity key: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 40 :end-before: DOCEND 40 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 40 :end-before: DOCEND 40 @@ -763,13 +763,13 @@ Or using another one of our public keys: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 41 :end-before: DOCEND 41 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 41 :end-before: DOCEND 41 diff --git a/docs/source/api-vault-query.rst b/docs/source/api-vault-query.rst index 1b80d50dcf..a012b84ea7 100644 --- a/docs/source/api-vault-query.rst +++ b/docs/source/api-vault-query.rst @@ -166,6 +166,44 @@ An example of a custom query in Java is illustrated here: where an anonymous party does not resolve to an X500 name via the ``IdentityService``, no query results will ever be produced. For performance reasons, queries do not use ``PublicKey`` as search criteria. +Custom queries can be either case sensitive or case insensitive. They are defined via a ``Boolean`` as one of the function parameters of each operator function. By default each operator is case sensitive. + +An example of a case sensitive custom query operator is illustrated here: + +.. container:: codeset + + .. sourcecode:: kotlin + + val currencyIndex = PersistentCashState::currency.equal(USD.currencyCode, true) + +.. note:: The ``Boolean`` input of ``true`` in this example could be removed since the function will default to ``true`` when not provided. + +An example of a case insensitive custom query operator is illustrated here: + +.. container:: codeset + + .. sourcecode:: kotlin + + val currencyIndex = PersistentCashState::currency.equal(USD.currencyCode, false) + +An example of a case sensitive custom query operator in Java is illustrated here: + +.. container:: codeset + + .. sourcecode:: java + + FieldInfo attributeCurrency = getField("currency", CashSchemaV1.PersistentCashState.class); + CriteriaExpression currencyIndex = Builder.equal(attributeCurrency, "USD", true); + +An example of a case insensitive custom query operator in Java is illustrated here: + +.. container:: codeset + + .. sourcecode:: java + + FieldInfo attributeCurrency = getField("currency", CashSchemaV1.PersistentCashState.class); + CriteriaExpression currencyIndex = Builder.equal(attributeCurrency, "USD", false); + Pagination ---------- The API provides support for paging where large numbers of results are expected (by default, a page size is set to 200 diff --git a/docs/source/blob-inspector.rst b/docs/source/blob-inspector.rst index 90c3e96976..fe58b63a3e 100644 --- a/docs/source/blob-inspector.rst +++ b/docs/source/blob-inspector.rst @@ -90,3 +90,25 @@ Here's what a node-info file from the node's data directory may look like: Notice the file is actually a serialised ``SignedNodeInfo`` object, which has a ``raw`` property of type ``SerializedBytes<NodeInfo>``. This property is materialised into a ``NodeInfo`` and is output under the ``deserialized`` field. + +Command-line options +~~~~~~~~~~~~~~~~~~~~ + +The blob inspector can be started with the following command-line options: + +.. code-block:: shell + + blob-inspector [-hvV] [--full-parties] [--install-shell-extensions] [--schema] + [--format=type] [--input-format=type] + [--logging-level=<loggingLevel>] [SOURCE] + +* ``--format=type``: Output format. Possible values: [YAML, JSON]. Default: YAML. +* ``--input-format=type``: Input format. If the file can't be decoded with the given value it's auto-detected, so you should + never normally need to specify this. Possible values [BINARY, HEX, BASE64]. Default: BINARY. +* ``--full-parties``: Display the owningKey and certPath properties of Party and PartyAndReference objects respectively. +* ``--schema``: Print the blob's schema first. +* ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file. +* ``--logging-level=<loggingLevel>``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO. +* ``--install-shell-extensions``: Install ``blob-inspector`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. +* ``--help``, ``-h``: Show this help message and exit. +* ``--version``, ``-V``: Print version information and exit. \ No newline at end of file diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 403b1155ba..78e75bd607 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,7 +6,20 @@ release, see :doc:`upgrade-notes`. Unreleased ---------- -* Removed experimental feature `CordformDefinition` + +* Introduce minimum and target platform version for CorDapps. + +* New overload for ``CordaRPCClient.start()`` method allowing to specify target legal identity to use for RPC call. + +* Case insensitive vault queries can be specified via a boolean on applicable SQL criteria builder operators. By default queries will be case sensitive. + +* Getter added to ``CordaRPCOps`` for the node's network parameters. + +* The RPC client library now checks at startup whether the server is of the client libraries major version or higher. + Therefore to connect to a Corda 4 node you must use version 4 or lower of the library. This behaviour can be overridden + by specifying a lower number in the ``CordaRPCClientConfiguration`` class. + +* Removed experimental feature ``CordformDefinition`` * Vault query fix: support query by parent classes of Contract State classes (see https://github.com/corda/corda/issues/3714) @@ -17,7 +30,7 @@ Unreleased * Introduced ``TestCorDapp`` and utilities to support asymmetric setups for nodes through ``DriverDSL``, ``MockNetwork`` and ``MockServices``. -* Change type of the `checkpoint_value` column. Please check the upgrade-notes on how to update your database. +* Change type of the ``checkpoint_value`` column. Please check the upgrade-notes on how to update your database. * Removed buggy :serverNameTablePrefix: configuration. @@ -94,6 +107,9 @@ Unreleased * ``WireTransaction.Companion.createComponentGroups`` has been marked as ``@CordaInternal``. It was never intended to be public and was already internal for Kotlin code. +* RPC server will now mask internal errors to RPC clients if not in devMode. ``Throwable``s implementing ``ClientRelevantError`` + will continue to be propagated to clients. + * RPC Framework moved from Kryo to the Corda AMQP implementation [Corda-847]. This completes the removal of ``Kryo`` from general use within Corda, remaining only for use in flow checkpointing. @@ -144,7 +160,7 @@ Unreleased Values are: [FAIL, WARN, IGNORE], default to FAIL if unspecified. * Introduced a placeholder for custom properties within ``node.conf``; the property key is "custom". * The deprecated web server now has its own ``web-server.conf`` file, separate from ``node.conf``. - * Property keys with double quotes (e.g. `"key"`) in ``node.conf`` are no longer allowed, for rationale refer to :doc:`corda-configuration-file`. + * Property keys with double quotes (e.g. "key") in ``node.conf`` are no longer allowed, for rationale refer to :doc:`corda-configuration-file`. * Added public support for creating ``CordaRPCClient`` using SSL. For this to work the node needs to provide client applications a certificate to be added to a truststore. See :doc:`tutorial-clientrpc-api` @@ -161,7 +177,7 @@ Unreleased * The whitelist.txt file is no longer needed. The existing network parameters file is used to update the current contracts whitelist. - * The CorDapp jars are also copied to each nodes' `cordapps` directory. + * The CorDapp jars are also copied to each nodes' ``cordapps`` directory. * Errors thrown by a Corda node will now reported to a calling RPC client with attention to serialization and obfuscation of internal data. @@ -171,7 +187,7 @@ Unreleased reference to the outer class) as per the Java documentation `here <https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html>`_ we are disallowing this as the paradigm in general makes little sense for contract states. -* Node can be shut down abruptly by ``shutdown`` function in `CordaRPCOps` or gracefully (draining flows first) through ``gracefulShutdown`` command from shell. +* Node can be shut down abruptly by ``shutdown`` function in ``CordaRPCOps`` or gracefully (draining flows first) through ``gracefulShutdown`` command from shell. * API change: ``net.corda.core.schemas.PersistentStateRef`` fields (index and txId) are now non-nullable. The fields were always effectively non-nullable - values were set from non-nullable fields of other objects. @@ -185,8 +201,8 @@ Unreleased * Table name with a typo changed from ``NODE_ATTCHMENTS_CONTRACTS`` to ``NODE_ATTACHMENTS_CONTRACTS``. -* Node logs a warning for any ``MappedSchema`` containing a JPA entity referencing another JPA entity from a different ``MappedSchema`. - The log entry starts with `Cross-reference between MappedSchemas.`. +* Node logs a warning for any ``MappedSchema`` containing a JPA entity referencing another JPA entity from a different ``MappedSchema``. + The log entry starts with "Cross-reference between MappedSchemas". API: Persistence documentation no longer suggests mapping between different schemas. * Upgraded Artemis to v2.6.2. @@ -206,7 +222,7 @@ Version 3.1 * Update the fast-classpath-scanner dependent library version from 2.0.21 to 2.12.3 .. note:: Whilst this is not the latest version of this library, that being 2.18.1 at time of writing, versions - later than 2.12.3 (including 2.12.4) exhibit a different issue. +later than 2.12.3 (including 2.12.4) exhibit a different issue. * Updated the api scanner gradle plugin to work the same way as the version in master. These changes make the api scanner more accurate and fix a couple of bugs, and change the format of the api-current.txt file slightly. Backporting these changes @@ -1024,15 +1040,15 @@ Special thank you to `Qian Hong <https://github.com/fracting>`_, `Marek Skocovsk to Corda in M10. .. warning:: Due to incompatibility between older version of IntelliJ and gradle 3.4, you will need to upgrade Intellij - to 2017.1 (with kotlin-plugin v1.1.1) in order to run Corda demos in IntelliJ. You can download the latest IntelliJ +to 2017.1 (with kotlin-plugin v1.1.1) in order to run Corda demos in IntelliJ. You can download the latest IntelliJ from `JetBrains <https://www.jetbrains.com/idea/download/>`_. .. warning:: The Kapt-generated models are no longer included in our codebase. If you experience ``unresolved references`` - errors when building in IntelliJ, please rebuild the schema model by running ``gradlew kaptKotlin`` in Windows or +errors when building in IntelliJ, please rebuild the schema model by running ``gradlew kaptKotlin`` in Windows or ``./gradlew kaptKotlin`` in other systems. Alternatively, perform a full gradle build or install. .. note:: Kapt is used to generate schema model and entity code (from annotations in the codebase) using the Kotlin Annotation - processor. +processor. * Corda DemoBench: * DemoBench is a new tool to make it easy to configure and launch local Corda nodes. A very useful tool to demonstrate diff --git a/docs/source/cipher-suites.rst b/docs/source/cipher-suites.rst index d923fdbdee..13a4bf9ae7 100644 --- a/docs/source/cipher-suites.rst +++ b/docs/source/cipher-suites.rst @@ -73,7 +73,7 @@ are compatible with TLS 1.2, while the default scheme per key type is also shown | | and SHA-256 | | vendors. | | - tls | | | | | - network map (CN) | +-------------------------+---------------------------------------------------------------+-----+-------------------------+ -| | ECDSA using the | | secp256k1 is the curve adopted by Bitcoin and as such there | YES | | +| | ECDSA using the | | secp256k1 is the curve adopted by Bitcoin and as such there | NO | | | | Koblitz k1 curve | | is a wealth of infrastructure, code and advanced algorithms | | | | | (secp256k1) | | designed for use with it. This curve is standardised by | | | | | and SHA-256 | | NIST as part of the "Suite B" cryptographic algorithms and | | | diff --git a/docs/source/cli-application-shell-extensions.rst b/docs/source/cli-application-shell-extensions.rst new file mode 100644 index 0000000000..4c81ea6c07 --- /dev/null +++ b/docs/source/cli-application-shell-extensions.rst @@ -0,0 +1,76 @@ +Shell extensions for CLI Applications +===================================== + +.. _installing-shell-extensions: + +Installing shell extensions +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Users of ``bash`` or ``zsh`` can install an alias and auto-completion for Corda applications that contain a command line interface. Run: + +.. code-block:: shell + + java -jar <name-of-JAR>.jar --install-shell-extensions + +Then, either restart your shell, or for ``bash`` users run: + +.. code-block:: shell + + . ~/.bashrc + +Or, for ``zsh`` run: + +.. code-block:: shell + + . ~/.zshrc + +You will now be able to run the command line application from anywhere by running the following: + +.. code-block:: shell + + <alias> --<option> + +For example, for the Corda node, install the shell extensions using + +.. code-block:: shell + + java -jar corda-<version>.jar --install-shell-extensions + +And then run the node by running: + +.. code-block:: shell + + corda --<option> + +Upgrading shell extensions +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Once the shell extensions have been installed, you can upgrade them in one of two ways. + +1) Overwrite the existing JAR with the newer version. The next time you run the application, it will automatically update + the completion file. Either restart the shell or see :ref:`above<installing-shell-extensions>` for instructions + on making the changes take effect immediately. +2) If you wish to use a new JAR from a different directory, navigate to that directory and run: + + .. code-block:: shell + + java -jar <name-of-JAR> + + Which will update the alias to point to the new location, and update command line completion functionality. Either + restart the shell or see :ref:`above<installing-shell-extensions>` for instructions on making the changes take effect immediately. + +List of existing CLI applications +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++----------------------------------------------------------------+--------------------------------------------------------------+--------------------------------+ +| Description | JAR name | Alias | ++----------------------------------------------------------------+--------------------------------------------------------------+--------------------------------+ +| :ref:`Corda node<starting-an-individual-corda-node>` | ``corda-<version>.jar`` | ``corda --<option>`` | ++----------------------------------------------------------------+--------------------------------------------------------------+--------------------------------+ +| :doc:`Network bootstrapper<network-bootstrapper>` | ``corda-tools-network-bootstrapper-<version>.jar`` | ``bootstrapper --<option>`` | ++----------------------------------------------------------------+--------------------------------------------------------------+--------------------------------+ +| :ref:`Standalone shell<standalone-shell>` | ``corda-tools-shell-cli-<version>.jar`` | ``corda-shell --<option>`` | ++----------------------------------------------------------------+--------------------------------------------------------------+--------------------------------+ +| :doc:`Blob inspector<blob-inspector>` | ``corda-tools-blob-inspector-<version>.jar`` | ``blob-inspector --<option>`` | ++----------------------------------------------------------------+--------------------------------------------------------------+--------------------------------+ + diff --git a/docs/source/cli-ux-guidelines.rst b/docs/source/cli-ux-guidelines.rst new file mode 100644 index 0000000000..a4917b1620 --- /dev/null +++ b/docs/source/cli-ux-guidelines.rst @@ -0,0 +1,144 @@ +.. highlight:: kotlin +.. raw:: html + + <script type="text/javascript" src="_static/jquery.js"></script> + <script type="text/javascript" src="_static/codesets.js"></script> + +CLI UX Guide +============ + +Command line options +-------------------- + +Command line utilities should use picocli (http://picocli.info) to provide a unified interface and follow the conventions in the picocli documentation, some of the more important of which are repeated below. + +Option names +~~~~~~~~~~~~ + +* Options should be specified on the command line using a double dash, e.g. ``--parameter``. +* Options that consist of multiple words should be separated via hyphens e.g. ``--my-multiple-word-parameter-name``. + +Short names +~~~~~~~~~~~ + +* Where possible a POSIX style short option should be provided for ease of use (see http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_02). + + * These should be prefixed with a single hyphen. + * For example ``-V`` for ``--verbose``, ``-d`` for ``--dev-mode``. + * Consider adding short options for commands that would be ran regularly as part of troubleshooting/operational processes. + * Short options should not be used for commands that would be used just once, for example initialising/registration type tasks. + +* The picocli interface allows combinations of options without parameters, for example, ```-v`` and ```-d`` can be combined as ``-vd``. + +Positional parameters +~~~~~~~~~~~~~~~~~~~~~ + +* Parameters specified without an option should ideally all be part of a list. + + * For example, in ``java -jar test.jar file1 file2 file3``, the parameters file1, file2 and file3 should be a list of files that are all acted on together (e.g. a list of CorDapps). + +* Avoid using positional parameters to mean different things, which involves someone remembering in which order things need to be specified. + + * For example, avoid ``java -jar test.jar configfile1 cordapp1 cordapp2`` where parameter 1 is the config file and any subsequent parameters are the CorDapps. + * Use ``java -jar test.jar cordapp1 cordapp2 --config-file configfile1`` instead. + +Standard options +~~~~~~~~~~~~~~~~ + +* A ``--help`` option should be provided which details all possible options with a brief description and any short name equivalents. A ``-h`` short option should also be provided. +* A ``--version`` option that should output the version number of the software. A ``-V`` short option should also be provided. +* A ``--logging-level`` option should be provided which specifies the logging level to be used in any logging files. Acceptable values should be ``DEBUG``, ``TRACE``, ``INFO``, ``WARN`` and ``ERROR``. +* ``--verbose`` and ``--log-to-console`` options should be provided (both equivalent) which specifies that logging output should be displayed in the console. + A ``-v`` short option should also be provided. +* A ``--install-shell-extensions`` option should be provided that creates and installs a bash completion file. + + +Defaults +~~~~~~~~ + +* Flags should have sensible defaults. +* Boolean flags should always default to false. Specifying the flag without a parameter should set it to true. For example ``--use-something` should be equal to ``--use-something=true`` and no option should be equal to ``--my-flag=false``. +* Do a bit of work to figure out reasonable defaults. Nobody likes having to set a dozen flags before the tool will cooperate. + +Adding a new option +~~~~~~~~~~~~~~~~~~~ + +* Boolean options should start with is, has or with. For example, ``--is-cheesy``, ``--with-cheese``, ``--has-cheese-on``. +* Any new options must be documented in the docsite and via the ``--help`` screen. +* Never use acronyms in option names and try and make them as descriptive as possible. + +Parameter stability +~~~~~~~~~~~~~~~~~~~ + +* Avoid removing parameters. If, for some reason, a parameter needs to be renamed, add a new parameter with the new name and deprecate the old parameter, or alternatively keep both versions of the parameter. + + +The ``CordaCliWrapper`` base class +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``CordaCliWrapper`` base class from the ``cliutils`` module should be used as a base where practicable, this will provide a set of default options out of the box. +In order to use it, create a class containing your command line options using the syntax provided at (see the `picocli<https://picocli.info/>`_ website for more information) + + +.. container:: codeset + + .. sourcecode:: kotlin + + import net.corda.cliutils.ExitCodes + import net.corda.cliutils.CordaCliWrapper + + class UsefulUtilityExitCodes: ExitCodes { + companion object { + val APPLICATION_SPECIFIC_ERROR_CODE: Int = 100 + } + } + + class UsefulUtility : CordaCliWrapper( + "useful-utility", // the alias to be used for this utility in bash. When --install-shell-extensions is run + // you will be able to invoke this command by running <useful-utility --opts> from the command line + "A command line utility that is super useful!" // A description of this utility to be displayed when --help is run + ) { + @Option(names = ["--extra-usefulness", "-e"], // A list of the different ways this option can be referenced + description = ["Use this option to add extra usefulness"] // Help description to be displayed for this option + ) + private var extraUsefulness: Boolean = false // This default option will be shown in the help output + + override fun runProgram(): Int { // override this function to run the actual program + try { + // do some stuff + } catch (KnownException: ex) { + return UsefulUtilityExitCodes.APPLICATION_SPECIFIC_ERROR_CODE // return a special exit code for known exceptions + } + + return UsefulUtilityExitCodes.SUCCESS // this is the exit code to be returned to the system inherited from the ExitCodes base class + } + } + + +Then in your ``main()`` method: + +.. container:: codeset + + .. sourcecode:: kotlin + + import net.corda.cliutils.start + + fun main(args: Array<String>) { + UsefulUtility().start(args) + } + + + +Application behavior +-------------------- + +* Set exit codes using exitProcess. + + * Zero means success. + * Other numbers mean errors. + +* Setting a unique error code (starting from 1) for each thing that can conceivably break makes your tool shell-scripting friendly. +* Make sure all exit codes are documented with recommended remedies where applicable. +* Your ``--help`` text or other docs should ideally include examples. Writing examples is also a good way to find out if your program requires a dozen flags to do anything. +* Don’t print logging output to the console unless the user requested it via a ``-–verbose`` flag (conventionally shortened to ``-v``). Logs should be either suppressed or saved to a text file during normal usage, except for errors, which are always OK to print. +* Don't print stack traces to the console. Stack traces can be added to logging files, but the user should see as meaningful error description as possible. \ No newline at end of file diff --git a/docs/source/clientrpc.rst b/docs/source/clientrpc.rst index a2db1469a9..7390f69a55 100644 --- a/docs/source/clientrpc.rst +++ b/docs/source/clientrpc.rst @@ -18,8 +18,8 @@ object as normal, and the marshalling back and forth is handled for you. .. warning:: The built-in Corda webserver is deprecated and unsuitable for production use. If you want to interact with your node via HTTP, you will need to stand up your own webserver, then create an RPC connection between your node - and this webserver using the `CordaRPCClient`_ library. You can find an example of how to do this - `here <https://github.com/corda/spring-webserver>`_. + and this webserver using the `CordaRPCClient`_ library. You can find an example of how to do this using the popular + Spring Boot server `here <https://github.com/corda/spring-webserver>`_. Connecting to a node via RPC ---------------------------- @@ -31,12 +31,12 @@ Here is an example of using `CordaRPCClient`_ to connect to a node and log the c .. container:: codeset - .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcExample.kt + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/ClientRpcExample.kt :language: kotlin :start-after: START 1 :end-before: END 1 - .. literalinclude:: example-code/src/main/java/net/corda/docs/ClientRpcExampleJava.java + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/ClientRpcExample.java :language: java :start-after: START 1 :end-before: END 1 @@ -291,31 +291,43 @@ would expect. This feature comes with a cost: the server must queue up objects emitted by the server-side observable until you download them. Note that the server side observation buffer is bounded, once it fills up the client is considered -slow and kicked. You are expected to subscribe to all the observables returned, otherwise client-side memory starts -filling up as observations come in. If you don't want an observable then subscribe then unsubscribe immediately to -clear the client-side buffers and to stop the server from streaming. If your app quits then server side resources -will be freed automatically. +slow and will be disconnected. You are expected to subscribe to all the observables returned, otherwise client-side +memory starts filling up as observations come in. If you don't want an observable then subscribe then unsubscribe +immediately to clear the client-side buffers and to stop the server from streaming. For Kotlin users there is a +convenience extension method called ``notUsed()`` which can be called on an observable to automate this step. + +If your app quits then server side resources will be freed automatically. .. warning:: If you leak an observable on the client side and it gets garbage collected, you will get a warning printed to the logs and the observable will be unsubscribed for you. But don't rely on this, as garbage collection - is non-deterministic. + is non-deterministic. If you set ``-Dnet.corda.client.rpc.trackRpcCallSites=true`` on the JVM command line then + this warning comes with a stack trace showing where the RPC that returned the forgotten observable was called from. + This feature is off by default because tracking RPC call sites is moderately slow. .. note:: Observables can only be used as return arguments of an RPC call. It is not currently possible to pass - Observables as parameters to the RPC methods. + Observables as parameters to the RPC methods. In other words the streaming is always server to client and not + the other way around. Futures ------- A method can also return a ``CordaFuture`` in its object graph and it will be treated in a similar manner to -observables. Calling the ``cancel`` method on the future will unsubscribe it from any future value and release any resources. +observables. Calling the ``cancel`` method on the future will unsubscribe it from any future value and release +any resources. Versioning ---------- -The client RPC protocol is versioned using the node's Platform Version (see :doc:`versioning`). When a proxy is created +The client RPC protocol is versioned using the node's platform version number (see :doc:`versioning`). When a proxy is created the server is queried for its version, and you can specify your minimum requirement. Methods added in later versions are tagged with the ``@RPCSinceVersion`` annotation. If you try to use a method that the server isn't advertising support of, an ``UnsupportedOperationException`` is thrown. If you want to know the version of the server, just use the ``protocolVersion`` property (i.e. ``getProtocolVersion`` in Java). +The RPC client library defaults to requiring the platform version it was built with. That means if you use the client +library released as part of Corda N, then the node it connects to must be of version N or above. This is checked when +the client first connects. If you want to override this behaviour, you can alter the ``minimumServerProtocolVersion`` +field in the ``CordaRPCClientConfiguration`` object passed to the client. Alternatively, just link your app against +an older version of the library. + Thread safety ------------- A proxy is thread safe, blocking, and allows multiple RPCs to be in flight at once. Any observables that are returned and @@ -326,9 +338,15 @@ Error handling -------------- If something goes wrong with the RPC infrastructure itself, an ``RPCException`` is thrown. If you call a method that requires a higher version of the protocol than the server supports, ``UnsupportedOperationException`` is thrown. -Otherwise, if the server implementation throws an exception, that exception is serialised and rethrown on the client +Otherwise the behaviour depends on the ``devMode`` node configuration option. + +In ``devMode``, if the server implementation throws an exception, that exception is serialised and rethrown on the client side as if it was thrown from inside the called RPC method. These exceptions can be caught as normal. +When not in ``devMode``, the server will mask exceptions not meant for clients and return an ``InternalNodeException`` instead. +This does not expose internal information to clients, strengthening privacy and security. CorDapps can have exceptions implement +``ClientRelevantError`` to allow them to reach RPC clients. + Connection management --------------------- It is possible to not be able to connect to the server on the first attempt. In that case, the ``CordaRPCClient.start()`` @@ -338,7 +356,6 @@ such situations: .. sourcecode:: Kotlin fun establishConnectionWithRetry(nodeHostAndPort: NetworkHostAndPort, username: String, password: String): CordaRPCConnection { - val retryInterval = 5.seconds do { @@ -382,7 +399,6 @@ on the ``Observable`` returned by ``CordaRPCOps``. .. sourcecode:: Kotlin fun performRpcReconnect(nodeHostAndPort: NetworkHostAndPort, username: String, password: String) { - val connection = establishConnectionWithRetry(nodeHostAndPort, username, password) val proxy = connection.proxy @@ -414,10 +430,6 @@ Client code if fed with instances of ``StateMachineInfo`` using call ``clientCod all the items. Some of these items might have already been delivered to client code prior to failover occurred. It is down to client code in this case handle those duplicate items as appropriate. -Wire protocol -------------- -The client RPC wire protocol is defined and documented in ``net/corda/client/rpc/RPCApi.kt``. - Wire security ------------- ``CordaRPCClient`` has an optional constructor parameter of type ``ClientRpcSslOptions``, defaulted to ``null``, which allows @@ -430,7 +442,6 @@ In order for this to work, the client needs to provide a truststore containing a For the communication to be secure, we recommend using the standard SSL best practices for key management. - Whitelisting classes with the Corda node ---------------------------------------- CorDapps must whitelist any classes used over RPC with Corda's serialization framework, unless they are whitelisted by diff --git a/docs/source/contributing-flow-state-machines.rst b/docs/source/contributing-flow-state-machines.rst index 7f5a9a37c8..c1736288a5 100644 --- a/docs/source/contributing-flow-state-machines.rst +++ b/docs/source/contributing-flow-state-machines.rst @@ -28,7 +28,7 @@ implement ``FlowAsyncOperation``: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt :language: kotlin :start-after: DOCSTART SummingOperation :end-before: DOCEND SummingOperation @@ -62,7 +62,7 @@ flow: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt :language: kotlin :start-after: DOCSTART ExampleSummingFlow :end-before: DOCEND ExampleSummingFlow @@ -104,7 +104,7 @@ Let's assume we made a mistake in our summing operation: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt :language: kotlin :start-after: DOCSTART SummingOperationThrowing :end-before: DOCEND SummingOperationThrowing diff --git a/docs/source/contributing-index.rst b/docs/source/contributing-index.rst index 1187c1af22..67a3b10f7c 100644 --- a/docs/source/contributing-index.rst +++ b/docs/source/contributing-index.rst @@ -12,6 +12,7 @@ of contributing to Corda. building-corda testing codestyle + cli-ux-guidelines building-the-docs api-scanner contributing-flow-state-machines \ No newline at end of file diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst index 6e4516dd05..5ac581af65 100644 --- a/docs/source/contributing.rst +++ b/docs/source/contributing.rst @@ -65,20 +65,7 @@ make merging more complicated. Things to consider when writing CLI apps ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -* Set exit codes using ``exitProcess``. Zero means success. Other numbers mean errors. Setting a unique error code - (starting from 1) for each thing that can conceivably break makes your tool shell-scripting friendly - -* Do a bit of work to figure out reasonable defaults. Nobody likes having to set a dozen flags before the tool will - cooperate - -* Your ``--help`` text or other docs should ideally include examples. Writing examples is also a good way to find out - that your program requires a dozen flags to do anything - -* Flags should have sensible defaults - -* Don’t print logging output to the console unless the user requested it via a ``–verbose`` flag (conventionally - shortened to ``-v``) or a ``–log-to-console`` flag. Logs should be either suppressed or saved to a text file during - normal usage, except for errors, which are always OK to print +Make sure any changes to CLI applications conform to the :doc:`cli-ux-guidelines`. Testing the changes ------------------- diff --git a/docs/source/corda-api.rst b/docs/source/corda-api.rst index 9e79e9d8a7..84c243173b 100644 --- a/docs/source/corda-api.rst +++ b/docs/source/corda-api.rst @@ -26,21 +26,8 @@ Before reading this page, you should be familiar with the :doc:`key concepts of Internal APIs and stability guarantees -------------------------------------- -.. warning:: For Corda 1.0 we do not currently provide a stable wire protocol or support for database upgrades. - Additionally, the JSON format produced by the client-jackson module may change in future. - Therefore, you should not expect to be able to migrate persisted data from 1.0 to future versions. - - Additionally, it may be necessary to recompile applications against future versions of the API until we begin offering - ABI stability as well. We plan to do this soon after the release of Corda 1.0. - - Finally, please note that the 1.0 release has not yet been security audited. You should not run it in situations - where security is required. - -Corda artifacts can be required from Java 9 Jigsaw modules. -From within a ``module-info.java``, you can reference one of the modules e.g., ``requires net.corda.core;``. - -.. warning:: while Corda artifacts can be required from ``module-info.java`` files, they are still not proper Jigsaw modules, - because they rely on the automatic module mechanism and declare no module descriptors themselves. We plan to integrate Jigsaw more thoroughly in the future. +Corda makes certain commitments about what parts of the API will preserve backwards compatibility as they change and +which will not. Over time, more of the API will fall under the stability guarantees. Corda stable modules -------------------- diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index d21076eed4..8c6e003ee0 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -96,6 +96,9 @@ absolute path to the node's base directory. note that the host is the included as the advertised entry in the network map. As a result the value listed here must be externally accessible when running nodes across a cluster of machines. If the provided host is unreachable, the node will try to auto-discover its public one. + +:additionalP2PAddresses: An array of additional host:port values, which will be included in the advertised NodeInfo in the network map in addition to the ``p2pAddress``. + Nodes can use this configuration option to advertise HA endpoints and aliases to external parties. If not specified the default value is an empty list. :flowTimeout: When a flow implementing the ``TimedFlow`` interface does not complete in time, it is restarted from the initial checkpoint. Currently only used for notarisation requests: if a notary replica dies while processing a notarisation request, diff --git a/docs/source/corda-networks-index.rst b/docs/source/corda-networks-index.rst index 296d92738d..67f544d69c 100644 --- a/docs/source/corda-networks-index.rst +++ b/docs/source/corda-networks-index.rst @@ -4,6 +4,7 @@ Networks .. toctree:: :maxdepth: 1 + joining-a-network setting-up-a-corda-network running-a-notary permissioning diff --git a/docs/source/cordapp-build-systems.rst b/docs/source/cordapp-build-systems.rst index 7d3a4a8cb7..af785ae777 100644 --- a/docs/source/cordapp-build-systems.rst +++ b/docs/source/cordapp-build-systems.rst @@ -189,3 +189,30 @@ CorDapp configuration can be accessed from ``CordappContext::config`` whenever a There is an example project that demonstrates in ``samples`` called ``cordapp-configuration`` and API documentation in `<api/kotlin/corda/net.corda.core.cordapp/index.html>`_. + + +Minimum and target platform version +----------------------------------- + +CorDapps can advertise their minimum and target platform version. The minimum platform version indicates that a node has to run at least this version in order to be able to run this CorDapp. The target platform version indicates that a CorDapp was tested with this version of the Corda Platform and should be run at this API level if possible. It provides a means of maintaining behavioural compatibility for the cases where the platform's behaviour has changed. These attributes are specified in the JAR manifest of the CorDapp, for example: + +.. sourcecode:: groovy + + 'Min-Platform-Version': 3 + 'Target-Platform-Version': 4 + + +In gradle, this can be achieved by modifying the jar task as shown in this example: + +.. container:: codeset + + .. sourcecode:: groovy + + jar { + manifest { + attributes( + 'Min-Platform-Version': 3 + 'Target-Platform-Version': 4 + ) + } + } diff --git a/docs/source/deploying-a-node.rst b/docs/source/deploying-a-node.rst index 7071466df8..81e6854286 100644 --- a/docs/source/deploying-a-node.rst +++ b/docs/source/deploying-a-node.rst @@ -16,10 +16,11 @@ handling, and ensures the Corda service is run at boot. * Oracle Java 8. The supported versions are listed in :doc:`getting-set-up` -1. Add a system user which will be used to run Corda: +1. As root/sys admin user - add a system user which will be used to run Corda: ``sudo adduser --system --no-create-home --group corda`` + 2. Create a directory called ``/opt/corda`` and change its ownership to the user you want to use to run Corda: ``mkdir /opt/corda; chown corda:corda /opt/corda`` diff --git a/docs/source/design/data-model-upgrades/signature-constraints.md b/docs/source/design/data-model-upgrades/signature-constraints.md index 67f5de1b95..09bbeef5f1 100644 --- a/docs/source/design/data-model-upgrades/signature-constraints.md +++ b/docs/source/design/data-model-upgrades/signature-constraints.md @@ -4,7 +4,7 @@ This design document outlines an additional kind of *contract constraint*, used ## Background -Contract constraints are a part of how Corda manages application upgrades. There are two kinds of upgrade that can be applied to the ledger: +Contract constraints are a part of how Corda ensures the correct code is executed to verify transactions, and also how it manages application upgrades. There are two kinds of upgrade that can be applied to the ledger: * Explicit * Implicit @@ -31,21 +31,20 @@ We would like a new kind of constraint that is more convenient and decentralised ## Goals * Improve usability by eliminating the need to change the network parameters. - * Improve decentralisation by allowing apps to be developed and upgraded without the zone operator knowing or being able to influence it. - * Eventually, phase out zone whitelisting constraints. - ## Non-goals * Preventing downgrade attacks. Downgrade attack prevention will be tackled in a different design effort. * Phase out of hash constraints. If malicious app creators are in the users threat model then hash constraints are the way to go. * Handling the case where third parties re-sign app jars. +* Package namespace ownership (a separate effort). +* Allowing the zone operator to override older constraints, to provide a non-explicit upgrade path. ## Design details -We propose being able to constrain to any attachments signed by a specified set of keys. +We propose being able to constrain to any attachments whose files are signed by a specified set of keys. This satisfies the usability requirement because the creation of a new application is as simple as invoking the `jarsigner` tool that comes with the JDK. This can be integrated with the build system via a Gradle or Maven task. For example, Gradle can use jarsigner via [the signjar task](https://ant.apache.org/manual/Tasks/signjar.html) ([example](https://gist.github.com/Lien/7150434)). @@ -87,7 +86,7 @@ The `TransactionBuilder` class can select the right constraint given what it alr ### Tooling and workflow -The primary tool required is of course `jarsigner`. In dev and integration test modes, the node will ignore missing signatures in attachment JARs and will simply log a warning if no signature is present. +The primary tool required is of course `jarsigner`. In dev mode, the node will ignore missing signatures in attachment JARs and will simply log an error if no signature is present when a constraint requires one. To verify and print information about the signatures on a JAR, the `jarsigner` tool can be used again. In addition, we should add some new shell commands that do the same thing, but for a given attachment hash or transaction hash - these may be useful for debugging and analysis. Actually a new shell command should cover all aspects of inspecting attachments - not just signatures but what's inside them, simple way to save them to local disk etc. diff --git a/docs/source/design/targetversion/design.md b/docs/source/design/targetversion/design.md new file mode 100644 index 0000000000..a0de10d1db --- /dev/null +++ b/docs/source/design/targetversion/design.md @@ -0,0 +1,90 @@ +# CorDapp Minimum and Target Platform Version + +## Overview + +We want to give CorDapps the ability to specify which versions of the platform they support. This will make it easier for CorDapp developers to support multiple platform versions, and enable CorDapp developers to tweak behaviour and opt in to changes that might be breaking (e.g. sandboxing). Corda developers gain the ability to introduce changes to the implementation of the API that would otherwise break existing CorDapps. + +This document proposes that CorDapps will have metadata associated with them specifying a minimum platform version and a target platform Version. The minimum platform version of a CorDapp would indicate that a Corda node would have to be running at least this version of the Corda platform in order to be able to run this CorDapp. The target platform version of a CorDapp would indicate that it was tested for this version of the Corda platform. + +## Background + +> Introduce target version and min platform version as app attributes +> +> This is probably as simple as a couple of keys in a MANIFEST.MF file. +> We should document what it means, make sure API implementations can always access the target version of the calling CorDapp (i.e. by examining the flow, doing a stack walk or using Reflection.getCallerClass()) and do a simple test of an API that acts differently depending on the target version of the app. +> We should also implement checking at CorDapp load time that min platform version <= current platform version. + +([from CORDA-470](https://r3-cev.atlassian.net/browse/CORDA-470)) + +### Definitions + +* *Platform version (Corda)* An integer representing the API version of the Corda platform + +> It starts at 1 and will increment by exactly 1 for each release which changes any of the publicly exposed APIs in the entire platform. This includes public APIs on the node itself, the RPC system, messaging, serialisation, etc. API backwards compatibility will always be maintained, with the use of deprecation to migrate away from old APIs. In rare situations APIs may have to be removed, for example due to security issues. There is no relationship between the Platform Version and the release version - a change in the major, minor or patch values may or may not increase the Platform Version. + +([from the docs](https://docs.corda.net/head/versioning.html#versioning)). + +* *Platform version (Node)* The value of the Corda platform version that a node is running and advertising to the network. + +* *Minimum platform version (Network)* The minimum platform version that the nodes must run in order to be able to join the network. Set by the network zone operator. The minimum platform version is distributed with the network parameters as `minimumPlatformVersion`. + ([see docs:](https://docs.corda.net/network-map.html#network-parameters)) + +* *Target platform version (CorDapp)* Introduced in this document. Indicates that a CorDapp was tested with this version of the Corda Platform and should be run at this API level if possible. + +* *Minimum platform version (CorDapp)* Introduced in this document. Indicates the minimum version of the Corda platform that a Corda Node has to run in order to be able to run a CorDapp. + + +## Goals + +Define the semantics of target platform version and minimum platform version attributes for CorDapps, and the minimum platform version for the Corda network. Describe how target and platform versions would be specified by CorDapp developers. Define how these values can be accessed by the node and the CorDapp itself. + +## Non-goals + +In the future it might make sense to integrate the minimum and target versions into a Corda gradle plugin. Such a plugin is out of scope of this document. + +## Timeline + +This is intended as a long-term solution. The first iteration of the implementation will be part of platform version 4 and contain the minimum and target platform version. + +## Requirements + +* The CorDapp's minimum and target platform version must be accessible to nodes at CorDapp load time. + +* At CorDapp load time there should be a check that the node's platform version is greater or equal to the CorDapp's Minimum Platform version. + +* API implementations must be able to access the target version of the calling CorDapp. + +* The node's platform version must be accessible to CorDapps. + +* The CorDapp's target platform version must be accessible to the node when running CorDapps. + +## Design + +### Testing + +When a new platform version is released, CorDapp developers can increase their CorDapp's target version and re-test their app. If the tests are successful, they can then release their CorDapp with the increased target version. This way they would opt-in to potentially breaking changes that were introduced in that version. If they choose to keep their current target version, their CorDapp will continue to work. + +### Implications for platform developers + +When new features or changes are introduced that require all nodes on the network to understand them (e.g. changes in the wire transaction format), they must be version-gated on the network level. This means that the new behaviour should only take effect if the minimum platform version of the network is equal to or greater than the version in which these changes were introduced. Failing that, the old behaviour must be used instead. + +Changes that risk breaking apps must be gated on targetVersion>=X where X is the version where the change was made, and the old behaviour must be preserved if that condition isn't met. + +## Technical Design + +The minimum- and target platform version will be written to the manifest of the CorDapp's JAR, in fields called `Min-Platform-Version` and `Target-Platform-Version`. +The node's CorDapp loader reads these values from the manifest when loading the CorDapp. If the CorDapp's minimum platform version is greater than the node's platform version, the node will not load the CorDapp and log a warning. The CorDapp loader sets the minimum and target version in `net.corda.core.cordapp.Cordapp`, which can be obtained via the `CorDappContext` from the service hub. + +To make APIs caller-sensitive in cases where the service hub is not available a different approach has to be used. It would possible to do a stack walk, and parse the manifest of each class on the stack to determine if it belongs to a CorDapp, and if yes, what its target version is. Alternatively, the mapping of classes to `Cordapp`s obtained by the CorDapp loader could be stored in a global singleton. This singleton would expose a lambda returning the current CorDapp's version information (e.g. `() -> Cordapp.Info`). + +Let's assume that we want to change `TimeWindow.Between` to make it inclusive, i.e. change `contains(instant: Instant) = instant >= fromTime && instant < untilTime` to `contains(instant: Instant) = instant >= fromTime && instant <= untilTime`. However, doing so will break existing CorDapps. We could then version-guard the change such that the new behaviour is only used if the target version of the CorDapp calling `contains` is equal to or greater than the platform version that contains this change. It would look similar to this: + + ``` + fun contains(instant: Instant) { + if (CorDappVersionResolver.resolve().targetVersion > 42) { + return instant >= fromTime && instant <= untilTime + } else { + return instant >= fromTime && instant < untilTime + } + ``` +Version-gating API changes when the service hub is available would look similar to the above example, in that case the service hub's CorDapp provider would be used to determine if this code is being called from a CorDapp and to obtain its target version information. diff --git a/docs/source/design/threat-model/corda-threat-model.md b/docs/source/design/threat-model/corda-threat-model.md new file mode 100644 index 0000000000..83e5a1da1f --- /dev/null +++ b/docs/source/design/threat-model/corda-threat-model.md @@ -0,0 +1,429 @@ +<style>.wy-table-responsive table td, .wy-table-responsive table th { white-space: normal;}</style> +Corda Threat Model +================== + +This document describes the security threat model of the Corda Platform. The Corda Threat Model is the result of architectural and threat modelling sessions, +and is designed to provide a high level overview of the security objectives for the Corda Network , and the controls and mitigations used to deliver on those +objectives. It is intended to support subsequent analysis and architecture of systems connecting with the network and the applications which interact with data +across it. + +It is incumbent on all ledger network participants to review and assess the security measures described in this document against their specific organisational +requirements and policies, and to implement any additional measures needed. + +Scope +----- + +Built on the [Corda](http://www.corda.net/) distributed ledger platform designed by R3, the ledger network enables the origination and management of agreements +between business partners. Participants to the network create and maintain Corda *nodes,* each hosting one or more pluggable applications ( *CorDapps* ) which +define the data to be exchanged and its workflow. See the [Corda Technical White Paper](https://docs.corda.net/_static/corda-technical-whitepaper.pdf) for a +detailed description of Corda's design and functionality. + +R3 provide and maintain a number of essential services underpinning the ledger network. In the future these services are intended to be operated by a separate +Corda Foundation. The network services currently include: + +- Network Identity service ('Doorman'): Issues signed digital certificates that uniquely identity parties on the network. +- Network Map service: Provides a way for nodes to advertise their identity, and identify other nodes on the network, their network address and advertised + services. + +Participants to the ledger network include major institutions, financial organisations and regulated bodies, across various global jurisdictions. In a majority +of cases, there are stringent requirements in place for participants to demonstrate that their handling of all data is performed in an appropriately secure +manner, including the exchange of data over the ledger network. This document identifies measures within the Corda platform and supporting infrastructure to +mitigate key security risks in support of these requirements. + +The Corda Network +----------------- + +The diagram below illustrates the network architecture, protocols and high level data flows that comprise the Corda Network. The threat model has been developed +based upon this architecture. + + + +Threat Model +------------ + +Threat Modelling is an iterative process that works to identify, describe and mitigate threats to a system. One of the most common models for identifying +threats is the [STRIDE](https://en.wikipedia.org/wiki/STRIDE_(security)) framework. It provides a set of security threats in six categories: + +- Spoofing +- Tampering +- Information Disclosure +- Repudiation +- Denial of Service +- Elevation of Privilege + +The Corda threat model uses the STRIDE framework to present the threats to the Corda Network in a structured way. It should be stressed that threat modelling is +an iterative process that is never complete. The model described below is part of an on-going process intended to continually refine the security architecture +of the Corda platform. + +### Spoofing + +Spoofing is pretending to be something or someone other than yourself. It is the actions taken by an attacker to impersonate another party, typically for the +purposes of gaining unauthorised access to privileged data, or perpetrating fraudulent transactions. Spoofing can occur on multiple levels. Machines can be +impersonated at the network level by a variety of methods such as ARP & IP spoofing or DNS compromise. + +Spoofing can also occur at an application or user-level. Attacks at this level typically target authentication logic, using compromised passwords and +cryptographic keys, or by subverting cryptography systems. + +Corda employs a Public Key Infrastructure (PKI) to validate the identity of nodes, both at the point of registration with the network map service and +subsequently through the cryptographic signing of transactions. An imposter would need to acquire an organisation's private keys in order to meaningfully +impersonate that organisation. R3 provides guidance to all ledger network participants to ensure adequate security is maintained around cryptographic keys. + ++-------------+------------------------------------------------------------------------------+----------------------------------------------------------------+ +| Element | Attacks | Mitigations | ++=============+==============================================================================+================================================================+ +| RPC Client | An external attacker impersonates an RPC client and is able to initiate | The RPC Client is authenticated by the node and must supply | +| | flows on their behalf. | valid credentials (username & password). | +| | | | +| | A malicious RPC client connects to the node and impersonates another, | RPC Client permissions are configured by the node | +| | higher-privileged client on the same system, and initiates flows on their | administrator and can be used to restrict the actions and | +| | behalf. | flows available to the client. | +| | | | +| | **Impacts** | RPC credentials and permissions can be managed by an Apache | +| | | Shiro service. The RPC service restricts which actions are | +| | If successful, the attacker would be able to perform actions that they are | available to a client based on what permissions they have been | +| | not authorised to perform, such initiating flows. The impact of these | assigned. | +| | actions could have financial consequences depending on what flows were | | +| | available to the attacker. | | ++-------------+------------------------------------------------------------------------------+----------------------------------------------------------------+ +| Node | An attacker attempts to impersonate a node and issue a transaction using | Nodes must connect to each other using using | +| | their identity. | mutually-authenticated TLS connections. Node identity is | +| | | authenticated using the certificates exchanged as part of the | +| | An attacker attempts to impersonate another node on the network by | TLS protocol. Only the node that owns the corresponding | +| | submitting NodeInfo updates with falsified address and/or identity | private key can assert their true identity. | +| | information. | | +| | | NodeInfo updates contain the node's public identity | +| | **Impacts** | certificate and must be signed by the corresponding private | +| | | key. Only the node in possession of this private key can sign | +| | If successful, a node able to assume the identity of another party could | the NodeInfo. | +| | conduct fraudulent transactions (e.g. pay cash to its own identity), giving | | +| | a direct financial impact to the compromised identity. Demonstrating that | Corda employs a Public Key Infrastructure (PKI) to validate | +| | the actions were undertaken fraudulently could prove technically challenging | the identity of nodes. An imposter would need to acquire an | +| | to any subsequent dispute resolution process. | organisation's private keys in order to meaningfully | +| | | impersonate that organisation. Corda will soon support a range | +| | In addition, an impersonating node may be able to obtain privileged | of HSMs (Hardware Security Modules) for storing a node's | +| | information from other nodes, including receipt of messages intended for the | private keys, which mitigates this risk. | +| | original party containing information on new and historic transactions. | | ++-------------+------------------------------------------------------------------------------+----------------------------------------------------------------+ +| Network Map | An attacker with appropriate network access performs a DNS compromise, | Connections to the Network Map service are secured using the | +| | resulting in network traffic to the Doorman & Network Map being routed to | HTTPS protocol. The connecting node authenticates the | +| | their attack server, which attempts to impersonate these machines. | NetworkMap servers using their public certificates, to ensure | +| | | the identity of these servers is correct. | +| | **Impact** | | +| | | All data received from the NetworkMap is digitally signed (in | +| | Impersonation of the Network Map would enable an attacker to issue | addition to being protected by TLS) - an attacker attempting | +| | unauthorised updates to the map. | to spoof the Network Map would need to acquire both private | +| | | TLS keys, and the private NetworkMap signing keys. | +| | | | +| | | The Doorman and NetworkMap signing keys are stored inside a | +| | | (Hardware Security Module (HSM) with strict security controls | +| | | (network separation and physical access controls). | ++-------------+------------------------------------------------------------------------------+----------------------------------------------------------------+ +| Doorman | An malicious attacker operator attempts to join the Corda Network by | R3 operate strict validation procedures to ensure that | +| | impersonating an existing organisation and issues a fraudulent registration | requests to join the Corda Network have legitimately | +| | request. | originated from the organisation in question. | +| | | | +| | **Impact** | | +| | | | +| | The attacker would be able to join and impersonate an organisation. | | +| | | | +| | The operator could issue an identity cert for any organisation, publish a | | +| | valid NodeInfo and redirect all traffic to themselves in the clear. | | ++-------------+------------------------------------------------------------------------------+----------------------------------------------------------------+ + + + +### Tampering + +Tampering refers to the modification of data with malicious intent. This typically involves modification of data at rest (such as a file on disk, or fields in a +database), or modification of data in transit. + +To be successful, an attacker would require privileged access to some part of the network infrastructure (either public or internal private networks). They +might also have access to a node's file-system, database or even direct memory access. + ++------------+-----------------------------------------------------------------------------+------------------------------------------------------------------+ +| Element | Attacks | Mitigations | ++============+=============================================================================+==================================================================+ +| Node | Unintended, adverse behaviour of a CorDapp running on one or more nodes - | By design, Corda's notary-based consensus model and contract | +| (CorDapp) | either its core code or any supporting third party libraries. A coding bug | validation mechanisms provide protection against attempts to | +| | is assumed to be the default cause, although malicious modification of a | alter shared data or perform invariant operations. The primary | +| | CorDapp could result in similar effects. | risk is therefore to local systems. | +| | | | +| | | Future versions of Corda will require CorDapps to be executed | +| | | inside a sandboxed JVM environment, modified to restrict | +| | | unauthorised access to the local file system and network. This | +| | | is intended to minimise the potential of a compromised CorDapp | +| | | to affect systems local to the node. | ++------------+-----------------------------------------------------------------------------+------------------------------------------------------------------+ +| P2P & RPC | An attacker performs Man-in-the-Middle (MITM) attack against a node's | Mutually authenticated TLS connections between nodes ensures | +| connection | peer-to-peer (P2P) connection | that Man-In-The-Middle (MITM) attacks cannot take place. Corda | +| s | | Nodes restrict their connections to TLS v1.2 and also restrict | +| | **Impact** | which cipher suites are accepted. | +| | | | +| | An attacker would be able to modify transactions between participating | | +| | nodes. | | ++------------+-----------------------------------------------------------------------------+------------------------------------------------------------------+ +| Node Vault | An attacker gains access to the node's vault and modifies tables in the | There are not currently any direct controls to mitigate this | +| | database. | kind of attack. A node's vault is assumed to be within the same | +| | | trust boundary of the node JVM. Access to the vault must be | +| | **Impact** | restricted such that only the node can access it. Both | +| | | network-level controls (fire-walling) and database permissions | +| | Transaction history would become compromised. The impact could range from | must be employed. | +| | deletion of data to malicious tampering of financial detail. | | +| | | Note that the tampering of a node's vault only affects that | +| | | specific node's transaction history. No other node in the | +| | | network is affected and any tampering attempts are easily | +| | | detected. | +| | | | +| | | | ++------------+-----------------------------------------------------------------------------+------------------------------------------------------------------+ +| Network | An attacker compromises the Network Map service and publishes an | Individual Node entries in the NetworkMap must be signed by the | +| Map | illegitimate update. | associated node's private key. The signatures are validated by | +| | | the NetworkMap service, and all other Nodes in the network, to | +| | **Impact** | ensure they have not been tampered with. An attacker would need | +| | | to acquire a node's private identity signing key to be able to | +| | NodeInfo entries (name & address information) could potentially become | make modifications to a NodeInfo. This is only possible if the | +| | altered if this attack was possible | attacker has control of the node in question. | +| | | | +| | The NetworkMap could be deleted and/or unauthorized nodes could be added | It is not possible for the NetworkMap service (or R3) to modify | +| | to, or removed from the map. | entries in the network map (because the node's private keys are | +| | | not accessible). If the NetworkMap service were compromised, the | +| | | only impact the attacker could have would be to add or remove | +| | | individual entries in the map. | ++------------+-----------------------------------------------------------------------------+------------------------------------------------------------------+ + +### Repudiation + +Repudiation refers to the ability to claim a malicious action did not take place. Repudiation becomes relevant when it is not possible to verify the identity of +an attacker, or there is a lack of evidence to link their malicious actions with events in a system. + +Preventing repudiation does not prevent other forms of attack. Rather, the goal is to ensure that the attacker is identifiable, their actions can be traced, and +there is no way for the attacker to deny having committed those actions. + ++-------------+------------------------------------------------------------------------------+-----------------------------------------------------------------+ +| Element | Attacks | Mitigations | ++=============+==============================================================================+=================================================================+ +| RPC Client | Attacker attempts to initiate a flow that they are not entitled to perform | RPC clients must authenticate to the Node using credentials | +| | | passed over TLS. It is therefore not possible for an RPC client | +| | **Impact** | to perform actions without first proving their identity. | +| | | | +| | Flows could be initiated without knowing the identity of the client. | All interactions with an RPC user are also logged by the node. | +| | | An attacker's identity and actions will be recorded and cannot | +| | | be repudiated. | ++-------------+------------------------------------------------------------------------------+-----------------------------------------------------------------+ +| Node | A malicious CorDapp attempts to spend a state that does not belong to them. | Corda transactions must be signed with a node's private | +| | The node operator then claims that it was not their node that initiated the | identity key in order to be accepted by the rest of the | +| | transaction. | network. The signature directly identities the signing party | +| | | and cannot be made by any other node - therefore the act of | +| | **Impact** | signing a transaction | +| | | | +| | Financial transactions could be initiated by anonymous parties, leading to | Corda transactions between nodes utilize the P2P protocol, | +| | financial loss, and loss of confidence in the network. | which requires a mutually authenticated TLS connection. It is | +| | | not possible for a node to issue transactions without having | +| | | it's identity authenticated by other nodes in the network. Node | +| | | identity and TLS certificates are issued via Corda Network | +| | | services, and use the Corda PKI (Public Key Infrastructure) for | +| | | authentication. | +| | | | +| | | All P2P transactions are logged by the node, meaning that any | +| | | interactions are recorded | ++-------------+------------------------------------------------------------------------------+-----------------------------------------------------------------+ +| Node | A node attempts to perform a denial-of-state attack. | Non-validating Notaries require a signature over every request, | +| | | therefore nobody can deny performing denial-of-state attack | +| | | because every transaction clearly identities the node that | +| | | initiated it. | ++-------------+------------------------------------------------------------------------------+-----------------------------------------------------------------+ +| Node | | | ++-------------+------------------------------------------------------------------------------+-----------------------------------------------------------------+ + + + +### Information Disclosure + +Information disclosure is about the unauthorised access of data. Attacks of this kind have an impact when confidential data is accessed. Typical examples of +attack include extracting secrets from a running process, and accessing confidential files on a file-system which have not been appropriately secured. +Interception of network communications between trusted parties can also lead to information disclosure. + +An attacker capable of intercepting network traffic from a Corda node would, at a minimum, be able to identify which other parties that node was interacting +with, along with relative frequency and volume of data being shared; this could be used to infer additional privileged information without the parties' +consent. All network communication of a Corda is encrypted using the TLS protocol (v1.2), using modern cryptography algorithms. + ++------------+------------------------------------------------------------------------------+------------------------------------------------------------------+ +| Element | Attack | Mitigations | ++============+==============================================================================+==================================================================+ +| Node | An attacker attempts to retrieve transaction history from a peer node in the | By design, Corda nodes do not globally broadcast transaction | +| | network, for which they have no legitimate right of access. | information to all participants in the network. | +| | | | +| | Corda nodes will, upon receipt of a request referencing a valid transaction | A node will not divulge arbitrary transactions to a peer unless | +| | hash, respond with the dependency graph of that transaction. One theoretical | that peer has been included in the transaction flow. A node only | +| | scenario is therefore that a participant is able to guess (or otherwise | divulges transaction history if the transaction being requested | +| | acquire by illicit means) the hash of a valid transaction, thereby being | is a descendant of a transaction that the node itself has | +| | able to acquire its content from another node. | previously shared as part of the current flow session. | +| | | | +| | **Impact** | The SGX integration feature currently envisaged for Corda will | +| | | implement CPU peer-to-peer encryption under which transaction | +| | If successful, an exploit of the form above could result in information | graphs are transmitted in an encrypted state and only decrypted | +| | private to specific participants being shared with one or more | within a secure enclave. Knowledge of a transaction hash will | +| | non-privileged parties. This may include market-sensitive information used | then be further rendered insufficient for a non-privileged party | +| | to derive competitive advantage. | to view the content of a transaction. | ++------------+------------------------------------------------------------------------------+------------------------------------------------------------------+ +| Node Vault | An unauthorised user attempts to access the node's vault | Access to the Vault uses standard JDBC authentication mechanism. | +| (database) | | Any user connecting to the vault must have permission to do so. | +| | **Impact** | | +| | | | +| | Access to the vault would reveal the full transaction history that the node | | +| | has taken part in. This may include financial information. | | ++------------+------------------------------------------------------------------------------+------------------------------------------------------------------+ +| Node | An attacker who gains access to the machine running the Node attempts to | Corda Nodes are designed to be executed using a designated | +| Process | read memory from the JVM process. | 'corda' system process, which other users and processes on the | +| (JVM) | | system do not have permission to access. | +| | An attacker with access the file-system attempts to read the node's | | +| | cryptographic key-store, containing the private identity keys. | The node's Java Key Store is encrypted using PKCS\#12 | +| | | encryption. In the future Corda will eventually store its keys | +| | **Impact** | in a HSM (Hardware Security Module). | +| | | | +| | An attacker would be able to read sensitive such as private identity keys. | | +| | The worst impact would be the ability to extract private keys from the JVM | | +| | process. | | +| | | | +| | | | ++------------+------------------------------------------------------------------------------+------------------------------------------------------------------+ +| RPC Client | Interception of RPC traffic between a client system and the node. | RPC communications are protected by the TLS protocol. | +| | | | +| | A malicious RPC client authenticates to a Node and attempts to query the | Permission to query a node's vault must be explicitly granted on | +| | transaction vault. | a per-user basis. It is recommended that RPC credentials and | +| | | permissions are managed in an Apache Shiro database. | +| | **Impact** | | +| | | | +| | An attacker would be able to see details of transactions shared between the | | +| | connected business systems and any transacting party. | | ++------------+------------------------------------------------------------------------------+------------------------------------------------------------------+ + + + +### Denial of Service + +Denial-of-service (DoS) attacks target the availability of a resource from its intended users. There are two anticipated targets of a DoS attack - network +participants (Corda Nodes) and network services (Doorman and the Network Map). DoS attacks occur by targeting the node or network services with a high +volume/frequency of requests, or by sending malformed requests. Typical DoS attacks leverage a botnet or other distributed group of systems (Distributed Denial +of Service, DDoS). A successful DoS attack may result in non-availability of targeted ledger network node(s)/service(s), both during the attack and thereafter +until normal service can be resumed. + +Communication over the ledger network is primarily peer-to-peer. Therefore the network as a whole is relatively resilient to DoS attacks. Notaries and oracles +will only communicate with peers in the network, so are protected from non-member-on-member application-level attack. + +Corda Network Services are protected by enterprise-grade DDoS detection and mitigation services. + ++------------+------------------------------------------------------------------------------+------------------------------------------------------------------+ +| Element | Attack | Mitigations | ++============+==============================================================================+==================================================================+ +| Node | An attacker control sends high volume of malformed transactions to a node. | P2P communcation is authenticated as part of the TLS protocol, | +| | | meaning that attackers must be part of the Corda network to | +| | **Impact** | launch an attack. | +| | | | +| | Nodes targeted by this attack could exhaust their processing & memory | Communication over the ledger network is primarily peer-to-peer, | +| | resources, or potentially cease responding to transactions. | the network as a whole is relatively resilient to DoS attacks, | +| | | the primary threat being to specific nodes or services. | +| | | | +| | | Note that there is no specific mitigation against DoS attacks at | +| | | the per-node level. DoS attacks by participants on other | +| | | participants will be expressly forbidden under the terms of the | +| | | ledger network's network agreement. Measures will be taken | +| | | against any ledger network participant found to have perpetrated | +| | | a DoS attack, including exclusion from the ledger network | +| | | network and potential litigation. As a result, the perceived | +| | | risk of a member-on-member attack is low and technical measures | +| | | are not considered under this threat model, although they may be | +| | | included in future iterations. | ++------------+------------------------------------------------------------------------------+------------------------------------------------------------------+ +| CorDapp | Unintended termination or other logical sequence (e.g. due to a coding bug | The network agreement will stipulate a default maximum allowable | +| | in either Corda or a CorDapp) by which a party is rendered unable to resolve | period time - the 'event horizon' - within which a party is | +| | a f low. The most likely results from another party failing to respond when | required to provide a valid response to any message sent to it | +| | required to do so under the terms of the agreed transaction protocol. | in the course of a flow. If that period is exceeded, the flow | +| | | will be considered to be cancelled and may be discontinued | +| | **Impact** | without prejudice by all parties. The event horizon may be | +| | | superseded by agreements between parties specifying other | +| | Depending on the nature of the flow, a party could be financially impacted | timeout periods, which may be encoded into flows under the Corda | +| | by failure to resolve a flow on an indefinite basis. For example, a party | flow framework. | +| | may be left in possession of a digital asset without the means to transfer | | +| | it to another party. | Additional measures may be taken under the agreement against | +| | | parties who repeatedly fail to meet their response obligations | +| | | under the network agreement. | ++------------+------------------------------------------------------------------------------+------------------------------------------------------------------+ +| Doorman | Attacker submits excessive registration requests to the Doorman service | Doorman is deployed behind a rate-limiting firewall. | +| | | | +| | | Doorman requests are validated and filtered to ensure malformed | +| | | requests are rejected. | ++------------+------------------------------------------------------------------------------+------------------------------------------------------------------+ +| Network | Attacker causes the network map service to become unavailable | Updates to the network map must be signed by participant nodes | +| Map | | and are authenticated before being processed. | +| | | | +| | | The network map is designed to be distributed by a CDN (Content | +| | | Delivery Network). This design leverages the architecture and | +| | | security controls of the CDN and is expected to be resilient to | +| | | DDoS (Distributed Denial of Service) attack. | +| | | | +| | | The Network Map is also cached locally by nodes on the network. | +| | | If the network map online service were temporarily unavailable, | +| | | the Corda network would not be affected. | +| | | | +| | | There is no requirement for the network map services to be | +| | | highly available in order for the ledger network to be | +| | | operational. Temporary non-availability of the network map | +| | | service may delay certification of new entrants to the network, | +| | | but will have no impact on existing participants. Similarly, the | +| | | network map will be cached by individual nodes once downloaded | +| | | from the network map service; unplanned downtime would prevent | +| | | broadcast of updates relating to new nodes connecting to / | +| | | disconnecting from the network, but not affect communication | +| | | between nodes whose connection state remains unchanged | +| | | throughout the incident. | ++------------+------------------------------------------------------------------------------+------------------------------------------------------------------+ + + + +### Elevation of Privilege + +Elevation of Privilege is enabling somebody to perform actions they are not permitted to do. Attacks range from a normal user executing actions as a more +privileged administrator, to a remote (external) attacker with no privileges executing arbitrary code. + ++------------+------------------------------------------------------------------------------+-----------------------------------------------------------------+ +| Element | Attack | Mitigations | ++============+==============================================================================+=================================================================+ +| Node | Malicious contract attempts to instantiate classes in the JVM that it is not | The AMQP serialiser uses a combination of white and black-lists | +| | authorised to access. | to mitigate against de-serialisation vulnerabilities. | +| | | | +| | Malicious CorDapp sends malformed serialised data to a peer. | Corda does not currently provide specific security controls to | +| | | mitigate all classes of privilege escalation vulnerabilities. | +| | **Impact** | The design of Corda requires that CorDapps are inherently | +| | | trusted by the node administrator. | +| | Unauthorised remote code execution would lead to complete system compromise. | | +| | | Future security research will introduce stronger controls that | +| | | can mitigate this class of threat. The Deterministic JVM will | +| | | provide a sandbox that prevents execution of code & classes | +| | | outside of the security boundary that contract code is | +| | | restricted to. | +| | | | +| | | | ++------------+------------------------------------------------------------------------------+-----------------------------------------------------------------+ +| RPC Client | A malicious RPC client connects to the node and impersonates another, | Nodes implement an access-control model that restricts what | +| | higher-privileged client on the same system, and initiates flows on their | actions RPC users can perform. | +| | behalf. | | +| | | Session replay is mitigated by virtue of the TLS protocol used | +| | | to protect RPC communications. | ++------------+------------------------------------------------------------------------------+-----------------------------------------------------------------+ + + + +Conclusion +---------- + +The threat model presented here describes the main threats to the Corda Network, and the controls that are included to mitigate these threats. It was necessary +to restrict this model to a high-level perspective of the Corda Network. It is hoped that enough information is provided to allow network participants to +understand the security model of Corda. + +Threat modelling is an on-going process. There is active research at R3 to continue evolving the Corda Threat Model. In particular, models are being developed +that focus more closely on individual components - such as the Node, Network Map and Doorman. + + + + diff --git a/docs/source/design/threat-model/images/threat-model.png b/docs/source/design/threat-model/images/threat-model.png new file mode 100644 index 0000000000..7735b61cf2 Binary files /dev/null and b/docs/source/design/threat-model/images/threat-model.png differ diff --git a/docs/source/example-code/build.gradle b/docs/source/example-code/build.gradle index 9a2d0ed5a4..eeeb489eb4 100644 --- a/docs/source/example-code/build.gradle +++ b/docs/source/example-code/build.gradle @@ -3,15 +3,6 @@ apply plugin: 'application' apply plugin: 'net.corda.plugins.cordformation' apply plugin: 'net.corda.plugins.quasar-utils' -repositories { - mavenLocal() - mavenCentral() - jcenter() - maven { - url 'http://oss.sonatype.org/content/repositories/snapshots' - } -} - configurations { integrationTestCompile.extendsFrom testCompile integrationTestRuntime.extendsFrom testRuntime diff --git a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/TutorialFlowAsyncOperationTest.kt b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/TutorialFlowAsyncOperationTest.kt index fb3717f109..251c9d8029 100644 --- a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/TutorialFlowAsyncOperationTest.kt +++ b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/TutorialFlowAsyncOperationTest.kt @@ -3,7 +3,7 @@ package net.corda.docs import net.corda.client.rpc.CordaRPCClient import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow -import net.corda.docs.tutorial.flowstatemachines.ExampleSummingFlow +import net.corda.docs.kotlin.tutorial.flowstatemachines.ExampleSummingFlow import net.corda.node.services.Permissions import net.corda.testing.core.ALICE_NAME import net.corda.testing.driver.DriverParameters diff --git a/docs/source/example-code/src/main/java/com/template/TemplateContract.java b/docs/source/example-code/src/main/java/com/template/TemplateContract.java index 5c4278a408..e77823a71a 100644 --- a/docs/source/example-code/src/main/java/com/template/TemplateContract.java +++ b/docs/source/example-code/src/main/java/com/template/TemplateContract.java @@ -1,8 +1,10 @@ +// We purposefully have this template here as part of progressing through the tutorial package com.template; import net.corda.core.contracts.CommandData; import net.corda.core.contracts.Contract; import net.corda.core.transactions.LedgerTransaction; +import org.jetbrains.annotations.NotNull; public class TemplateContract implements Contract { // This is used to identify our contract when building a transaction. @@ -13,9 +15,9 @@ public class TemplateContract implements Contract { * and output states does not throw an exception. */ @Override - public void verify(LedgerTransaction tx) {} + public void verify(@NotNull LedgerTransaction tx) {} public interface Commands extends CommandData { class Action implements Commands {} } -} \ No newline at end of file +} diff --git a/docs/source/example-code/src/main/java/net/corda/docs/ClientRpcExampleJava.java b/docs/source/example-code/src/main/java/net/corda/docs/java/ClientRpcExample.java similarity index 75% rename from docs/source/example-code/src/main/java/net/corda/docs/ClientRpcExampleJava.java rename to docs/source/example-code/src/main/java/net/corda/docs/java/ClientRpcExample.java index 2a7bdb4d9b..224dbccf45 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/ClientRpcExampleJava.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/ClientRpcExample.java @@ -1,20 +1,17 @@ -package net.corda.docs; +package net.corda.docs.java; // START 1 import net.corda.client.rpc.CordaRPCClient; import net.corda.client.rpc.CordaRPCConnection; import net.corda.core.messaging.CordaRPCOps; import net.corda.core.utilities.NetworkHostAndPort; -import org.apache.activemq.artemis.api.core.ActiveMQException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.concurrent.ExecutionException; +class ClientRpcExample { + private static final Logger logger = LoggerFactory.getLogger(ClientRpcExample.class); -class ExampleRpcClientJava { - private static final Logger logger = LoggerFactory.getLogger(ExampleRpcClient.class); - - public static void main(String[] args) throws ActiveMQException, InterruptedException, ExecutionException { + public static void main(String[] args) { if (args.length != 3) { throw new IllegalArgumentException("Usage: TemplateClient <node address> <username> <password>"); } diff --git a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java b/docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java similarity index 99% rename from docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java rename to docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java index 997e642105..b8cb3a439b 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java @@ -1,4 +1,4 @@ -package net.corda.docs; +package net.corda.docs.java; import co.paralleluniverse.fibers.Suspendable; import com.google.common.collect.ImmutableList; @@ -37,7 +37,7 @@ import static net.corda.core.contracts.ContractsDSL.requireThat; import static net.corda.core.crypto.Crypto.generateKeyPair; @SuppressWarnings("unused") -public class FlowCookbookJava { +public class FlowCookbook { // ``InitiatorFlow`` is our first flow, and will communicate with // ``ResponderFlow``, below. // We mark ``InitiatorFlow`` as an ``InitiatingFlow``, allowing it to be diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/LaunchSpaceshipFlow.java b/docs/source/example-code/src/main/java/net/corda/docs/java/LaunchSpaceshipFlow.java new file mode 100644 index 0000000000..9ef0d72864 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/LaunchSpaceshipFlow.java @@ -0,0 +1,133 @@ +package net.corda.docs.java; + +import co.paralleluniverse.fibers.Suspendable; +import net.corda.core.flows.*; +import net.corda.core.identity.Party; + +@SuppressWarnings("ALL") +// DOCSTART LaunchSpaceshipFlow +@InitiatingFlow +class LaunchSpaceshipFlow extends FlowLogic<Void> { + @Suspendable + @Override + public Void call() throws FlowException { + boolean shouldLaunchSpaceship = receive(Boolean.class, getPresident()).unwrap(s -> s); + if (shouldLaunchSpaceship) { + launchSpaceship(); + } + return null; + } + + public void launchSpaceship() { + } + + public Party getPresident() { + throw new AbstractMethodError(); + } +} + +@InitiatedBy(LaunchSpaceshipFlow.class) +@InitiatingFlow +class PresidentSpaceshipFlow extends FlowLogic<Void> { + private final Party launcher; + + public PresidentSpaceshipFlow(Party launcher) { + this.launcher = launcher; + } + + @Suspendable + @Override + public Void call() { + boolean needCoffee = true; + send(getSecretary(), needCoffee); + boolean shouldLaunchSpaceship = false; + send(launcher, shouldLaunchSpaceship); + return null; + } + + public Party getSecretary() { + throw new AbstractMethodError(); + } +} + +@InitiatedBy(PresidentSpaceshipFlow.class) +class SecretaryFlow extends FlowLogic<Void> { + private final Party president; + + public SecretaryFlow(Party president) { + this.president = president; + } + + @Suspendable + @Override + public Void call() { + // ignore + return null; + } +} +// DOCEND LaunchSpaceshipFlow + +@SuppressWarnings("ALL") +// DOCSTART LaunchSpaceshipFlowCorrect +@InitiatingFlow +class LaunchSpaceshipFlowCorrect extends FlowLogic<Void> { + @Suspendable + @Override + public Void call() throws FlowException { + FlowSession presidentSession = initiateFlow(getPresident()); + boolean shouldLaunchSpaceship = presidentSession.receive(Boolean.class).unwrap(s -> s); + if (shouldLaunchSpaceship) { + launchSpaceship(); + } + return null; + } + + public void launchSpaceship() { + } + + public Party getPresident() { + throw new AbstractMethodError(); + } +} + +@InitiatedBy(LaunchSpaceshipFlowCorrect.class) +@InitiatingFlow +class PresidentSpaceshipFlowCorrect extends FlowLogic<Void> { + private final FlowSession launcherSession; + + public PresidentSpaceshipFlowCorrect(FlowSession launcherSession) { + this.launcherSession = launcherSession; + } + + @Suspendable + @Override + public Void call() { + boolean needCoffee = true; + FlowSession secretarySession = initiateFlow(getSecretary()); + secretarySession.send(needCoffee); + boolean shouldLaunchSpaceship = false; + launcherSession.send(shouldLaunchSpaceship); + return null; + } + + public Party getSecretary() { + throw new AbstractMethodError(); + } +} + +@InitiatedBy(PresidentSpaceshipFlowCorrect.class) +class SecretaryFlowCorrect extends FlowLogic<Void> { + private final FlowSession presidentSession; + + public SecretaryFlowCorrect(FlowSession presidentSession) { + this.presidentSession = presidentSession; + } + + @Suspendable + @Override + public Void call() { + // ignore + return null; + } +} +// DOCEND LaunchSpaceshipFlowCorrect diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/TutorialFlowStateMachines.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/TutorialFlowStateMachines.java index ac7f387ee7..468a0c5711 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/TutorialFlowStateMachines.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/TutorialFlowStateMachines.java @@ -4,6 +4,7 @@ import net.corda.core.flows.SignTransactionFlow; import net.corda.core.utilities.ProgressTracker; import org.jetbrains.annotations.Nullable; +@SuppressWarnings("ALL") public class TutorialFlowStateMachines { // DOCSTART 1 private final ProgressTracker progressTracker = new ProgressTracker( diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcExample.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/ClientRpcExample.kt similarity index 86% rename from docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcExample.kt rename to docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/ClientRpcExample.kt index e41eb2ed18..14c7c599b4 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcExample.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/ClientRpcExample.kt @@ -1,6 +1,6 @@ @file:Suppress("unused") -package net.corda.docs +package net.corda.docs.kotlin // START 1 import net.corda.client.rpc.CordaRPCClient @@ -8,9 +8,9 @@ import net.corda.core.utilities.NetworkHostAndPort.Companion.parse import net.corda.core.utilities.loggerFor import org.slf4j.Logger -class ExampleRpcClient { +class ClientRpcExample { companion object { - val logger: Logger = loggerFor<ExampleRpcClient>() + val logger: Logger = loggerFor<ClientRpcExample>() } fun main(args: Array<String>) { diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/ClientRpcTutorial.kt similarity index 99% rename from docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt rename to docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/ClientRpcTutorial.kt index 093b4bc8d0..dd0ea14b4f 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/ClientRpcTutorial.kt @@ -1,6 +1,6 @@ @file:Suppress("unused") -package net.corda.docs +package net.corda.docs.kotlin import net.corda.client.rpc.CordaRPCClient import net.corda.core.contracts.Amount diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/CustomVaultQuery.kt similarity index 98% rename from docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt rename to docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/CustomVaultQuery.kt index 5a4a1c04d7..c89997f25a 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/CustomVaultQuery.kt @@ -1,4 +1,6 @@ -package net.corda.docs +@file:Suppress("unused", "MemberVisibilityCanBePrivate") + +package net.corda.docs.kotlin import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Amount diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt similarity index 99% rename from docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt rename to docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt index 077defee10..31135dc6c7 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt @@ -1,6 +1,6 @@ @file:Suppress("UNUSED_VARIABLE", "unused", "DEPRECATION") -package net.corda.docs +package net.corda.docs.kotlin import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FxTransactionBuildTutorial.kt similarity index 99% rename from docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt rename to docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FxTransactionBuildTutorial.kt index 964715bea5..052b4ddf21 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FxTransactionBuildTutorial.kt @@ -1,4 +1,4 @@ -package net.corda.docs +package net.corda.docs.kotlin import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/LaunchSpaceshipFlow.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/LaunchSpaceshipFlow.kt similarity index 96% rename from docs/source/example-code/src/main/kotlin/net/corda/docs/LaunchSpaceshipFlow.kt rename to docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/LaunchSpaceshipFlow.kt index 8e94e82b32..c5b7f3a637 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/LaunchSpaceshipFlow.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/LaunchSpaceshipFlow.kt @@ -1,6 +1,6 @@ -@file:Suppress("DEPRECATION") +@file:Suppress("DEPRECATION", "MemberVisibilityCanBePrivate", "unused") -package net.corda.docs +package net.corda.docs.kotlin import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.FlowLogic diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/WorkflowTransactionBuildTutorial.kt similarity index 98% rename from docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt rename to docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/WorkflowTransactionBuildTutorial.kt index 34e46f5ba3..b2b34841f5 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/WorkflowTransactionBuildTutorial.kt @@ -1,4 +1,6 @@ -package net.corda.docs +@file:Suppress("unused") + +package net.corda.docs.kotlin import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* @@ -23,7 +25,7 @@ enum class WorkflowState { REJECTED } -const val TRADE_APPROVAL_PROGRAM_ID = "net.corda.docs.TradeApprovalContract" +const val TRADE_APPROVAL_PROGRAM_ID = "net.corda.docs.kotlin.TradeApprovalContract" /** * Minimal contract to encode a simple workflow with one initial state and two possible eventual states. diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/contract/TutorialContract.kt similarity index 99% rename from docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt rename to docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/contract/TutorialContract.kt index 78946fbd67..39c9be245f 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/contract/TutorialContract.kt @@ -1,4 +1,4 @@ -package net.corda.docs.tutorial.contract +package net.corda.docs.kotlin.tutorial.contract import net.corda.core.contracts.* import net.corda.core.crypto.NullKeys diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt similarity index 95% rename from docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt rename to docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt index 8619361485..dfbf8c158d 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt @@ -1,4 +1,4 @@ -package net.corda.docs.tutorial.flowstatemachines +package net.corda.docs.kotlin.tutorial.flowstatemachines import co.paralleluniverse.fibers.Suspendable import net.corda.core.concurrent.CordaFuture diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/flowstatemachines/TutorialFlowStateMachines.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/flowstatemachines/TutorialFlowStateMachines.kt similarity index 95% rename from docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/flowstatemachines/TutorialFlowStateMachines.kt rename to docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/flowstatemachines/TutorialFlowStateMachines.kt index 0131077a8f..23c0e12dc9 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/flowstatemachines/TutorialFlowStateMachines.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/flowstatemachines/TutorialFlowStateMachines.kt @@ -1,4 +1,6 @@ -package net.corda.docs.tutorial.flowstatemachines +@file:Suppress("unused", "MemberVisibilityCanBePrivate") + +package net.corda.docs.kotlin.tutorial.flowstatemachines import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Amount diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/flow.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUFlow.kt similarity index 88% rename from docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/flow.kt rename to docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUFlow.kt index 676af37f4c..9c92571ce3 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/flow.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUFlow.kt @@ -1,6 +1,9 @@ -package net.corda.docs.tutorial.helloworld +@file:Suppress("MemberVisibilityCanBePrivate") + +package net.corda.docs.kotlin.tutorial.helloworld import co.paralleluniverse.fibers.Suspendable +import com.template.TemplateContract import net.corda.core.flows.FinalityFlow import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow @@ -13,6 +16,8 @@ import net.corda.core.identity.Party import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker +import com.template.TemplateContract.TEMPLATE_CONTRACT_ID + // Replace TemplateFlow's definition with: @InitiatingFlow @StartableByRPC @@ -44,4 +49,4 @@ class IOUFlow(val iouValue: Int, subFlow(FinalityFlow(signedTx)) } } -// DOCEND 01 \ No newline at end of file +// DOCEND 01 diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/state.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUState.kt similarity index 84% rename from docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/state.kt rename to docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUState.kt index 19f431bfb1..f79e3311cd 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/state.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUState.kt @@ -1,4 +1,4 @@ -package net.corda.docs.tutorial.helloworld +package net.corda.docs.kotlin.tutorial.helloworld import net.corda.core.contracts.ContractState diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/tearoffs/TutorialTearOffs.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/tearoffs/TutorialTearOffs.kt similarity index 96% rename from docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/tearoffs/TutorialTearOffs.kt rename to docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/tearoffs/TutorialTearOffs.kt index d4f67c02a0..82ee763780 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/tearoffs/TutorialTearOffs.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/tearoffs/TutorialTearOffs.kt @@ -1,6 +1,6 @@ @file:Suppress("UNUSED_VARIABLE") -package net.corda.docs.tutorial.tearoffs +package net.corda.docs.kotlin.tutorial.tearoffs import net.corda.core.contracts.Command import net.corda.core.contracts.StateRef diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/contract.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUContract.kt similarity index 95% rename from docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/contract.kt rename to docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUContract.kt index 66def91a3f..2f55ff6441 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/contract.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUContract.kt @@ -1,4 +1,4 @@ -package net.corda.docs.tutorial.twoparty +package net.corda.docs.kotlin.tutorial.twoparty import net.corda.core.contracts.CommandData import net.corda.core.contracts.Contract diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlow.kt similarity index 97% rename from docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt rename to docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlow.kt index 546ac2fb39..22714acd33 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlow.kt @@ -1,4 +1,4 @@ -package net.corda.docs.tutorial.twoparty +package net.corda.docs.kotlin.tutorial.twoparty // DOCSTART 01 import co.paralleluniverse.fibers.Suspendable diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flowResponder.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlowResponder.kt similarity index 85% rename from docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flowResponder.kt rename to docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlowResponder.kt index f2ff5ecdc9..45e792abe0 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flowResponder.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlowResponder.kt @@ -1,12 +1,12 @@ -package net.corda.docs.tutorial.twoparty +package net.corda.docs.kotlin.tutorial.twoparty 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.SignTransactionFlow -import net.corda.docs.tutorial.helloworld.IOUFlow -import net.corda.docs.tutorial.helloworld.IOUState +import net.corda.docs.kotlin.tutorial.helloworld.IOUFlow +import net.corda.docs.kotlin.tutorial.helloworld.IOUState // DOCSTART 01 // Add these imports: diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/state.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUState.kt similarity index 84% rename from docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/state.kt rename to docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUState.kt index a690625d65..b1a631d025 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/state.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUState.kt @@ -1,4 +1,4 @@ -package net.corda.docs.tutorial.twoparty +package net.corda.docs.kotlin.tutorial.twoparty import net.corda.core.contracts.ContractState import net.corda.core.identity.Party diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/templateContract.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/templateContract.kt deleted file mode 100644 index 14a6746eeb..0000000000 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/templateContract.kt +++ /dev/null @@ -1,20 +0,0 @@ -package net.corda.docs.tutorial.helloworld - -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.Contract -import net.corda.core.transactions.LedgerTransaction - -const val TEMPLATE_CONTRACT_ID = "com.template.TemplateContract" - -open class TemplateContract : Contract { - // A transaction is considered valid if the verify() function of the contract of each of the transaction's input - // and output states does not throw an exception. - override fun verify(tx: LedgerTransaction) { - // Verification logic goes here. - } - - // Used to indicate the transaction's intent. - interface Commands : CommandData { - class Action : Commands - } -} \ No newline at end of file diff --git a/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java b/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java similarity index 99% rename from docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java rename to docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java index d84166613a..ae35a39668 100644 --- a/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java +++ b/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java @@ -25,7 +25,7 @@ import static net.corda.testing.node.MockServicesKt.makeTestIdentityService; import static net.corda.testing.node.NodeTestUtils.ledger; import static net.corda.testing.node.NodeTestUtils.transaction; -public class CommercialPaperTest { +public class TutorialTestDSL { private static final TestIdentity alice = new TestIdentity(ALICE_NAME, 70L); // DOCSTART 14 private static final TestIdentity bigCorp = new TestIdentity(new CordaX500Name("BigCorp", "New York", "GB")); diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt index 53e92a35c5..e5a5ec8786 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt @@ -4,20 +4,12 @@ import net.corda.core.contracts.Amount import net.corda.core.contracts.ContractState import net.corda.core.identity.Party import net.corda.core.node.services.queryBy -import net.corda.core.node.services.vault.DEFAULT_PAGE_NUM -import net.corda.core.node.services.vault.DEFAULT_PAGE_SIZE -import net.corda.core.node.services.vault.PageSpecification -import net.corda.core.node.services.vault.QueryCriteria -import net.corda.core.node.services.vault.builder +import net.corda.core.node.services.vault.* import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.docs.java.tutorial.helloworld.IOUFlow -import net.corda.finance.CHF -import net.corda.finance.DOLLARS -import net.corda.finance.GBP -import net.corda.finance.POUNDS -import net.corda.finance.SWISS_FRANCS -import net.corda.finance.USD +import net.corda.docs.kotlin.TopupIssuerFlow +import net.corda.finance.* import net.corda.finance.contracts.getCashBalances import net.corda.finance.flows.CashIssueFlow import net.corda.node.services.vault.VaultSchemaV1 @@ -112,4 +104,4 @@ class CustomVaultQueryTest { return Pair(balancesNodesA, balancesNodesB) } -} \ No newline at end of file +} diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt index b27f382ca7..430fc025c8 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt @@ -4,6 +4,8 @@ import net.corda.core.identity.Party import net.corda.core.toFuture import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow +import net.corda.docs.kotlin.ForeignExchangeFlow +import net.corda.docs.kotlin.ForeignExchangeRemoteFlow import net.corda.finance.DOLLARS import net.corda.finance.GBP import net.corda.finance.POUNDS diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt index 3304f4707a..ccc0793663 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt @@ -9,6 +9,10 @@ import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.toFuture import net.corda.core.utilities.getOrThrow +import net.corda.docs.kotlin.SubmitCompletionFlow +import net.corda.docs.kotlin.SubmitTradeApprovalFlow +import net.corda.docs.kotlin.TradeApprovalContract +import net.corda.docs.kotlin.WorkflowState import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.node.MockNetwork diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt similarity index 97% rename from docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt rename to docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt index 26f1f6885b..5069e8c11c 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt @@ -1,4 +1,6 @@ -package net.corda.docs.tutorial.testdsl +@file:Suppress("MemberVisibilityCanBePrivate") + +package net.corda.docs.kotlin.tutorial.testdsl import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever @@ -13,11 +15,7 @@ import net.corda.finance.contracts.CommercialPaper import net.corda.finance.contracts.ICommercialPaperState import net.corda.finance.contracts.asset.CASH import net.corda.finance.contracts.asset.Cash -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.BOB_NAME -import net.corda.testing.core.DUMMY_NOTARY_NAME -import net.corda.testing.core.SerializationEnvironmentRule -import net.corda.testing.core.TestIdentity +import net.corda.testing.core.* import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices import net.corda.testing.node.ledger @@ -26,7 +24,7 @@ import org.junit.Rule import org.junit.Test import java.time.Instant -class CommercialPaperTest { +class TutorialTestDSL { private companion object { val alice = TestIdentity(ALICE_NAME, 70) val bob = TestIdentity(BOB_NAME, 80) diff --git a/docs/source/flow-cookbook.rst b/docs/source/flow-cookbook.rst index 1402c2ea1c..695e49f701 100644 --- a/docs/source/flow-cookbook.rst +++ b/docs/source/flow-cookbook.rst @@ -11,8 +11,8 @@ This flow showcases how to use Corda's API, in both Java and Kotlin. .. container:: codeset - .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin - .. literalinclude:: example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java \ No newline at end of file diff --git a/docs/source/flow-state-machines.rst b/docs/source/flow-state-machines.rst index c48481df3c..63de6e6a25 100644 --- a/docs/source/flow-state-machines.rst +++ b/docs/source/flow-state-machines.rst @@ -108,7 +108,7 @@ each side. .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/flowstatemachines/TutorialFlowStateMachines.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/flowstatemachines/TutorialFlowStateMachines.kt :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 diff --git a/docs/source/generating-a-node.rst b/docs/source/generating-a-node.rst index cc4ee9e1cb..052bdd3af5 100644 --- a/docs/source/generating-a-node.rst +++ b/docs/source/generating-a-node.rst @@ -47,9 +47,11 @@ in the `Kotlin CorDapp Template <https://github.com/corda/cordapp-template-kotli sshdPort 2223 // Includes the corda-finance CorDapp on our node. cordapps = ["net.corda:corda-finance:$corda_release_version"] - // Specify a JVM argument to be used when running the node (in this case, extra heap size). extraConfig = [ - jvmArgs : [ "-Xmx1g"] + // Setting the JMX reporter type. + jmxReporterType: 'JOLOKIA', + // Setting the H2 address. + h2Settings: [ address: 'localhost:10030' ] ] } node { diff --git a/docs/source/getting-set-up.rst b/docs/source/getting-set-up.rst index 1ad1a71465..fe0f0f62e8 100644 --- a/docs/source/getting-set-up.rst +++ b/docs/source/getting-set-up.rst @@ -9,7 +9,7 @@ Corda uses industry-standard tools: * **IntelliJ IDEA** - supported versions **2017.x** and **2018.x** (with Kotlin plugin version |kotlin_version|) * **Git** -We also use Gradle and Kotlin, but you do not need to install them. A standalone Gradle wrapper is provided, and it +We also use Gradle and Kotlin, but you do not need to install them. A standalone Gradle wrapper is provided, and it will download the correct version of Kotlin. Please note: @@ -33,7 +33,7 @@ others to provide support. However, if you do use other tools, we'd be intereste Set-up instructions ------------------- -The instructions below will allow you to set up your development environment for running Corda and writing CorDapps. If +The instructions below will allow you to set up your development environment for running Corda and writing CorDapps. If you have any issues, please reach out on `Stack Overflow <https://stackoverflow.com/questions/tagged/corda>`_ or via `our Slack channels <http://slack.corda.net/>`_. @@ -45,12 +45,14 @@ The set-up instructions are available for the following platforms: * :ref:`deb-ubuntu-label` +* :ref:`fedora-label` + .. _windows-label: Windows ------- -.. warning:: If you are using a Mac or a Debian/Ubuntu machine, please follow the :ref:`mac-label` or :ref:`deb-ubuntu-label` instructions instead. +.. warning:: If you are using a Mac, Debian/Ubuntu or Fedora machine, please follow the :ref:`mac-label`, :ref:`deb-ubuntu-label` or :ref:`fedora-label` instructions instead. Java ^^^^ @@ -80,7 +82,7 @@ IntelliJ Mac --- -.. warning:: If you are using a Windows or a Debian/Ubuntu machine, please follow the :ref:`windows-label` or :ref:`deb-ubuntu-label` instructions instead. +.. warning:: If you are using a Windows, Debian/Ubuntu or Fedora machine, please follow the :ref:`windows-label`, :ref:`deb-ubuntu-label` or :ref:`fedora-label` instructions instead. Java ^^^^ @@ -102,7 +104,7 @@ IntelliJ Debian/Ubuntu ------------- -.. warning:: If you are using a Mac or a Windows machine, please follow the :ref:`mac-label` or :ref:`windows-label` instructions instead. +.. warning:: If you are using a Mac, Windows or Fedora machine, please follow the :ref:`mac-label`, :ref:`windows-label` or :ref:`fedora-label` instructions instead. These instructions were tested on Ubuntu Desktop 18.04 LTS. @@ -126,6 +128,34 @@ Jetbrains offers a pre-built snap package that allows for easy, one-step install 2. Click ``Install``, then ``View in Desktop Store``. Choose ``Ubuntu Software`` in the Launch Application window. 3. Ensure the Kotlin plugin in Intellij is updated to version |kotlin_version| +.. _fedora-label: + +Fedora +------------- + +.. warning:: If you are using a Mac, Windows or Debian/Ubuntu machine, please follow the :ref:`mac-label`, :ref:`windows-label` or :ref:`deb-ubuntu-label` instructions instead. + +These instructions were tested on Fedora 28. + +Java +^^^^ +1. Download the RPM installation file of Oracle JDK from https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html. +2. Install the package with ``rpm -ivh jdk-<version>-linux-<architecture>.rpm`` or use the default software manager. +3. Choose java version by using the following command ``alternatives --config java`` +4. Verify that the JDK was installed correctly by running ``java -version`` + +Git +^^^^ +1. From the terminal, Git can be installed using dnf with the command ``sudo dnf install git`` +2. Verify that git was installed correctly by typing ``git --version`` + +IntelliJ +^^^^^^^^ +1. Visit https://www.jetbrains.com/idea/download/download-thanks.html?platform=linux&code=IIC +2. Unpack the ``tar.gz`` file using the following command ``tar xfz ideaIC-<version>.tar.gz -C /opt`` +3. Run IntelliJ with ``/opt/ideaIC-<version>/bin/idea.sh`` +4. Ensure the Kotlin plugin in IntelliJ is updated to version |kotlin_version| + Next steps ---------- diff --git a/docs/source/hello-world-flow.rst b/docs/source/hello-world-flow.rst index 42bf242dab..b50ff584ec 100644 --- a/docs/source/hello-world-flow.rst +++ b/docs/source/hello-world-flow.rst @@ -45,7 +45,7 @@ template (``Initiator`` and ``Responder``), and replace them with the following: .. container:: codeset - .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/flow.kt + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUFlow.kt :language: kotlin :start-after: DOCSTART 01 :end-before: DOCEND 01 diff --git a/docs/source/hello-world-state.rst b/docs/source/hello-world-state.rst index e4b3e5f488..0a2d9c4b12 100644 --- a/docs/source/hello-world-state.rst +++ b/docs/source/hello-world-state.rst @@ -63,7 +63,7 @@ Let's get started by opening ``TemplateState.java`` (for Java) or ``StatesAndCon .. container:: codeset - .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/state.kt + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUState.kt :language: kotlin :start-after: DOCSTART 01 :end-before: DOCEND 01 diff --git a/docs/source/index.rst b/docs/source/index.rst index 13fe8e45fd..fc92e187ef 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -53,6 +53,7 @@ We look forward to seeing what you can do with Corda! aws-vm.rst loadtesting.rst building-container-images.rst + cli-application-shell-extensions.rst .. Documentation is not included in the pdf unless it is included in a toctree somewhere @@ -79,6 +80,7 @@ We look forward to seeing what you can do with Corda! design/monitoring-management/design.md design/sgx-integration/design.md design/sgx-infrastructure/design.md + design/threat-model/corda-threat-model.md design/data-model-upgrades/signature-constraints.md .. conditional-toctree:: diff --git a/docs/source/joining-a-network.rst b/docs/source/joining-a-network.rst new file mode 100644 index 0000000000..6de161b781 --- /dev/null +++ b/docs/source/joining-a-network.rst @@ -0,0 +1,54 @@ +.. highlight:: kotlin +.. raw:: html + + <script type="text/javascript" src="_static/jquery.js"></script> + <script type="text/javascript" src="_static/codesets.js"></script> + +Connecting to a compatibility zone +================================== + +Every Corda node is part of a network (also called a zone) that is *permissioned*. Production deployments require a +secure certificate authority. Most users will join an existing network such as the main Corda network or the Corda +TestNet. + +To connect to a compatibility zone you need to register with its certificate signing authority (doorman) by submitting +a certificate signing request (CSR) to obtain a valid identity for the zone. You could do this out of band, for instance +via email or a web form, but there's also a simple request/response protocol built into Corda. + +Before you can register, you must first have received the trust store file containing the root certificate from the zone +operator. For high security zones this might be delivered physically. Then run the following command: + +``java -jar corda.jar --initial-registration --network-root-truststore-password <trust store password>`` + +By default it will expect the trust store file to be in the location ``certificates/network-root-truststore.jks``. +This can be overridden with the additional ``--network-root-truststore`` flag. + +The certificate signing request will be created based on node information obtained from the node configuration. +The following information from the node configuration file is needed to generate the request. + +* **myLegalName** Your company's legal name as an X.500 string. X.500 allows differentiation between entities with the same + name, as the legal name needs to be unique on the network. If another node has already been permissioned with this + name then the permissioning server will automatically reject the request. The request will also be rejected if it + violates legal name rules, see :ref:`node_naming` for more information. You can use the X.500 schema to disambiguate + entities that have the same or similar brand names. + +* **emailAddress** e.g. "admin@company.com" + +* **devMode** must be set to false + +* **networkServices** or **compatibilityZoneURL** The Corda compatibility zone services must be configured. This must be either: + + * **compatibilityZoneURL** The Corda compatibility zone network management service root URL. + * **networkServices** Replaces the ``compatibilityZoneURL`` when the doorman and network map services + are configured to operate on different URL endpoints. The ``doorman`` entry is used for registration. + +A new pair of private and public keys generated by the Corda node will be used to create the request. + +The utility will submit the request to the doorman server and poll for a result periodically to retrieve the +certificates. Once the request has been approved and the certificates downloaded from the server, the node will create +the keystore and trust store using the certificates and the generated private key. + +.. note:: You can exit the utility at any time if the approval process is taking longer than expected. The request + process will resume on restart as long as the ``--initial-registration`` flag is specified. + +This process only is needed when the node connects to the network for the first time, or when the certificate expires. diff --git a/docs/source/network-bootstrapper.rst b/docs/source/network-bootstrapper.rst index 8d9dce1ad4..a1d33d0034 100644 --- a/docs/source/network-bootstrapper.rst +++ b/docs/source/network-bootstrapper.rst @@ -245,3 +245,22 @@ To give the following: .. note:: The whitelist can only ever be appended to. Once added a contract implementation can never be removed. + +Command-line options +-------------------- + +The network bootstrapper can be started with the following command-line options: + +.. code-block:: shell + + bootstrapper [-hvV] [--install-shell-extensions] [--no-copy] [--dir=<dir>] + [--logging-level=<loggingLevel>] + +* ``--dir=<dir>``: Root directory containing the node configuration files and CorDapp JARs that will form the test network. + It may also contain existing node directories. Defaults to the current directory. +* ``--no-copy``: Don't copy the CorDapp JARs into the nodes' "cordapps" directories. +* ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file. +* ``--logging-level=<loggingLevel>``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO. +* ``--install-shell-extensions``: Install ``bootstrapper`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. +* ``--help``, ``-h``: Show this help message and exit. +* ``--version``, ``-V``: Print version information and exit. \ No newline at end of file diff --git a/docs/source/node-administration.rst b/docs/source/node-administration.rst index 690a339732..98e5697ddb 100644 --- a/docs/source/node-administration.rst +++ b/docs/source/node-administration.rst @@ -13,6 +13,12 @@ It may be the case that you require to amend the log level of a particular subse closer look at hibernate activity). So, for more bespoke logging configuration, the logger settings can be completely overridden with a `Log4j 2 <https://logging.apache.org/log4j/2.x>`_ configuration file assigned to the ``log4j.configurationFile`` system property. +The node is using log4j asynchronous logging by default (configured via log4j2 properties file in its resources) +to ensure that log message flushing is not slowing down the actual processing. +If you need to switch to synchronous logging (e.g. for debugging/testing purposes), you can override this behaviour +by adding ``-DLog4jContextSelector=org.apache.logging.log4j.core.selector.ClassLoaderContextSelector`` to the node's +command line or to the ``jvmArgs`` section of the node configuration (see :doc:`corda-configuration-file`). + Example +++++++ diff --git a/docs/source/node-structure.rst b/docs/source/node-structure.rst index 9d785df0a9..371f258ae7 100644 --- a/docs/source/node-structure.rst +++ b/docs/source/node-structure.rst @@ -22,13 +22,23 @@ A Corda node has the following structure: ├── persistence.mv.db // The node's database └── shell-commands // Custom shell commands defined by the node owner -The node is configured by editing its ``node.conf`` file (see :doc:`corda-configuration-file`). You install CorDapps on -the node by dropping CorDapp JARs into the ``cordapps`` folder. +You install CorDapps on the node by placing CorDapp JARs in the ``cordapps`` folder. -In development mode (i.e. when ``devMode = true``, see :doc:`corda-configuration-file`), the ``certificates`` -directory is filled with pre-configured keystores if the required keystores do not exist. This ensures that developers -can get the nodes working as quickly as possible. However, these pre-configured keystores are not secure, to learn more -see :doc:`permissioning`. +In development mode (i.e. when ``devMode = true``), the ``certificates`` directory is filled with pre-configured +keystores if they do not already exist to ensure that developers can get the nodes working as quickly as +possible. + +.. warning:: These pre-configured keystores are not secure and must not used in a production environments. + +The keystores store the key pairs and certificates under the following aliases: + +* ``nodekeystore.jks`` uses the aliases ``cordaclientca`` and ``identity-private-key`` +* ``sslkeystore.jks`` uses the alias ``cordaclienttls`` + +All the keystores use the password provided in the node's configuration file using the ``keyStorePassword`` attribute. +If no password is configured, it defaults to ``cordacadevpass``. + +To learn more, see :doc:`permissioning`. .. _node_naming: diff --git a/docs/source/permissioning.rst b/docs/source/permissioning.rst index 0ea28af247..c7ca9986cc 100644 --- a/docs/source/permissioning.rst +++ b/docs/source/permissioning.rst @@ -9,43 +9,28 @@ Network permissioning .. contents:: -Every Corda node is a part of a network (also called a zone), and networks are *permissioned*. To connect to a -zone, a node needs a signed X.509 certificate from the network operator. Production deployments require a secure certificate authority. -The issued certificates take the form of three keystores in a node's ``<workspace>/certificates/`` folder: - -* ``network-root-truststore.jks``, the network/zone operator's public keys and certificates as provided by them with a standard password. Can be deleted after initial registration -* ``truststore.jks``, the network/zone operator's public keys and certificates in keystore with a locally configurable password as protection against certain attacks -* ``nodekeystore.jks``, which stores the node’s identity keypairs and certificates -* ``sslkeystore.jks``, which stores the node’s TLS keypairs and certificates - -Most users will join an existing network such as the main Corda network or the Corda TestNet. You can also build your -own networks. During development, no network is required because you can use the included tools to pre-create -and pre-distribute the certificates and map files that would normally be provided dynamically by the network. Effectively -the :doc:`bootstrapper tool <network-bootstrapper>` creates a private semi-static network for you. - Certificate hierarchy --------------------- A Corda network has three types of certificate authorities (CAs): -* The **root network CA**, that defines the extent of a compatibility zone. -* The **doorman CA**. The doorman CA is used instead of the root network CA for day-to-day key signing to reduce the - risk of the root network CA's private key being compromised. This is equivalent to an intermediate certificate - in the web PKI. -* Each node also serves as its own CA in issuing the child certificates that it uses to sign its identity keys and TLS - certificates. +* The **root network CA** that defines the extent of a compatibility zone +* The **doorman CA** that is used instead of the root network CA for day-to-day key signing to reduce the risk of the root + network CA's private key being compromised. This is equivalent to an intermediate certificate in the web PKI +* Each node also serves as its own CA, issuing the child certificates that it uses to sign its identity keys and TLS + certificates -Each certificate has an X.509 extension in it that defines the certificate/key's role in the system (see below for details). -They also use X.509 name constraints to ensure that the X.500 names that encode a human meaningful identity are propagated +Each certificate contains an X.509 extension that defines the certificate/key's role in the system (see below for details). +It also uses X.509 name constraints to ensure that the X.500 names that encode human meaningful identities are propagated to all the child certificates properly. The following constraints are imposed: -* Doorman certificates are issued by a network root. Network root certs do not contain a role extension. -* Node certificates are signed by a doorman certificate (as defined by the extension). -* Legal identity/TLS certificates are issued by a certificate marked as node CA. -* Confidential identity certificates are issued by a certificate marked as well known legal identity. -* Party certificates are marked as either a well known identity or a confidential identity. +* Doorman certificates are issued by a network root. Network root certs do not contain a role extension +* Node certificates are signed by a doorman certificate (as defined by the extension) +* Legal identity/TLS certificates are issued by a certificate marked as node CA +* Confidential identity certificates are issued by a certificate marked as well known legal identity +* Party certificates are marked as either a well known identity or a confidential identity -The structure of certificates above Doorman/Network map is intentionally left untouched, as they are not relevant to +The structure of certificates above the doorman/network map is intentionally left untouched, as they are not relevant to the identity service and therefore there is no advantage in enforcing a specific structure on those certificates. The certificate hierarchy consistency checks are required because nodes can issue their own certificates and can set their own role flags on certificates, and it's important to verify that these are set consistently with the @@ -58,23 +43,29 @@ We can visualise the permissioning structure as follows: :scale: 55% :align: center -Keypair and certificate formats -------------------------------- +Key pair and certificate formats +-------------------------------- -You can use any standard key tools to create the required public/private keypairs and certificates. The keypairs and -certificates must obey the following restrictions: +The required key pairs and certificates take the form of the following Java-style keystores (this may change in future to +support PKCS#12 keystores) in the node's ``<workspace>/certificates/`` folder: + +* ``network-root-truststore.jks``, the network/zone operator's root certificate as provided by them with a standard password. Can be deleted after initial registration +* ``truststore.jks``, the network/zone operator's root certificate in keystore with a locally configurable password as protection against certain attacks +* ``nodekeystore.jks``, which stores the node’s identity key pairs and certificates +* ``sslkeystore.jks``, which stores the node’s TLS key pair and certificate + +The key pairs and certificates must obey the following restrictions: 1. The certificates must follow the `X.509v3 standard <https://tools.ietf.org/html/rfc5280>`__ 2. The TLS certificates must follow the `TLS v1.2 standard <https://tools.ietf.org/html/rfc5246>`__ 3. The root network CA, doorman CA, and node CA keys, as well as the node TLS keys, must follow one of the following schemes: + * ECDSA using the NIST P-256 curve (secp256r1) * ECDSA using the Koblitz k1 curve (secp256k1) - * RSA with 3072-bit key size or higher. + * RSA with 3072-bit key size or higher -The required identity and TLS keys/certificates will be automatically generated for you by the node on first run. -However, you can also generate them manually for more control. The ``X509Utilities`` class shows how to generate the -required public/private keypairs and certificates using Bouncy Castle. You can find it in the `Corda repository -<https://github.com/corda/corda/blob/master/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt>`__. +4. The node CA certificates must have the basic constraints extension set to true +5. The TLS certificates must have the basic constraints extension set to false Certificate role extension -------------------------- @@ -97,235 +88,3 @@ it is important to recognise these extensions and the constraints noted above. Certificate path validation is extended so that a certificate must contain the extension if the extension was present in the certificate of the issuer. - - -Manually creating the node keys -------------------------------- - -The node expects a Java-style key store (this may change in future to support PKCS#12 keystores) called ``nodekeystore.jks``, -with the private key and certificate having an alias of "cordaclientca". This certificate should be signed by the -doorman CA for your network. The basic constraints extension must be set to true. - -For the TLS keys, the basic constraints extension must be set to false. The keystore name is ``sslkeystore.jks`` and -the key alias must be ``cordaclienttls``. - -These two files should be in the node's certificate directory (``<workspace>/certificates/``), along with the network's -own root certificates in a ``network-root-truststore.jks`` file. - -Connecting to a compatibility zone ----------------------------------- - -To connect to a compatibility zone you need to register with their certificate signing authority (doorman) by submitting -a certificate signing request (CSR) to obtain a valid identity for the zone. You could do this out of band, for instance -via email or a web form, but there's also a simple request/response protocol built into Corda. - -Before you can register, you must first have received the trust store file containing the root certificate from the zone -operator. For high security zones this might be delivered physically. Then run the following command: - -``java -jar corda.jar --initial-registration --network-root-truststore-password <trust store password>`` - -By default it will expect the trust store file to be in the location ``certificates/network-root-truststore.jks``. -This can be overridden with the additional ``--network-root-truststore`` flag. - -The certificate signing request will be created based on node information obtained from the node configuration. -The following information from the node configuration file is needed to generate the request. - -* **myLegalName** Your company's legal name as an X.500 string. X.500 allows differentiation between entities with the same - name, as the legal name needs to be unique on the network. If another node has already been permissioned with this - name then the permissioning server will automatically reject the request. The request will also be rejected if it - violates legal name rules, see :ref:`node_naming` for more information. You can use the X.500 schema to disambiguate - entities that have the same or similar brand names. - -* **emailAddress** e.g. "admin@company.com" - -* **devMode** must be set to false - -* **networkServices** or **compatibilityZoneURL** The Corda compatibility zone services must be configured. This must be either: - - * **compatibilityZoneURL** The Corda compatibility zone network management service root URL. - * **networkServices** Replaces the ``compatibilityZoneURL`` when the doorman and network map services - are configured to operate on different URL endpoints. The ``doorman`` entry is used for registration. - -A new pair of private and public keys generated by the Corda node will be used to create the request. - -The utility will submit the request to the doorman server and poll for a result periodically to retrieve the -certificates. Once the request has been approved and the certificates downloaded from the server, the node will create -the keystore and trust store using the certificates and the generated private key. - -.. note:: You can exit the utility at any time if the approval process is taking longer than expected. The request - process will resume on restart as long as the ``--initial-registration`` flag is specified. - -This process only is needed when the node connects to the network for the first time, or when the certificate expires. - -Creating your own compatibility zone ------------------------------------- - -This section documents how to implement your own doorman and network map servers, which is the basic process required to -create a dedicated zone. At this time we do not provide tooling to do this, because the needs of each zone are different -and no generic, configurable doorman codebase has been written. - -Do you need a zone? -^^^^^^^^^^^^^^^^^^^ - -Think twice before going down this route: - -1. It isn't necessary for testing. -2. It isn't necessary for adding another layer of permissioning or 'know your customer' requirements onto your app. - -**Testing.** Creating a production-ready zone isn't necessary for testing as you can use the :doc:`network bootstrapper <network-bootstrapper>` -tool to create all the certificates, keys, and distribute the needed map files to run many nodes. The bootstrapper can -create a network locally on your desktop/laptop but it also knows how to automate cloud providers via their APIs and -using Docker. In this way you can bring up a simulation of a real Corda network with different nodes on different -machines in the cloud for your own testing. Testing this way has several advantages, most obviously that you avoid -race conditions in your tests caused by nodes/tests starting before all map data has propagated to all nodes. -You can read more about the reasons for the creation of the bootstrapper tool -`in a blog post on the design thinking behind Corda's network map infrastructure <https://medium.com/corda/cordas-new-network-map-infrastructure-8c4c248fd7f3>`__. - -**Permissioning.** And creating a zone is also unnecessary for imposing permissioning requirements beyond that of the -base Corda network. You can control who can use your app by creating a *business network*. A business network is what we -call a coalition of nodes that have chosen to run a particular app within a given commercial context. Business networks -aren't represented in the Corda API at this time, partly because the technical side is so simple. You can create one -via a simple three step process: - -1. Distribute a list of X.500 names that are members of your business network, e.g. a simple way to do this is by - hosting a text file with one name per line on your website at a fixed HTTPS URL. You could also write a simple - request/response flow that serves the list over the Corda protocol itself, although this requires the business - network to have a node for itself. -2. Write a bit of code that downloads and caches the contents of this file on disk, and which loads it into memory in - the node. A good place to do this is in a class annotated with ``@CordaService``, because this class can expose - a ``Set<Party>`` field representing the membership of your service. -3. In your flows use ``serviceHub.findService`` to get a reference to your ``@CordaService`` class, read the list of - members and at the start of each flow, throw a FlowException if the counterparty isn't in the membership list. - -In this way you can impose a centrally controlled ACL that all members will collectively enforce. - -.. note:: A production-ready Corda network and a new iteration of the testnet will be available soon. - -Why create your own zone? -^^^^^^^^^^^^^^^^^^^^^^^^^ - -The primary reason to create a zone and provide the associated infrastructure is control over *network parameters*. These -are settings that control Corda's operation, and on which all users in a network must agree. Failure to agree would create -the Corda equivalent of a blockchain "hard fork". Parameters control things like the root of identity, -how quickly users should upgrade, how long nodes can be offline before they are evicted from the system and so on. - -Creating a zone involves the following steps: - -1. Create the zone private keys and certificates. This procedure is conventional and no special knowledge is required: - any self-signed set of certificates can be used. A professional quality zone will probably keep the keys inside a - hardware security module (as the main Corda network and test networks do). -2. Write a network map server. -3. Optionally, create a doorman server. -4. Finally, you would select and generate your network parameter file. - -Writing a network map server -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This server implements a simple HTTP based protocol described in the ":doc:`network-map`" page. -The map server is responsible for gathering NodeInfo files from nodes, storing them, and distributing them back to the -nodes in the zone. By doing this it is also responsible for choosing who is in and who is out: having a signed -identity certificate is not enough to be a part of a Corda zone, you also need to be listed in the network map. -It can be thought of as a DNS equivalent. If you want to de-list a user, you would do it here. - -It is very likely that your map server won't be entirely standalone, but rather, integrated with whatever your master -user database is. - -The network map server also distributes signed network parameter files and controls the roll-out schedule for when they -become available for download and opt-in, and when they become enforced. This is again a policy decision you will -probably choose to place some simple UI or workflow tooling around, in particular to enforce restrictions on who can -edit the map or the parameters. - -Writing a doorman server -^^^^^^^^^^^^^^^^^^^^^^^^ - -This step is optional because your users can obtain a signed certificate in many different ways. The doorman protocol -is again a very simple HTTP based approach in which a node creates keys and requests a certificate, polling until it -gets back what it expects. However, you could also integrate this process with the rest of your signup process. For example, -by building a tool that's integrated with your payment flow (if payment is required to take part in your zone at all). -Alternatively you may wish to distribute USB smartcard tokens that generate the private key on first use, as is typically -seen in national PKIs. There are many options. - -If you do choose to make a doorman server, the bulk of the code you write will be workflow related. For instance, -related to keeping track of an applicant as they proceed through approval. You should also impose any naming policies -you have in the doorman process. If names are meant to match identities registered in government databases then that -should be enforced here, alternatively, if names can be self-selected or anonymous, you would only bother with a -deduplication check. Again it will likely be integrated with a master user database. - -Corda does not currently provide a doorman or network map service out of the box, partly because when stripped of the -zone specific policy there isn't much to them: just a basic HTTP server that most programmers will have favourite -frameworks for anyway. - -The protocol is: - -* If $URL = ``https://some.server.com/some/path`` -* Node submits a PKCS#10 certificate signing request using HTTP POST to ``$URL/certificate``. It will have a MIME - type of ``application/octet-stream``. The ``Client-Version`` header is set to be "1.0". -* The server returns an opaque string that references this request (let's call it ``$requestid``, or an HTTP error if something went wrong. -* The returned request ID should be persisted to disk, to handle zones where approval may take a long time due to manual - intervention being required. -* The node starts polling ``$URL/$requestid`` using HTTP GET. The poll interval can be controlled by the server returning - a response with a ``Cache-Control`` header. -* If the request is answered with a ``200 OK`` response, the body is expected to be a zip file. Each file is expected to - be a binary X.509 certificate, and the certs are expected to be in order. -* If the request is answered with a ``204 No Content`` response, the node will try again later. -* If the request is answered with a ``403 Not Authorized`` response, the node will treat that as request rejection and give up. -* Other response codes will cause the node to abort with an exception. - -Setting zone parameters -^^^^^^^^^^^^^^^^^^^^^^^ - -Zone parameters are stored in a file containing a Corda AMQP serialised ``SignedDataWithCert<NetworkParameters>`` -object. It is easy to create such a file with a small Java or Kotlin program. The ``NetworkParameters`` object is a -simple data holder that could be read from e.g. a config file, or settings from a database. Signing and saving the -resulting file is just a few lines of code. A full example can be found in `NetworkParametersCopier.kt -<https://github.com/corda/corda/blob/master/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkParametersCopier.kt>`__, -but a flavour of it looks like this: - -.. container:: codeset - - .. sourcecode:: java - - NetworkParameters networkParameters = new NetworkParameters( - 4, // minPlatformVersion - Collections.emptyList(), // notaries - 1024 * 1024 * 20, // maxMessageSize - 1024 * 1024 * 15, // maxTransactionSize - Instant.now(), // modifiedTime - 2, // epoch - Collections.emptyMap() // whitelist - ); - CertificateAndKeyPair signingCertAndKeyPair = loadNetworkMapCA(); - SerializedBytes<SignedDataWithCert<NetworkParameters>> bytes = SerializedBytes.from(netMapCA.sign(networkParameters)); - Files.copy(bytes.open(), Paths.get("params-file")); - - .. sourcecode:: kotlin - - val networkParameters = NetworkParameters( - minimumPlatformVersion = 4, - notaries = listOf(...), - maxMessageSize = 1024 * 1024 * 20 // 20mb, for example. - maxTransactionSize = 1024 * 1024 * 15, - modifiedTime = Instant.now(), - epoch = 2, - ... etc ... - ) - val signingCertAndKeyPair: CertificateAndKeyPair = loadNetworkMapCA() - val signedParams: SerializedBytes<SignedNetworkParameters> = signingCertAndKeyPair.sign(networkParameters).serialize() - signedParams.open().copyTo(Paths.get("/some/path")) - -Each individual parameter is documented in `the JavaDocs/KDocs for the NetworkParameters class -<https://docs.corda.net/api/kotlin/corda/net.corda.core.node/-network-parameters/index.html>`__. The network map -certificate is usually chained off the root certificate, and can be created according to the instructions above. Each -time the zone parameters are changed, the epoch should be incremented. Epochs are essentially version numbers for the -parameters, and they therefore cannot go backwards. Once saved, the new parameters can be served by the network map server. - -Selecting parameter values -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -How to choose the parameters? This is the most complex question facing you as a new zone operator. Some settings may seem -straightforward and others may involve cost/benefit trade-offs specific to your business. For example, you could choose -to run a validating notary yourself, in which case you would (in the absence of SGX) see all the users' data. Or you could -run a non-validating notary, with BFT fault tolerance, which implies recruiting others to take part in the cluster. - -New network parameters will be added over time as Corda evolves. You will need to ensure that when your users upgrade, -all the new network parameters are being served. You can ask for advice on the `corda-dev mailing list <https://groups.io/g/corda-dev>`__. \ No newline at end of file diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index cbb0d183ce..3f3e0e9d60 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -6,6 +6,9 @@ Unreleased Significant Changes in 4.0 ~~~~~~~~~~~~~~~~~~~~~~~~~~ +* **Retirement of non-elliptic Diffie-Hellman for TLS** + The TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 family of ciphers is retired from the list of allowed ciphers for TLS + as it is a legacy cipher family not supported by all native SSL/TLS implementations. * **Reference states**: diff --git a/docs/source/running-a-node.rst b/docs/source/running-a-node.rst index 14c6f91268..69264383a0 100644 --- a/docs/source/running-a-node.rst +++ b/docs/source/running-a-node.rst @@ -47,22 +47,27 @@ Command-line options ~~~~~~~~~~~~~~~~~~~~ The node can optionally be started with the following command-line options: -* ``--base-directory``: The node working directory where all the files are kept (default: ``.``) +* ``--base-directory``, ``-b``: The node working directory where all the files are kept (default: ``.``). * ``--bootstrap-raft-cluster``: Bootstraps Raft cluster. The node forms a single node cluster (ignoring otherwise configured peer - addresses), acting as a seed for other nodes to join the cluster -* ``--config-file``: The path to the config file (default: ``node.conf``) -* ``--help`` -* ``--initial-registration``: Start initial node registration with Corda network to obtain certificate from the permissioning - server -* ``--just-generate-node-info``: Perform the node start-up task necessary to generate its nodeInfo, save it to disk, then - quit -* ``--log-to-console``: If set, prints logging to the console as well as to a file -* ``--logging-level <[ERROR,WARN,INFO, DEBUG,TRACE]>``: Enable logging at this level and higher (default: INFO) -* ``--network-root-truststore``: Network root trust store obtained from network operator -* ``--network-root-truststore-password``: Network root trust store password obtained from network operator -* ``--no-local-shell``: Do not start the embedded shell locally -* ``--sshd``: Enables SSHD server for node administration -* ``--version``: Print the version and exit + addresses), acting as a seed for other nodes to join the cluster. +* ``--clear-network-map-cache``, ``-c``: Clears local copy of network map, on node startup it will be restored from server or file system. +* ``--config-file``, ``-f``: The path to the config file. Defaults to ``node.conf``. +* ``--dev-mode``, ``-d``: Runs the node in developer mode. Unsafe in production. Defaults to true on MacOS and desktop versions of Windows. False otherwise. +* ``--initial-registration``: Start initial node registration with the compatibility zone to obtain a certificate from the Doorman. +* ``--just-generate-node-info``: Perform the node start-up task necessary to generate its nodeInfo, save it to disk, then + quit. +* ``--just-generate-rpc-ssl-settings``: Generate the ssl keystore and truststore for a secure RPC connection. +* ``--network-root-truststore``, ``-t``: Network root trust store obtained from network operator. +* ``--network-root-truststore-password``, ``-p``: Network root trust store password obtained from network operator. +* ``--no-local-shell``, ``-n``: Do not start the embedded shell locally. +* ``--on-unknown-config-keys <[FAIL,WARN,INFO]>``: How to behave on unknown node configuration. Defaults to FAIL. +* ``--sshd``: Enables SSH server for node administration. +* ``--sshd-port``: Sets the port for the SSH server. If not supplied and SSH server is enabled, the port defaults to 2222. +* ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file. +* ``--logging-level=<loggingLevel>``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO. +* ``--install-shell-extensions``: Install ``corda`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. +* ``--help``, ``-h``: Show this help message and exit. +* ``--version``, ``-V``: Print version information and exit. .. _enabling-remote-debugging: diff --git a/docs/source/serialization-index.rst b/docs/source/serialization-index.rst index a01f6b15db..6ec054bd0b 100644 --- a/docs/source/serialization-index.rst +++ b/docs/source/serialization-index.rst @@ -2,11 +2,10 @@ Serialization ============= .. toctree:: - :caption: Other docs :maxdepth: 1 serialization.rst cordapp-custom-serializers serialization-default-evolution.rst serialization-enum-evolution.rst - blob-inspector \ No newline at end of file + blob-inspector diff --git a/docs/source/serialization.rst b/docs/source/serialization.rst index beb8ba9767..c015eb317e 100644 --- a/docs/source/serialization.rst +++ b/docs/source/serialization.rst @@ -57,7 +57,7 @@ Classes get onto the whitelist via one of three mechanisms: The annotation is the preferred method for whitelisting. An example is shown in :doc:`tutorial-clientrpc-api`. It's reproduced here as an example of both ways you can do this for a couple of example classes. -.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt +.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/ClientRpcTutorial.kt :language: kotlin :start-after: START 7 :end-before: END 7 diff --git a/docs/source/setting-up-a-corda-network.rst b/docs/source/setting-up-a-corda-network.rst index 99fdf861ea..320cca5f0d 100644 --- a/docs/source/setting-up-a-corda-network.rst +++ b/docs/source/setting-up-a-corda-network.rst @@ -5,73 +5,186 @@ Setting up a Corda network .. contents:: -A Corda network consists of a number of machines running nodes. These nodes communicate using persistent protocols in -order to create and validate transactions. +Bootstrapping a development network +----------------------------------- -There are three broader categories of functionality one such node may have. These pieces of functionality are provided -as services, and one node may run several of them. +When testing CorDapps during development, you should use the :doc:`bootstrapper tool <network-bootstrapper>` to create +a local test network. -* Notary: Nodes running a notary service witness state spends and have the final say in whether a transaction is a - double-spend or not -* Oracle: Network services that link the ledger to the outside world by providing facts that affect the validity of - transactions -* Regular node: All nodes have a vault and may start protocols communicating with other nodes, notaries and oracles and - evolve their private ledger +Creating your own compatibility zone +------------------------------------ -Setting up your own network ---------------------------- +This section documents how to implement your own doorman and network map servers, which is the basic process required to +create a dedicated zone. At this time we do not provide tooling to do this, because the needs of each zone are different +and no generic, configurable doorman codebase has been written. -Certificates -~~~~~~~~~~~~ +Do you need a zone? +^^^^^^^^^^^^^^^^^^^ -Every node in a given Corda network must have an identity certificate signed by the network's root CA. See -:doc:`permissioning` for more information. +Think twice before going down this route: -Configuration -~~~~~~~~~~~~~ +1. It isn't necessary for testing. +2. It isn't necessary for adding another layer of permissioning or 'know your customer' requirements onto your app. -A node can be configured by adding/editing ``node.conf`` in the node's directory. For details see :doc:`corda-configuration-file`. +**Testing.** Creating a production-ready zone isn't necessary for testing as you can use the :doc:`network bootstrapper <network-bootstrapper>` +tool to create all the certificates, keys, and distribute the needed map files to run many nodes. The bootstrapper can +create a network locally on your desktop/laptop but it also knows how to automate cloud providers via their APIs and +using Docker. In this way you can bring up a simulation of a real Corda network with different nodes on different +machines in the cloud for your own testing. Testing this way has several advantages, most obviously that you avoid +race conditions in your tests caused by nodes/tests starting before all map data has propagated to all nodes. +You can read more about the reasons for the creation of the bootstrapper tool +`in a blog post on the design thinking behind Corda's network map infrastructure <https://medium.com/corda/cordas-new-network-map-infrastructure-8c4c248fd7f3>`__. -An example configuration: +**Permissioning.** And creating a zone is also unnecessary for imposing permissioning requirements beyond that of the +base Corda network. You can control who can use your app by creating a *business network*. A business network is what we +call a coalition of nodes that have chosen to run a particular app within a given commercial context. Business networks +aren't represented in the Corda API at this time, partly because the technical side is so simple. You can create one +via a simple three step process: -.. literalinclude:: example-code/src/main/resources/example-node.conf - :language: cfg +1. Distribute a list of X.500 names that are members of your business network, e.g. a simple way to do this is by + hosting a text file with one name per line on your website at a fixed HTTPS URL. You could also write a simple + request/response flow that serves the list over the Corda protocol itself, although this requires the business + network to have a node for itself. +2. Write a bit of code that downloads and caches the contents of this file on disk, and which loads it into memory in + the node. A good place to do this is in a class annotated with ``@CordaService``, because this class can expose + a ``Set<Party>`` field representing the membership of your service. +3. In your flows use ``serviceHub.findService`` to get a reference to your ``@CordaService`` class, read the list of + members and at the start of each flow, throw a FlowException if the counterparty isn't in the membership list. -The most important fields regarding network configuration are: +In this way you can impose a centrally controlled ACL that all members will collectively enforce. -* ``p2pAddress``: This specifies a host and port to which Artemis will bind for messaging with other nodes. Note that the - address bound will **NOT** be ``my-corda-node``, but rather ``::`` (all addresses on all network interfaces). The hostname specified - is the hostname *that must be externally resolvable by other nodes in the network*. In the above configuration this is the - resolvable name of a machine in a VPN. -* ``rpcAddress``: The address to which Artemis will bind for RPC calls. -* ``notary.serviceLegalName``: The name of the notary service, required to setup distributed notaries with the network-bootstrapper. +.. note:: A production-ready Corda network and a new iteration of the testnet will be available soon. -Starting the nodes -~~~~~~~~~~~~~~~~~~ +Why create your own zone? +^^^^^^^^^^^^^^^^^^^^^^^^^ -You will first need to create the local network by bootstrapping it with the bootstrapper. Details of how to do that can -be found in :doc:`network-bootstrapper`. +The primary reason to create a zone and provide the associated infrastructure is control over *network parameters*. These +are settings that control Corda's operation, and on which all users in a network must agree. Failure to agree would create +the Corda equivalent of a blockchain "hard fork". Parameters control things like the root of identity, +how quickly users should upgrade, how long nodes can be offline before they are evicted from the system and so on. -Once that's done you may now start the nodes in any order. You should see a banner, some log lines and eventually -``Node started up and registered``, indicating that the node is fully started. +Creating a zone involves the following steps: -In terms of process management there is no prescribed method. You may start the jars by hand or perhaps use systemd and friends. +1. Create the zone private keys and certificates. This procedure is conventional and no special knowledge is required: + any self-signed set of certificates can be used. A professional quality zone will probably keep the keys inside a + hardware security module (as the main Corda network and test networks do). +2. Write a network map server. +3. Optionally, create a doorman server. +4. Finally, you would select and generate your network parameter file. -Logging -~~~~~~~ +Writing a network map server +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Only a handful of important lines are printed to the console. For -details/diagnosing problems check the logs. +This server implements a simple HTTP based protocol described in the ":doc:`network-map`" page. +The map server is responsible for gathering NodeInfo files from nodes, storing them, and distributing them back to the +nodes in the zone. By doing this it is also responsible for choosing who is in and who is out: having a signed +identity certificate is not enough to be a part of a Corda zone, you also need to be listed in the network map. +It can be thought of as a DNS equivalent. If you want to de-list a user, you would do it here. -Logging is standard log4j2_ and may be configured accordingly. Logs -are by default redirected to files in ``NODE_DIRECTORY/logs/``. +It is very likely that your map server won't be entirely standalone, but rather, integrated with whatever your master +user database is. -Connecting to the nodes -~~~~~~~~~~~~~~~~~~~~~~~ +The network map server also distributes signed network parameter files and controls the roll-out schedule for when they +become available for download and opt-in, and when they become enforced. This is again a policy decision you will +probably choose to place some simple UI or workflow tooling around, in particular to enforce restrictions on who can +edit the map or the parameters. -Once a node has started up successfully you may connect to it as a client to initiate protocols/query state etc. -Depending on your network setup you may need to tunnel to do this remotely. +Writing a doorman server +^^^^^^^^^^^^^^^^^^^^^^^^ -See the :doc:`tutorial-clientrpc-api` on how to establish an RPC link. +This step is optional because your users can obtain a signed certificate in many different ways. The doorman protocol +is again a very simple HTTP based approach in which a node creates keys and requests a certificate, polling until it +gets back what it expects. However, you could also integrate this process with the rest of your signup process. For example, +by building a tool that's integrated with your payment flow (if payment is required to take part in your zone at all). +Alternatively you may wish to distribute USB smartcard tokens that generate the private key on first use, as is typically +seen in national PKIs. There are many options. -Sidenote: A client is always associated with a single node with a single identity, which only sees their part of the ledger. +If you do choose to make a doorman server, the bulk of the code you write will be workflow related. For instance, +related to keeping track of an applicant as they proceed through approval. You should also impose any naming policies +you have in the doorman process. If names are meant to match identities registered in government databases then that +should be enforced here, alternatively, if names can be self-selected or anonymous, you would only bother with a +deduplication check. Again it will likely be integrated with a master user database. + +Corda does not currently provide a doorman or network map service out of the box, partly because when stripped of the +zone specific policy there isn't much to them: just a basic HTTP server that most programmers will have favourite +frameworks for anyway. + +The protocol is: + +* If $URL = ``https://some.server.com/some/path`` +* Node submits a PKCS#10 certificate signing request using HTTP POST to ``$URL/certificate``. It will have a MIME + type of ``application/octet-stream``. The ``Platform-Version`` header is set to be "1.0" and the ``Client-Version`` header to reflect the node software version +* The server returns an opaque string that references this request (let's call it ``$requestid``, or an HTTP error if something went wrong +* The returned request ID should be persisted to disk, to handle zones where approval may take a long time due to manual + intervention being required +* The node starts polling ``$URL/$requestid`` using HTTP GET. The poll interval can be controlled by the server returning + a response with a ``Cache-Control`` header +* If the request is answered with a ``200 OK`` response, the body is expected to be a zip file. Each file is expected to + be a binary X.509 certificate, and the certs are expected to be in order +* If the request is answered with a ``204 No Content`` response, the node will try again later +* If the request is answered with a ``403 Not Authorized`` response, the node will treat that as request rejection and give up +* Other response codes will cause the node to abort with an exception + +You can use any standard key tools to create the required key pairs and certificates. The ``X509Utilities`` class in the +`Corda repository +<https://github.com/corda/corda/blob/master/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt>`__ +shows how to generate the required key pairs and certificates using Bouncy Castle. + +Setting zone parameters +^^^^^^^^^^^^^^^^^^^^^^^ + +Zone parameters are stored in a file containing a Corda AMQP serialised ``SignedDataWithCert<NetworkParameters>`` +object. It is easy to create such a file with a small Java or Kotlin program. The ``NetworkParameters`` object is a +simple data holder that could be read from e.g. a config file, or settings from a database. Signing and saving the +resulting file is just a few lines of code. A full example can be found in `NetworkParametersCopier.kt +<https://github.com/corda/corda/blob/master/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkParametersCopier.kt>`__, +but a flavour of it looks like this: + +.. container:: codeset + + .. sourcecode:: java + + NetworkParameters networkParameters = new NetworkParameters( + 4, // minPlatformVersion + Collections.emptyList(), // notaries + 1024 * 1024 * 20, // maxMessageSize + 1024 * 1024 * 15, // maxTransactionSize + Instant.now(), // modifiedTime + 2, // epoch + Collections.emptyMap() // whitelist + ); + CertificateAndKeyPair signingCertAndKeyPair = loadNetworkMapCA(); + SerializedBytes<SignedDataWithCert<NetworkParameters>> bytes = SerializedBytes.from(netMapCA.sign(networkParameters)); + Files.copy(bytes.open(), Paths.get("params-file")); + + .. sourcecode:: kotlin + + val networkParameters = NetworkParameters( + minimumPlatformVersion = 4, + notaries = listOf(...), + maxMessageSize = 1024 * 1024 * 20 // 20mb, for example. + maxTransactionSize = 1024 * 1024 * 15, + modifiedTime = Instant.now(), + epoch = 2, + ... etc ... + ) + val signingCertAndKeyPair: CertificateAndKeyPair = loadNetworkMapCA() + val signedParams: SerializedBytes<SignedNetworkParameters> = signingCertAndKeyPair.sign(networkParameters).serialize() + signedParams.open().copyTo(Paths.get("/some/path")) + +Each individual parameter is documented in `the JavaDocs/KDocs for the NetworkParameters class +<https://docs.corda.net/api/kotlin/corda/net.corda.core.node/-network-parameters/index.html>`__. The network map +certificate is usually chained off the root certificate, and can be created according to the instructions above. Each +time the zone parameters are changed, the epoch should be incremented. Epochs are essentially version numbers for the +parameters, and they therefore cannot go backwards. Once saved, the new parameters can be served by the network map server. + +Selecting parameter values +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +How to choose the parameters? This is the most complex question facing you as a new zone operator. Some settings may seem +straightforward and others may involve cost/benefit trade-offs specific to your business. For example, you could choose +to run a validating notary yourself, in which case you would (in the absence of SGX) see all the users' data. Or you could +run a non-validating notary, with BFT fault tolerance, which implies recruiting others to take part in the cluster. + +New network parameters will be added over time as Corda evolves. You will need to ensure that when your users upgrade, +all the new network parameters are being served. You can ask for advice on the `corda-dev mailing list <https://groups.io/g/corda-dev>`__. \ No newline at end of file diff --git a/docs/source/shell.rst b/docs/source/shell.rst index 73ef8f0a6d..43fc87aae5 100644 --- a/docs/source/shell.rst +++ b/docs/source/shell.rst @@ -92,6 +92,8 @@ Windows Windows does not provide a built-in SSH tool. An alternative such as PuTTY should be used. +.. _standalone-shell: + The standalone shell -------------------- The standalone shell is a standalone application interacting with a Corda node via RPC calls. @@ -117,26 +119,34 @@ Windows .. code:: bash - java -jar corda-tools-shell-cli-VERSION_NUMBER.jar [--config-file PATH | --cordpass-directory PATH --commands-directory PATH --host HOST --port PORT - --user USER --password PASSWORD --sshd-port PORT --sshd-hostkey-directory PATH --keystore-password PASSWORD - --keystore-file FILE --truststore-password PASSWORD --truststore-file FILE | --help] + corda-shell [-hvV] [--install-shell-extensions] + [--logging-level=<loggingLevel>] [--password=<password>] + [--sshd-hostkey-directory=<sshdHostKeyDirectory>] + [--sshd-port=<sshdPort>] [--truststore-file=<trustStoreFile>] + [--truststore-password=<trustStorePassword>] + [--truststore-type=<trustStoreType>] [--user=<user>] [-a=<host>] + [-c=<cordappDirectory>] [-f=<configFile>] [-o=<commandsDirectory>] + [-p=<port>] Where: -* ``config-file`` is the path to config file, used instead of providing the rest of command line options -* ``cordpass-directory`` is the directory containing Cordapps jars, Cordapps are require when starting flows -* ``commands-directory`` is the directory with additional CrAsH shell commands -* ``host`` is the Corda node's host -* ``port`` is the Corda node's port, specified in the ``node.conf`` file -* ``user`` is the RPC username, if not provided it will be requested at startup -* ``password`` is the RPC user password, if not provided it will be requested at startup -* ``sshd-port`` instructs the standalone shell app to start SSH server on the given port, optional -* ``sshd-hostkey-directory`` is the directory containing hostkey.pem file for SSH server -* ``keystore-password`` the password to unlock the KeyStore file containing the standalone shell certificate and private key, optional, unencrypted RPC connection without SSL will be used if the option is not provided -* ``keystore-file`` is the path to the KeyStore file -* ``truststore-password`` the password to unlock the TrustStore file containing the Corda node certificate, optional, unencrypted RPC connection without SSL will be used if the option is not provided -* ``truststore-file`` is the path to the TrustStore file -* ``help`` prints Shell help +* ``--config-file=<configFile>``, ``--f`` The path to the shell configuration file, used instead of providing the rest of the command line options. +* ``--cordapp-directory=<cordappDirectory>``, ``-c`` The path to the directory containing CorDapp jars, CorDapps are required when starting flows. +* ``--commands-directory=<commandsDirectory>``, ``-o`` The path to the directory containing additional CRaSH shell commands. +* ``--host``, ``-a``: The host address of the Corda node. +* ``--port``, ``-p``: The RPC port of the Corda node. +* ``--user=<user>``: The RPC user name. +* ``--password=<password>`` The RPC user password. If not provided it will be prompted for on startup. +* ``--sshd-port=<sshdPort>`` Enables SSH server for shell. +* ``--sshd-hostkey-directory=<sshHostKeyDirectory``: The directory containing the hostkey.pem file for the SSH server. +* ``--truststore-password=<trustStorePassword>``: The password to unlock the TrustStore file. +* ``--truststore-file=<trustStoreFile>``: The path to the TrustStore file. +* ``--truststore-type=<trustStoreType>``: The type of the TrustStore (e.g. JKS). +* ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file. +* ``--logging-level=<loggingLevel>``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO. +* ``--install-shell-extensions``: Install ``corda-shell`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. +* ``--help``, ``-h``: Show this help message and exit. +* ``--version``, ``-V``: Print version information and exit. The format of ``config-file``: diff --git a/docs/source/tut-two-party-contract.rst b/docs/source/tut-two-party-contract.rst index 17e4bc77f2..8a94c75bad 100644 --- a/docs/source/tut-two-party-contract.rst +++ b/docs/source/tut-two-party-contract.rst @@ -81,7 +81,7 @@ Let's write a contract that enforces these constraints. We'll do this by modifyi .. container:: codeset - .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/contract.kt + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUContract.kt :language: kotlin :start-after: DOCSTART 01 :end-before: DOCEND 01 diff --git a/docs/source/tut-two-party-flow.rst b/docs/source/tut-two-party-flow.rst index aefa3cfb96..5ed7e5716b 100644 --- a/docs/source/tut-two-party-flow.rst +++ b/docs/source/tut-two-party-flow.rst @@ -21,7 +21,7 @@ In ``IOUFlow.java``/``App.kt``, change the imports block to the following: .. container:: codeset - .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlow.kt :language: kotlin :start-after: DOCSTART 01 :end-before: DOCEND 01 @@ -36,7 +36,7 @@ follows: .. container:: codeset - .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlow.kt :language: kotlin :start-after: DOCSTART 02 :end-before: DOCEND 02 @@ -89,7 +89,7 @@ to respond, we need to write a response flow as well. In a new ``IOUFlowResponde .. container:: codeset - .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flowResponder.kt + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlowResponder.kt :language: kotlin :start-after: DOCSTART 01 :end-before: DOCEND 01 diff --git a/docs/source/tutorial-building-transactions.rst b/docs/source/tutorial-building-transactions.rst index 324cb95ae2..2c5ef076b8 100644 --- a/docs/source/tutorial-building-transactions.rst +++ b/docs/source/tutorial-building-transactions.rst @@ -81,12 +81,12 @@ To give a few more specific details consider two simplified real world scenarios. First, a basic foreign exchange cash transaction. This transaction needs to locate a set of funds to exchange. A flow modelling this is implemented in ``FxTransactionBuildTutorial.kt`` -(see ``docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt`` in the +(see ``docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FxTransactionBuildTutorial.kt`` in the `main Corda repo <https://github.com/corda/corda>`_). Second, a simple business model in which parties manually accept or reject each other's trade proposals, which is implemented in ``WorkflowTransactionBuildTutorial.kt`` (see -``docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt`` in the +``docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/WorkflowTransactionBuildTutorial.kt`` in the `main Corda repo <https://github.com/corda/corda>`_). To run and explore these examples using the IntelliJ IDE one can run/step through the respective unit tests in ``FxTransactionBuildTutorialTest.kt`` and @@ -111,7 +111,7 @@ here will manually carry out the input queries by specifying relevant query criteria filters to the ``tryLockFungibleStatesForSpending`` method of the ``VaultService``. -.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt +.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/FxTransactionBuildTutorial.kt :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 @@ -148,7 +148,7 @@ parameters to the flow to identify the states being operated upon. Thus code to gather the latest input state for a given ``StateRef`` would use the ``VaultService`` as follows: -.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt +.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/WorkflowTransactionBuildTutorial.kt :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 @@ -203,7 +203,7 @@ state. Remember that the result of a successful transaction is always to fully consume/spend the input states, so this is required to conserve the total cash. For example from the demo code: -.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt +.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/FxTransactionBuildTutorial.kt :language: kotlin :start-after: DOCSTART 2 :end-before: DOCEND 2 @@ -221,13 +221,13 @@ and convert it into a ``SignedTransaction``. Examples of this process are: -.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt +.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/WorkflowTransactionBuildTutorial.kt :language: kotlin :start-after: DOCSTART 2 :end-before: DOCEND 2 :dedent: 8 -.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt +.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/FxTransactionBuildTutorial.kt :language: kotlin :start-after: DOCSTART 3 :end-before: DOCEND 3 @@ -260,7 +260,7 @@ context. For example, the flow may need to check that the parties are the right ones, or that the ``Command`` present on the transaction is as expected for this specific flow. An example of this from the demo code is: -.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt +.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/WorkflowTransactionBuildTutorial.kt :language: kotlin :start-after: DOCSTART 3 :end-before: DOCEND 3 @@ -277,7 +277,7 @@ Once all the signatures are applied to the ``SignedTransaction``, the final steps are notarisation and ensuring that all nodes record the fully-signed transaction. The code for this is standardised in the ``FinalityFlow``: -.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt +.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/WorkflowTransactionBuildTutorial.kt :language: kotlin :start-after: DOCSTART 4 :end-before: DOCEND 4 diff --git a/docs/source/tutorial-clientrpc-api.rst b/docs/source/tutorial-clientrpc-api.rst index 597e8a96f9..ea0c82e214 100644 --- a/docs/source/tutorial-clientrpc-api.rst +++ b/docs/source/tutorial-clientrpc-api.rst @@ -19,7 +19,7 @@ and a Alice node that can issue, move and exit cash. Here's how we configure the node to create a user that has the permissions to start the ``CashIssueFlow``, ``CashPaymentFlow``, and ``CashExitFlow``: -.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt +.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/ClientRpcTutorial.kt :language: kotlin :start-after: START 1 :end-before: END 1 @@ -27,7 +27,7 @@ Here's how we configure the node to create a user that has the permissions to st Now we can connect to the node itself using a valid RPC user login and start generating transactions in a different thread using ``generateTransactions`` (to be defined later): -.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt +.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/ClientRpcTutorial.kt :language: kotlin :start-after: START 2 :end-before: END 2 @@ -45,7 +45,7 @@ signature tells us that the RPC operation will return a list of transactions and general pattern, we query some data and the node will return the current snapshot and future updates done to it. Observables are described in further detail in :doc:`clientrpc` -.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt +.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/ClientRpcTutorial.kt :language: kotlin :start-after: START 3 :end-before: END 3 @@ -56,7 +56,7 @@ The graph will be defined as follows: * Each transaction is a vertex, represented by printing ``NODE <txhash>`` * Each input-output relationship is an edge, represented by printing ``EDGE <txhash> <txhash>`` -.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt +.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/ClientRpcTutorial.kt :language: kotlin :start-after: START 4 :end-before: END 4 @@ -64,7 +64,7 @@ The graph will be defined as follows: Now we just need to create the transactions themselves! -.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt +.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/ClientRpcTutorial.kt :language: kotlin :start-after: START 6 :end-before: END 6 @@ -91,7 +91,7 @@ listening on successfully created ones, which are dumped to the console. We just Now let's try to visualise the transaction graph. We will use a graph drawing library called graphstream_. -.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt +.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/ClientRpcTutorial.kt :language: kotlin :start-after: START 5 :end-before: END 5 @@ -105,7 +105,7 @@ Whitelisting classes from your CorDapp with the Corda node As described in :doc:`clientrpc`, you have to whitelist any additional classes you add that are needed in RPC requests or responses with the Corda node. Here's an example of both ways you can do this for a couple of example classes. -.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt +.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/ClientRpcTutorial.kt :language: kotlin :start-after: START 7 :end-before: END 7 diff --git a/docs/source/tutorial-contract.rst b/docs/source/tutorial-contract.rst index 7a53e176aa..d47cb643d7 100644 --- a/docs/source/tutorial-contract.rst +++ b/docs/source/tutorial-contract.rst @@ -77,7 +77,7 @@ A state is a class that stores data that is checked by the contract. A commercia .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/contract/TutorialContract.kt :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 @@ -129,7 +129,7 @@ Let's define a few commands now: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/contract/TutorialContract.kt :language: kotlin :start-after: DOCSTART 2 :end-before: DOCEND 2 @@ -160,7 +160,7 @@ run two contracts one time each: Cash and CommercialPaper. .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/contract/TutorialContract.kt :language: kotlin :start-after: DOCSTART 3 :end-before: DOCEND 3 @@ -285,7 +285,7 @@ logic. .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/contract/TutorialContract.kt :language: kotlin :start-after: DOCSTART 4 :end-before: DOCEND 4 @@ -404,7 +404,7 @@ a method to wrap up the issuance process: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/contract/TutorialContract.kt :language: kotlin :start-after: DOCSTART 5 :end-before: DOCEND 5 @@ -430,7 +430,7 @@ defined inside the contract itself .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/contract/TutorialContract.kt :language: kotlin :start-after: DOCSTART 8 :end-before: DOCEND 8 @@ -464,7 +464,7 @@ What about moving the paper, i.e. reassigning ownership to someone else? .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/contract/TutorialContract.kt :language: kotlin :start-after: DOCSTART 6 :end-before: DOCEND 6 @@ -487,7 +487,7 @@ Finally, we can do redemption. .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/contract/TutorialContract.kt :language: kotlin :start-after: DOCSTART 7 :end-before: DOCEND 7 diff --git a/docs/source/tutorial-cordapp.rst b/docs/source/tutorial-cordapp.rst index 75af66b69a..06789ea9a0 100644 --- a/docs/source/tutorial-cordapp.rst +++ b/docs/source/tutorial-cordapp.rst @@ -396,7 +396,7 @@ please follow the instructions in :doc:`node-database`. Using the example RPC client ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -``/src/main/kotlin-source/com/example/client/ExampleClientRPC.kt`` defines a simple RPC client that connects to a node, +``/src/main/kotlin/com/example/client/ExampleClientRPC.kt`` defines a simple RPC client that connects to a node, logs any existing IOUs and listens for any future IOUs. If you haven't created any IOUs when you first connect to one of the nodes, the client will simply log any future IOUs that are agreed. diff --git a/docs/source/tutorial-tear-offs.rst b/docs/source/tutorial-tear-offs.rst index 4370965414..114257017b 100644 --- a/docs/source/tutorial-tear-offs.rst +++ b/docs/source/tutorial-tear-offs.rst @@ -10,7 +10,7 @@ Each field will only be included if the filtering function returns `true` when t .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/tearoffs/TutorialTearOffs.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/tearoffs/TutorialTearOffs.kt :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 @@ -20,7 +20,7 @@ We can now use our filtering function to construct a ``FilteredTransaction``: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/tearoffs/TutorialTearOffs.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/tearoffs/TutorialTearOffs.kt :language: kotlin :start-after: DOCSTART 2 :end-before: DOCEND 2 @@ -35,7 +35,7 @@ transaction components is exactly the same. Note that unlike ``WireTransaction`` .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/tearoffs/TutorialTearOffs.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/tearoffs/TutorialTearOffs.kt :language: kotlin :start-after: DOCSTART 3 :end-before: DOCEND 3 diff --git a/docs/source/tutorial-test-dsl.rst b/docs/source/tutorial-test-dsl.rst index f568ef6a84..d29125d5ae 100644 --- a/docs/source/tutorial-test-dsl.rst +++ b/docs/source/tutorial-test-dsl.rst @@ -55,13 +55,13 @@ We will start with defining helper function that returns a ``CommercialPaper`` s .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 1 :end-before: DOCEND 1 @@ -122,13 +122,13 @@ last line of ``transaction``: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 2 :end-before: DOCEND 2 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 2 :end-before: DOCEND 2 @@ -138,13 +138,13 @@ Let's take a look at a transaction that fails. .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 3 :end-before: DOCEND 3 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 3 :end-before: DOCEND 3 @@ -167,13 +167,13 @@ However we can specify that this is an intended behaviour by changing ``verifies .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 4 :end-before: DOCEND 4 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 4 :end-before: DOCEND 4 @@ -183,13 +183,13 @@ We can continue to build the transaction until it ``verifies``: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 5 :end-before: DOCEND 5 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 5 :end-before: DOCEND 5 @@ -206,13 +206,13 @@ What should we do if we wanted to test what happens when the wrong party signs t .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 6 :end-before: DOCEND 6 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 6 :end-before: DOCEND 6 @@ -227,13 +227,13 @@ ledger with a single transaction: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 7 :end-before: DOCEND 7 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 7 :end-before: DOCEND 7 @@ -246,13 +246,13 @@ Now that we know how to define a single transaction, let's look at how to define .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 8 :end-before: DOCEND 8 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 8 :end-before: DOCEND 8 @@ -273,13 +273,13 @@ To do so let's create a simple example that uses the same input twice: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 9 :end-before: DOCEND 9 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 9 :end-before: DOCEND 9 @@ -290,13 +290,13 @@ verification (``fails()`` at the end). As in previous examples we can use ``twea .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 10 :end-before: DOCEND 10 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 10 :end-before: DOCEND 10 diff --git a/docs/source/versioning.rst b/docs/source/versioning.rst index 62b7cdc7a2..9d89d5c7b3 100644 --- a/docs/source/versioning.rst +++ b/docs/source/versioning.rst @@ -19,8 +19,4 @@ and the release version - a change in the major, minor or patch values may or ma The Platform Version is part of the node's ``NodeInfo`` object, which is available from the ``ServiceHub``. This enables a CorDapp to find out which version it's running on and determine whether a desired feature is available. When a node registers with the Network Map Service it will use the node's Platform Version to enforce a minimum version requirement -for the network. - -.. note:: A future release may introduce the concept of a target platform version, which would be similar to Android's - ``targetSdkVersion``, and would provide a means of maintaining behavioural compatibility for the cases where the - platform's behaviour has changed. \ No newline at end of file +for the network. \ No newline at end of file diff --git a/experimental/avalanche/build.gradle b/experimental/avalanche/build.gradle index 82cfd5135d..2e7c1970ec 100644 --- a/experimental/avalanche/build.gradle +++ b/experimental/avalanche/build.gradle @@ -1,24 +1,19 @@ -buildscript { - repositories { - jcenter() - } - - dependencies { - classpath "com.github.jengelman.gradle.plugins:shadow:$shadow_version" - } -} - apply plugin: 'kotlin' apply plugin: 'application' +// We need to set mainClassName before applying the shadow plugin. +mainClassName = "net.corda.avalanche.MainKt" + apply plugin: 'com.github.johnrengelman.shadow' dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - compile "info.picocli:picocli:3.0.1" + compile "info.picocli:picocli:$picocli_version" testCompile "junit:junit:$junit_version" } -mainClassName = "net.corda.avalanche.MainKt" +jar.enabled = false + shadowJar { baseName = "avalanche" } +assemble.dependsOn shadowJar diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/process/Command.kt b/experimental/behave/src/main/kotlin/net/corda/behave/process/Command.kt index ec7c112062..05d08f0bf4 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/process/Command.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/process/Command.kt @@ -4,7 +4,7 @@ import net.corda.behave.await import net.corda.behave.file.currentDirectory import net.corda.behave.process.output.OutputListener import net.corda.behave.waitFor -import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.contextLogger import net.corda.core.utilities.minutes import net.corda.core.utilities.seconds import rx.Observable @@ -20,8 +20,10 @@ open class Command( private val directory: Path = currentDirectory, private val timeout: Duration = 2.minutes ): Closeable { - - protected val log = loggerFor<Command>() + companion object { + private val WAIT_BEFORE_KILL: Duration = 5.seconds + private val log = contextLogger() + } private val terminationLatch = CountDownLatch(1) @@ -36,21 +38,16 @@ open class Command( var exitCode = -1 private set - val output: Observable<String> = Observable.create<String>({ emitter -> + val output: Observable<String> = Observable.create<String> { emitter -> outputListener = object : OutputListener { - override fun onNewLine(line: String) { - emitter.onNext(line) - } - - override fun onEndOfStream() { - emitter.onCompleted() - } + override fun onNewLine(line: String) = emitter.onNext(line) + override fun onEndOfStream() = emitter.onCompleted() } - }).share() + }.share() private val thread = Thread(Runnable { try { - log.info("Command: $command") + log.info("Executing command: $command from directory: $directory") val processBuilder = ProcessBuilder(command) .directory(directory.toFile()) .redirectErrorStream(true) @@ -132,12 +129,13 @@ open class Command( } override fun close() { + if (process?.isAlive == true) { + kill() + } waitFor() } - fun run() = use { _ -> } - - fun use(action: (Command) -> Unit): Int { + fun run(action: (Command) -> Unit = { }): Int { use { start() action(this) @@ -145,8 +143,8 @@ open class Command( return exitCode } - fun use(subscriber: Subscriber<String>, action: (Command, Observable<String>) -> Unit = { _, _ -> }): Int { - use { + fun run(subscriber: Subscriber<String>, action: (Command, Observable<String>) -> Unit = { _, _ -> }): Int { + run { output.subscribe(subscriber) start() action(this, output) @@ -155,11 +153,4 @@ open class Command( } override fun toString() = "Command(${command.joinToString(" ")})" - - companion object { - - private val WAIT_BEFORE_KILL: Duration = 5.seconds - - } - } diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/service/ContainerService.kt b/experimental/behave/src/main/kotlin/net/corda/behave/service/ContainerService.kt index 71f52a6c5a..70763d06f1 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/service/ContainerService.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/service/ContainerService.kt @@ -54,7 +54,6 @@ abstract class ContainerService( log.info("Container $id info: $info") client.startContainer(id) - true } catch (e: Exception) { id = null @@ -79,9 +78,7 @@ abstract class ContainerService( override fun checkPrerequisites() { if (!client.listImages().any { true == it.repoTags()?.contains(imageReference) }) { log.info("Pulling image $imageReference ...") - client.pull(imageReference, { _ -> - run { } - }) + client.pull(imageReference) { } log.info("Image $imageReference downloaded") } } diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt index 747c82cdae..88152420dd 100644 --- a/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt +++ b/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt @@ -20,7 +20,7 @@ class CommandTests { @Test fun `output stream for command can be observed`() { val subscriber = TestSubscriber<String>() - val exitCode = Command(listOf("ls", "/")).use(subscriber) { _, _ -> + val exitCode = Command(listOf("ls", "/")).run(subscriber) { _, _ -> subscriber.awaitTerminalEvent() subscriber.assertCompleted() subscriber.assertNoErrors() diff --git a/experimental/build.gradle b/experimental/build.gradle index 2aa7d36615..1be6ecc6c5 100644 --- a/experimental/build.gradle +++ b/experimental/build.gradle @@ -3,15 +3,6 @@ version '1.0-SNAPSHOT' apply plugin: 'kotlin' -repositories { - mavenLocal() - mavenCentral() - maven { - url 'http://oss.sonatype.org/content/repositories/snapshots' - } - jcenter() -} - compileKotlin { kotlinOptions.suppressWarnings = true } diff --git a/finance/build.gradle b/finance/build.gradle index 493b3babfb..b55819f201 100644 --- a/finance/build.gradle +++ b/finance/build.gradle @@ -71,11 +71,22 @@ jar { exclude "META-INF/*.MF" exclude "META-INF/LICENSE" exclude "META-INF/NOTICE" + + manifest { + attributes( + "Manifest-Version": "1.0", + "Specification-Title": description, + "Specification-Version": version, + "Specification-Vendor": "Corda Open Source", + "Implementation-Title": "$group.$baseName", + ) + } } cordapp { info { - vendor "R3" + name "net/corda/finance" + vendor "Corda Open Source" } } diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt b/finance/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt index b35bb0fb41..34fb16b688 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt @@ -22,7 +22,7 @@ private fun generateCashSumCriteria(currency: Currency): QueryCriteria { val ccyIndex = builder { CashSchemaV1.PersistentCashState::currency.equal(currency.currencyCode) } // This query should only return cash states the calling node is a participant of (meaning they can be modified/spent). - val ccyCriteria = QueryCriteria.VaultCustomQueryCriteria(ccyIndex, isRelevant = Vault.RelevancyStatus.RELEVANT) + val ccyCriteria = QueryCriteria.VaultCustomQueryCriteria(ccyIndex, relevancyStatus = Vault.RelevancyStatus.RELEVANT) return sumCriteria.and(ccyCriteria) } @@ -32,7 +32,7 @@ private fun generateCashSumsCriteria(): QueryCriteria { orderBy = Sort.Direction.DESC) } // This query should only return cash states the calling node is a participant of (meaning they can be modified/spent). - return QueryCriteria.VaultCustomQueryCriteria(sum, isRelevant = Vault.RelevancyStatus.RELEVANT) + return QueryCriteria.VaultCustomQueryCriteria(sum, relevancyStatus = Vault.RelevancyStatus.RELEVANT) } private fun rowsToAmount(currency: Currency, rows: Vault.Page<FungibleAsset<*>>): Amount<Currency> { diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt index af56a852b7..c02441f2fb 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt @@ -40,7 +40,7 @@ class CashSelectionH2Impl : AbstractCashSelection() { FROM vault_states AS vs, contract_cash_states AS ccs WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index AND vs.state_status = 0 - AND vs.is_relevant = 0 + AND vs.relevancy_status = 0 AND ccs.ccy_code = ? and @t < ? AND (vs.lock_id = ? OR vs.lock_id is null) """ + diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionPostgreSQLImpl.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionPostgreSQLImpl.kt index 35feeec332..4e5012e07a 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionPostgreSQLImpl.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionPostgreSQLImpl.kt @@ -42,7 +42,7 @@ class CashSelectionPostgreSQLImpl : AbstractCashSelection() { FROM vault_states AS vs, contract_cash_states AS ccs WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index AND vs.state_status = 0 - AND vs.is_relevant = 0 + AND vs.relevancy_status = 0 AND ccs.ccy_code = ? AND (vs.lock_id = ? OR vs.lock_id is null) """ + diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionSQLServerImpl.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionSQLServerImpl.kt index 0ca0d2d668..52779a149c 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionSQLServerImpl.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionSQLServerImpl.kt @@ -58,7 +58,7 @@ class CashSelectionSQLServerImpl : AbstractCashSelection() { ON vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index WHERE vs.state_status = 0 - AND vs.is_relevant = 0 + AND vs.relevancy_status = 0 AND ccs.ccy_code = ? AND (vs.lock_id = ? OR vs.lock_id IS NULL) """ diff --git a/finance/src/main/kotlin/net/corda/finance/schemas/CashSchemaV1.kt b/finance/src/main/kotlin/net/corda/finance/schemas/CashSchemaV1.kt index 166cfc764b..7486ddb590 100644 --- a/finance/src/main/kotlin/net/corda/finance/schemas/CashSchemaV1.kt +++ b/finance/src/main/kotlin/net/corda/finance/schemas/CashSchemaV1.kt @@ -22,7 +22,7 @@ object CashSchema object CashSchemaV1 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCashState::class.java)) { override val migrationResource = "cash.changelog-master" - + @Entity @Table(name = "contract_cash_states", indexes = [Index(name = "ccy_code_idx", columnList = "ccy_code"), Index(name = "pennies_idx", columnList = "pennies")]) class PersistentCashState( diff --git a/finance/src/main/resources/migration/cash.changelog-master.xml b/finance/src/main/resources/migration/cash.changelog-master.xml index c01eeff1e4..75b78444bd 100644 --- a/finance/src/main/resources/migration/cash.changelog-master.xml +++ b/finance/src/main/resources/migration/cash.changelog-master.xml @@ -1,5 +1,8 @@ <?xml version="1.1" encoding="UTF-8" standalone="no"?> -<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd" > +<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" + xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd" > <include file="migration/cash.changelog-init.xml"/> <include file="migration/cash.changelog-v1.xml"/> diff --git a/finance/src/main/resources/migration/commercial-paper.changelog-master.xml b/finance/src/main/resources/migration/commercial-paper.changelog-master.xml index a91edb5f2d..41c6d30d9f 100644 --- a/finance/src/main/resources/migration/commercial-paper.changelog-master.xml +++ b/finance/src/main/resources/migration/commercial-paper.changelog-master.xml @@ -1,5 +1,8 @@ <?xml version="1.1" encoding="UTF-8" standalone="no"?> -<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"> +<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" + xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"> <include file="migration/commercial-paper.changelog-init.xml"/> <include file="migration/commercial-paper.changelog-v1.xml"/> diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt index a721f07f36..25e8c9d22a 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt @@ -248,7 +248,7 @@ class CommercialPaperTestsGeneric { val notaryServices = MockServices(listOf("net.corda.finance.contracts", "net.corda.finance.contracts.asset", "net.corda.finance.schemas"), dummyNotary) val issuerServices = MockServices(listOf("net.corda.finance.contracts", "net.corda.finance.contracts.asset", "net.corda.finance.schemas"), dummyCashIssuer, dummyNotary) val (aliceDatabase, aliceServices) = makeTestDatabaseAndMockServices( - listOf("net.corda.finance.contracts", "net.corda.finance.contracts.asset", "net.corda.finance.schemas"), + listOf("net.corda.finance.contracts", "net.corda.finance.schemas"), makeTestIdentityService(*allIdentities), alice ) @@ -257,7 +257,7 @@ class CommercialPaperTestsGeneric { } val (megaCorpDatabase, megaCorpServices) = makeTestDatabaseAndMockServices( - listOf("net.corda.finance.contracts", "net.corda.finance.contracts.asset", "net.corda.finance.schemas"), + listOf("net.corda.finance.contracts", "net.corda.finance.schemas"), makeTestIdentityService(*allIdentities), megaCorp ) diff --git a/finance/src/test/kotlin/net/corda/finance/flows/CashIssueFlowTests.kt b/finance/src/test/kotlin/net/corda/finance/flows/CashIssueFlowTests.kt index ecb9573be9..83ab2b4a28 100644 --- a/finance/src/test/kotlin/net/corda/finance/flows/CashIssueFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashIssueFlowTests.kt @@ -24,7 +24,9 @@ class CashIssueFlowTests { @Before fun start() { - mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("net.corda.finance.contracts", "net.corda.finance.contracts.asset", "net.corda.finance.schemas")) + mockNet = MockNetwork( + servicePeerAllocationStrategy = RoundRobin(), + cordappPackages = listOf("net.corda.finance.contracts", "net.corda.finance.schemas")) bankOfCordaNode = mockNet.createPartyNode(BOC_NAME) bankOfCorda = bankOfCordaNode.info.identityFromX500Name(BOC_NAME) notary = mockNet.defaultNotaryIdentity diff --git a/gradle.properties b/gradle.properties index 59e65ce554..0c6bf2d49f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,3 @@ kotlin.incremental=true -org.gradle.jvmargs=-XX:+UseG1GC -Xmx1g +org.gradle.jvmargs=-XX:+UseG1GC -Xmx1g -Dfile.encoding=UTF-8 +org.gradle.caching=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 758de960ec..28861d273a 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index debd024022..4e974715fd 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.8.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/BrokerRpcSslOptions.kt b/node-api/src/main/kotlin/net/corda/nodeapi/BrokerRpcSslOptions.kt new file mode 100644 index 0000000000..2509b83617 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/BrokerRpcSslOptions.kt @@ -0,0 +1,5 @@ +package net.corda.nodeapi + +import java.nio.file.Path + +data class BrokerRpcSslOptions(val keyStorePath: Path, val keyStorePassword: String) \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt index 4e0366b9d7..e7b57d10f1 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt @@ -3,9 +3,8 @@ package net.corda.nodeapi.internal import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor -import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_P2P_USER -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.config.MutualSslConfiguration import org.apache.activemq.artemis.api.core.client.ActiveMQClient import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE import org.apache.activemq.artemis.api.core.client.ClientProducer @@ -18,7 +17,7 @@ interface ArtemisSessionProvider { val started: ArtemisMessagingClient.Started? } -class ArtemisMessagingClient(private val config: SSLConfiguration, +class ArtemisMessagingClient(private val config: MutualSslConfiguration, private val serverAddress: NetworkHostAndPort, private val maxMessageSize: Int) : ArtemisSessionProvider { companion object { @@ -59,8 +58,11 @@ class ArtemisMessagingClient(private val config: SSLConfiguration, override fun stop() = synchronized(this) { started?.run { producer.close() - // Ensure any trailing messages are committed to the journal - session.commit() + // Since we are leaking the session outside of this class it may well be already closed. + if(!session.isClosed) { + // Ensure any trailing messages are committed to the journal + session.commit() + } // Closing the factory closes all the sessions it produced as well. sessionFactory.close() } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt index a369aa710a..fb1034f1ba 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt @@ -6,7 +6,6 @@ import net.corda.core.messaging.MessageRecipientGroup import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.serialization.CordaSerializable -import net.corda.core.utilities.NetworkHostAndPort import org.apache.activemq.artemis.api.core.Message import org.apache.activemq.artemis.api.core.SimpleString import java.security.PublicKey @@ -77,9 +76,7 @@ class ArtemisMessagingComponent { val queueName: String } - interface ArtemisPeerAddress : ArtemisAddress, SingleMessageRecipient { - val hostAndPort: NetworkHostAndPort - } + interface ArtemisPeerAddress : ArtemisAddress, SingleMessageRecipient /** * This is the class used to implement [SingleMessageRecipient], for now. Note that in future this class @@ -90,12 +87,11 @@ class ArtemisMessagingComponent { * an advertised service's queue. * * @param queueName The name of the queue this address is associated with. - * @param hostAndPort The address of the node. */ @CordaSerializable - data class NodeAddress(override val queueName: String, override val hostAndPort: NetworkHostAndPort) : ArtemisPeerAddress { - constructor(peerIdentity: PublicKey, hostAndPort: NetworkHostAndPort) : - this("$PEERS_PREFIX${peerIdentity.toStringShort()}", hostAndPort) + data class NodeAddress(override val queueName: String) : ArtemisPeerAddress { + constructor(peerIdentity: PublicKey) : + this("$PEERS_PREFIX${peerIdentity.toStringShort()}") } /** diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisTcpTransport.kt similarity index 62% rename from node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt rename to node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisTcpTransport.kt index 18fbad6ff2..c4c184342d 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisTcpTransport.kt @@ -1,44 +1,29 @@ -package net.corda.nodeapi +package net.corda.nodeapi.internal import net.corda.core.messaging.ClientRpcSslOptions import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort -import net.corda.nodeapi.internal.config.SSLConfiguration -import net.corda.nodeapi.internal.requireOnDefaultFileSystem +import net.corda.nodeapi.BrokerRpcSslOptions +import net.corda.nodeapi.internal.config.CertificateStore +import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier +import net.corda.nodeapi.internal.config.SslConfiguration +import net.corda.nodeapi.internal.config.MutualSslConfiguration import org.apache.activemq.artemis.api.core.TransportConfiguration import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants import java.nio.file.Path -/** Class to set Artemis TCP configuration options. */ +// This avoids internal types from leaking in the public API. The "external" ArtemisTcpTransport delegates to this internal one. class ArtemisTcpTransport { companion object { - /** - * Corda supported TLS schemes. - * <p><ul> - * <li>TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 - * <li>TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 - * <li>TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 - * </ul></p> - * As shown above, current version restricts enabled TLS cipher suites to: - * AES128 using Galois/Counter Mode (GCM) for the block cipher being used to encrypt the message stream. - * SHA256 as message authentication algorithm. - * Ephemeral Diffie Hellman key exchange for advanced forward secrecy. ECDHE is preferred, but DHE is also - * supported in case one wants to completely avoid the use of ECC for TLS. - * ECDSA and RSA for digital signatures. Our self-generated certificates all use ECDSA for handshakes, - * but we allow classical RSA certificates to work in case one uses external tools or cloud providers or HSMs - * that do not support ECC certificates. - */ val CIPHER_SUITES = listOf( "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256" + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" ) - /** Supported TLS versions, currently TLSv1.2 only. */ val TLS_VERSIONS = listOf("TLSv1.2") - private fun defaultArtemisOptions(hostAndPort: NetworkHostAndPort) = mapOf( + internal fun defaultArtemisOptions(hostAndPort: NetworkHostAndPort) = mapOf( // Basic TCP target details. TransportConstants.HOST_PROP_NAME to hostAndPort.host, TransportConstants.PORT_PROP_NAME to hostAndPort.port, @@ -54,18 +39,47 @@ class ArtemisTcpTransport { //hick-ups under high load (CORDA-1336) TransportConstants.DIRECT_DELIVER to false) - private val defaultSSLOptions = mapOf( + internal val defaultSSLOptions = mapOf( TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME to CIPHER_SUITES.joinToString(","), TransportConstants.ENABLED_PROTOCOLS_PROP_NAME to TLS_VERSIONS.joinToString(",")) - private fun SSLConfiguration.toTransportOptions() = mapOf( + private fun SslConfiguration.toTransportOptions(): Map<String, Any> { + + val options = mutableMapOf<String, Any>() + (keyStore to trustStore).addToTransportOptions(options) + return options + } + + private fun Pair<FileBasedCertificateStoreSupplier?, FileBasedCertificateStoreSupplier?>.addToTransportOptions(options: MutableMap<String, Any>) { + + val keyStore = first + val trustStore = second + keyStore?.let { + with (it) { + path.requireOnDefaultFileSystem() + options.putAll(get().toKeyStoreTransportOptions(path)) + } + } + trustStore?.let { + with (it) { + path.requireOnDefaultFileSystem() + options.putAll(get().toTrustStoreTransportOptions(path)) + } + } + } + + private fun CertificateStore.toKeyStoreTransportOptions(path: Path) = mapOf( TransportConstants.SSL_ENABLED_PROP_NAME to true, TransportConstants.KEYSTORE_PROVIDER_PROP_NAME to "JKS", - TransportConstants.KEYSTORE_PATH_PROP_NAME to sslKeystore, - TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to keyStorePassword, + TransportConstants.KEYSTORE_PATH_PROP_NAME to path, + TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to password, + TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to true) + + private fun CertificateStore.toTrustStoreTransportOptions(path: Path) = mapOf( + TransportConstants.SSL_ENABLED_PROP_NAME to true, TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME to "JKS", - TransportConstants.TRUSTSTORE_PATH_PROP_NAME to trustStoreFile, - TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to trustStorePassword, + TransportConstants.TRUSTSTORE_PATH_PROP_NAME to path, + TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to password, TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to true) private fun ClientRpcSslOptions.toTransportOptions() = mapOf( @@ -81,34 +95,39 @@ class ArtemisTcpTransport { TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to keyStorePassword, TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to false) - private val acceptorFactoryClassName = "org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory" - private val connectorFactoryClassName = NettyConnectorFactory::class.java.name + internal val acceptorFactoryClassName = "org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory" + internal val connectorFactoryClassName = NettyConnectorFactory::class.java.name + + fun p2pAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: MutualSslConfiguration?, enableSSL: Boolean = true): TransportConfiguration { + + return p2pAcceptorTcpTransport(hostAndPort, config?.keyStore, config?.trustStore, enableSSL = enableSSL) + } + + fun p2pConnectorTcpTransport(hostAndPort: NetworkHostAndPort, config: MutualSslConfiguration?, enableSSL: Boolean = true): TransportConfiguration { + + return p2pConnectorTcpTransport(hostAndPort, config?.keyStore, config?.trustStore, enableSSL = enableSSL) + } + + fun p2pAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, keyStore: FileBasedCertificateStoreSupplier?, trustStore: FileBasedCertificateStoreSupplier?, enableSSL: Boolean = true): TransportConfiguration { - fun p2pAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: SSLConfiguration?, enableSSL: Boolean = true): TransportConfiguration { val options = defaultArtemisOptions(hostAndPort).toMutableMap() - - if (config != null && enableSSL) { - config.sslKeystore.requireOnDefaultFileSystem() - config.trustStoreFile.requireOnDefaultFileSystem() + if (enableSSL) { options.putAll(defaultSSLOptions) - options.putAll(config.toTransportOptions()) + (keyStore to trustStore).addToTransportOptions(options) } return TransportConfiguration(acceptorFactoryClassName, options) } - fun p2pConnectorTcpTransport(hostAndPort: NetworkHostAndPort, config: SSLConfiguration?, enableSSL: Boolean = true): TransportConfiguration { - val options = defaultArtemisOptions(hostAndPort).toMutableMap() + fun p2pConnectorTcpTransport(hostAndPort: NetworkHostAndPort, keyStore: FileBasedCertificateStoreSupplier?, trustStore: FileBasedCertificateStoreSupplier?, enableSSL: Boolean = true): TransportConfiguration { - if (config != null && enableSSL) { - config.sslKeystore.requireOnDefaultFileSystem() - config.trustStoreFile.requireOnDefaultFileSystem() + val options = defaultArtemisOptions(hostAndPort).toMutableMap() + if (enableSSL) { options.putAll(defaultSSLOptions) - options.putAll(config.toTransportOptions()) + (keyStore to trustStore).addToTransportOptions(options) } return TransportConfiguration(connectorFactoryClassName, options) } - /** [TransportConfiguration] for RPC TCP communication - server side. */ fun rpcAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: BrokerRpcSslOptions?, enableSSL: Boolean = true): TransportConfiguration { val options = defaultArtemisOptions(hostAndPort).toMutableMap() @@ -120,8 +139,6 @@ class ArtemisTcpTransport { return TransportConfiguration(acceptorFactoryClassName, options) } - /** [TransportConfiguration] for RPC TCP communication - * This is the Transport that connects the client JVM to the broker. */ fun rpcConnectorTcpTransport(hostAndPort: NetworkHostAndPort, config: ClientRpcSslOptions?, enableSSL: Boolean = true): TransportConfiguration { val options = defaultArtemisOptions(hostAndPort).toMutableMap() @@ -133,19 +150,16 @@ class ArtemisTcpTransport { return TransportConfiguration(connectorFactoryClassName, options) } - /** Create as list of [TransportConfiguration]. **/ fun rpcConnectorTcpTransportsFromList(hostAndPortList: List<NetworkHostAndPort>, config: ClientRpcSslOptions?, enableSSL: Boolean = true): List<TransportConfiguration> = hostAndPortList.map { rpcConnectorTcpTransport(it, config, enableSSL) } - fun rpcInternalClientTcpTransport(hostAndPort: NetworkHostAndPort, config: SSLConfiguration): TransportConfiguration { + fun rpcInternalClientTcpTransport(hostAndPort: NetworkHostAndPort, config: SslConfiguration): TransportConfiguration { return TransportConfiguration(connectorFactoryClassName, defaultArtemisOptions(hostAndPort) + defaultSSLOptions + config.toTransportOptions()) } - fun rpcInternalAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: SSLConfiguration): TransportConfiguration { + fun rpcInternalAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: SslConfiguration): TransportConfiguration { return TransportConfiguration(acceptorFactoryClassName, defaultArtemisOptions(hostAndPort) + defaultSSLOptions + config.toTransportOptions()) } } -} - -data class BrokerRpcSslOptions(val keyStorePath: Path, val keyStorePassword: String) +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt index 080bf680c4..456db77e9b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt @@ -7,10 +7,13 @@ import net.corda.core.identity.Party import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.utilities.trace -import net.corda.nodeapi.internal.config.NodeSSLConfiguration +import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier +import net.corda.nodeapi.internal.config.SslConfiguration import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_ALIAS_PREFIX +import net.corda.nodeapi.internal.crypto.X509Utilities.NODE_IDENTITY_ALIAS_PREFIX import org.slf4j.LoggerFactory import java.nio.file.Path import java.security.KeyPair @@ -24,22 +27,17 @@ import java.security.PublicKey object DevIdentityGenerator { private val log = LoggerFactory.getLogger(javaClass) - // TODO These don't need to be prefixes but can be the full aliases - // TODO Move these constants out of here as the node needs access to them - const val NODE_IDENTITY_ALIAS_PREFIX = "identity" - const val DISTRIBUTED_NOTARY_ALIAS_PREFIX = "distributed-notary" - /** Install a node key store for the given node directory using the given legal name. */ fun installKeyStoreWithNodeIdentity(nodeDir: Path, legalName: CordaX500Name): Party { - val nodeSslConfig = object : NodeSSLConfiguration { - override val baseDirectory = nodeDir - override val keyStorePassword: String = "cordacadevpass" - override val trustStorePassword get() = throw NotImplementedError("Not expected to be called") - override val crlCheckSoftFail: Boolean = true - } + val certificatesDirectory = nodeDir / "certificates" + val signingCertStore = FileBasedCertificateStoreSupplier(certificatesDirectory / "nodekeystore.jks", "cordacadevpass") + val p2pKeyStore = FileBasedCertificateStoreSupplier(certificatesDirectory / "sslkeystore.jks", "cordacadevpass") + val p2pTrustStore = FileBasedCertificateStoreSupplier(certificatesDirectory / "truststore.jks", "trustpass") + val p2pSslConfig = SslConfiguration.mutual(p2pKeyStore, p2pTrustStore) - nodeSslConfig.certificatesDirectory.createDirectories() - val (nodeKeyStore) = nodeSslConfig.createDevKeyStores(legalName) + certificatesDirectory.createDirectories() + val nodeKeyStore = signingCertStore.get(true).also { it.registerDevSigningCertificates(legalName) } + p2pSslConfig.keyStore.get(true).also { it.registerDevP2pCertificates(legalName) } val identity = nodeKeyStore.storeLegalIdentity("$NODE_IDENTITY_ALIAS_PREFIX-private-key") return identity.party diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt index 6d2ca5b271..d8a856afa5 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt @@ -5,7 +5,7 @@ import net.corda.core.crypto.Crypto.generateKeyPair import net.corda.core.identity.CordaX500Name import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.toX500Name -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.config.CertificateStore import net.corda.nodeapi.internal.crypto.* import org.bouncycastle.asn1.x509.GeneralName import org.bouncycastle.asn1.x509.GeneralSubtree @@ -20,48 +20,43 @@ import javax.security.auth.x500.X500Principal * Create the node and SSL key stores needed by a node. The node key store will be populated with a node CA cert (using * the given legal name), and the SSL key store will store the TLS cert which is a sub-cert of the node CA. */ -fun SSLConfiguration.createDevKeyStores(legalName: CordaX500Name, - rootCert: X509Certificate = DEV_ROOT_CA.certificate, - intermediateCa: CertificateAndKeyPair = DEV_INTERMEDIATE_CA): Pair<X509KeyStore, X509KeyStore> { - val (nodeCaCert, nodeCaKeyPair) = createDevNodeCa(intermediateCa, legalName) - val nodeKeyStore = loadNodeKeyStore(createNew = true) - nodeKeyStore.update { - setPrivateKey( - X509Utilities.CORDA_CLIENT_CA, - nodeCaKeyPair.private, - listOf(nodeCaCert, intermediateCa.certificate, rootCert)) +fun CertificateStore.registerDevSigningCertificates(legalName: CordaX500Name, + rootCert: X509Certificate = DEV_ROOT_CA.certificate, + intermediateCa: CertificateAndKeyPair = DEV_INTERMEDIATE_CA, + devNodeCa: CertificateAndKeyPair = createDevNodeCa(intermediateCa, legalName)) { + + update { + setPrivateKey(X509Utilities.CORDA_CLIENT_CA, devNodeCa.keyPair.private, listOf(devNodeCa.certificate, intermediateCa.certificate, rootCert)) } - - val sslKeyStore = loadSslKeyStore(createNew = true) - sslKeyStore.update { - val tlsKeyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, nodeCaCert, nodeCaKeyPair, legalName.x500Principal, tlsKeyPair.public) - setPrivateKey( - X509Utilities.CORDA_CLIENT_TLS, - tlsKeyPair.private, - listOf(tlsCert, nodeCaCert, intermediateCa.certificate, rootCert)) - } - - return Pair(nodeKeyStore, sslKeyStore) } -fun X509KeyStore.storeLegalIdentity(alias: String, keyPair: KeyPair = Crypto.generateKeyPair()): PartyAndCertificate { - val nodeCaCertPath = getCertificateChain(X509Utilities.CORDA_CLIENT_CA) - // Assume key password = store password. - val nodeCaCertAndKeyPair = getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) - // Create new keys and store in keystore. - val identityCert = X509Utilities.createCertificate( - CertificateType.LEGAL_IDENTITY, - nodeCaCertAndKeyPair.certificate, - nodeCaCertAndKeyPair.keyPair, - nodeCaCertAndKeyPair.certificate.subjectX500Principal, - keyPair.public) - // TODO: X509Utilities.validateCertificateChain() - // Assume key password = store password. - val identityCertPath = listOf(identityCert) + nodeCaCertPath - setPrivateKey(alias, keyPair.private, identityCertPath) - save() +fun CertificateStore.registerDevP2pCertificates(legalName: CordaX500Name, + rootCert: X509Certificate = DEV_ROOT_CA.certificate, + intermediateCa: CertificateAndKeyPair = DEV_INTERMEDIATE_CA, + devNodeCa: CertificateAndKeyPair = createDevNodeCa(intermediateCa, legalName)) { + + update { + val tlsKeyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, devNodeCa.certificate, devNodeCa.keyPair, legalName.x500Principal, tlsKeyPair.public) + setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, tlsKeyPair.private, listOf(tlsCert, devNodeCa.certificate, intermediateCa.certificate, rootCert)) + } +} + +fun CertificateStore.storeLegalIdentity(alias: String, keyPair: KeyPair = Crypto.generateKeyPair()): PartyAndCertificate { + val identityCertPath = query { + val nodeCaCertPath = getCertificateChain(X509Utilities.CORDA_CLIENT_CA) + // Assume key password = store password. + val nodeCaCertAndKeyPair = getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) + // Create new keys and store in keystore. + val identityCert = X509Utilities.createCertificate(CertificateType.LEGAL_IDENTITY, nodeCaCertAndKeyPair.certificate, nodeCaCertAndKeyPair.keyPair, nodeCaCertAndKeyPair.certificate.subjectX500Principal, keyPair.public) + // TODO: X509Utilities.validateCertificateChain() + // Assume key password = store password. + listOf(identityCert) + nodeCaCertPath + } + update { + setPrivateKey(alias, keyPair.private, identityCertPath) + } return PartyAndCertificate(X509Utilities.buildCertPath(identityCertPath)) } @@ -105,8 +100,10 @@ const val DEV_CA_TRUST_STORE_PASS: String = "trustpass" // We need a class so that we can get hold of the class loader internal object DevCaHelper { fun loadDevCa(alias: String): CertificateAndKeyPair { - // TODO: Should be identity scheme - val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/$DEV_CA_KEY_STORE_FILE"), DEV_CA_KEY_STORE_PASS) - return caKeyStore.getCertificateAndKeyPair(alias, DEV_CA_PRIVATE_KEY_PASS) + return loadDevCaKeyStore().query { getCertificateAndKeyPair(alias, DEV_CA_PRIVATE_KEY_PASS) } } } + +fun loadDevCaKeyStore(classLoader: ClassLoader = DevCaHelper::class.java.classLoader): CertificateStore = CertificateStore.fromResource("certificates/$DEV_CA_KEY_STORE_FILE", DEV_CA_KEY_STORE_PASS, classLoader) + +fun loadDevCaTrustStore(classLoader: ClassLoader = DevCaHelper::class.java.classLoader): CertificateStore = CertificateStore.fromResource("certificates/$DEV_CA_TRUST_STORE_FILE", DEV_CA_TRUST_STORE_PASS, classLoader) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/NodeInfoConstants.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/NodeInfoConstants.kt index 8a75621a2b..79cb6844de 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/NodeInfoConstants.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/NodeInfoConstants.kt @@ -1,4 +1,5 @@ package net.corda.nodeapi.internal // TODO: Add to Corda node.conf to allow customisation -const val NODE_INFO_DIRECTORY = "additional-node-infos" \ No newline at end of file +const val NODE_INFO_DIRECTORY = "additional-node-infos" +const val PLATFORM_VERSION = 4 diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/RpcHelpers.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/RpcHelpers.kt new file mode 100644 index 0000000000..e09112af4d --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/RpcHelpers.kt @@ -0,0 +1,52 @@ +package net.corda.nodeapi.internal + +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.messaging.DataFeed +import net.corda.core.messaging.StateMachineUpdate +import rx.Observable +import rx.schedulers.Schedulers +import rx.subjects.PublishSubject +import java.util.concurrent.TimeUnit + +/** + * Returns a [DataFeed] of the number of pending flows. The [Observable] for the updates will complete the moment all pending flows will have terminated. + */ +fun CordaRPCOps.pendingFlowsCount(): DataFeed<Int, Pair<Int, Int>> { + + val updates = PublishSubject.create<Pair<Int, Int>>() + val initialPendingFlowsCount = stateMachinesFeed().let { + var completedFlowsCount = 0 + var pendingFlowsCount = it.snapshot.size + it.updates.observeOn(Schedulers.io()).subscribe({ update -> + when (update) { + is StateMachineUpdate.Added -> { + pendingFlowsCount++ + updates.onNext(completedFlowsCount to pendingFlowsCount) + } + is StateMachineUpdate.Removed -> { + completedFlowsCount++ + updates.onNext(completedFlowsCount to pendingFlowsCount) + if (completedFlowsCount == pendingFlowsCount) { + updates.onCompleted() + } + } + } + }, updates::onError) + if (pendingFlowsCount == 0) { + updates.onCompleted() + } + pendingFlowsCount + } + return DataFeed(initialPendingFlowsCount, updates) +} + +/** + * Returns an [Observable] that will complete when the node will have cancelled the draining shutdown hook. + * + * @param interval the value of the polling interval, default is 5. + * @param unit the time unit of the polling interval, default is [TimeUnit.SECONDS]. + */ +fun CordaRPCOps.hasCancelledDrainingShutdown(interval: Long = 5, unit: TimeUnit = TimeUnit.SECONDS): Observable<Unit> { + + return Observable.interval(interval, unit).map { isWaitingForShutdown() }.takeFirst { waiting -> waiting == false }.map { Unit } +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt index 5b6a2d8cd5..1ce5a6ce57 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt @@ -4,17 +4,15 @@ import io.netty.channel.EventLoopGroup import io.netty.channel.nio.NioEventLoopGroup import net.corda.core.identity.CordaX500Name import net.corda.core.internal.VisibleForTesting -import net.corda.core.node.NodeInfo import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.nodeapi.internal.ArtemisMessagingClient -import net.corda.nodeapi.internal.ArtemisMessagingComponent import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_P2P_USER import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2PMessagingHeaders import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress.Companion.translateLocalQueueToInboxAddress import net.corda.nodeapi.internal.ArtemisSessionProvider -import net.corda.nodeapi.internal.bridging.AMQPBridgeManager.AMQPBridge.Companion.getBridgeName -import net.corda.nodeapi.internal.config.NodeSSLConfiguration +import net.corda.nodeapi.internal.config.CertificateStore +import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient import net.corda.nodeapi.internal.protonwrapper.netty.AMQPConfiguration @@ -25,7 +23,6 @@ import org.apache.activemq.artemis.api.core.client.ClientMessage import org.apache.activemq.artemis.api.core.client.ClientSession import org.slf4j.MDC import rx.Subscription -import java.security.KeyStore import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -37,26 +34,24 @@ import kotlin.concurrent.withLock * The Netty thread pool used by the AMQPBridges is also shared and managed by the AMQPBridgeManager. */ @VisibleForTesting -class AMQPBridgeManager(config: NodeSSLConfiguration, maxMessageSize: Int, private val artemisMessageClientFactory: () -> ArtemisSessionProvider) : BridgeManager { +class AMQPBridgeManager(config: MutualSslConfiguration, maxMessageSize: Int, + private val artemisMessageClientFactory: () -> ArtemisSessionProvider, + private val bridgeMetricsService: BridgeMetricsService? = null) : BridgeManager { private val lock = ReentrantLock() - private val bridgeNameToBridgeMap = mutableMapOf<String, AMQPBridge>() + private val queueNamesToBridgesMap = mutableMapOf<String, MutableList<AMQPBridge>>() - private class AMQPConfigurationImpl private constructor(override val keyStore: KeyStore, - override val keyStorePrivateKeyPassword: CharArray, - override val trustStore: KeyStore, + private class AMQPConfigurationImpl private constructor(override val keyStore: CertificateStore, + override val trustStore: CertificateStore, override val maxMessageSize: Int) : AMQPConfiguration { - constructor(config: NodeSSLConfiguration, maxMessageSize: Int) : this(config.loadSslKeyStore().internal, - config.keyStorePassword.toCharArray(), - config.loadTrustStore().internal, - maxMessageSize) + constructor(config: MutualSslConfiguration, maxMessageSize: Int) : this(config.keyStore.get(), config.trustStore.get(), maxMessageSize) } private val amqpConfig: AMQPConfiguration = AMQPConfigurationImpl(config, maxMessageSize) private var sharedEventLoopGroup: EventLoopGroup? = null private var artemis: ArtemisSessionProvider? = null - constructor(config: NodeSSLConfiguration, p2pAddress: NetworkHostAndPort, maxMessageSize: Int) : this(config, maxMessageSize, { ArtemisMessagingClient(config, p2pAddress, maxMessageSize) }) + constructor(config: MutualSslConfiguration, p2pAddress: NetworkHostAndPort, maxMessageSize: Int) : this(config, maxMessageSize, { ArtemisMessagingClient(config, p2pAddress, maxMessageSize) }) companion object { private const val NUM_BRIDGE_THREADS = 0 // Default sized pool @@ -70,14 +65,14 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, maxMessageSize: Int, priva * If the delivery fails the session is rolled back to prevent loss of the message. This may cause duplicate delivery, * however Artemis and the remote Corda instanced will deduplicate these messages. */ - private class AMQPBridge(private val queueName: String, - private val target: NetworkHostAndPort, - private val legalNames: Set<CordaX500Name>, + private class AMQPBridge(val queueName: String, + val targets: List<NetworkHostAndPort>, + val legalNames: Set<CordaX500Name>, private val amqpConfig: AMQPConfiguration, sharedEventGroup: EventLoopGroup, - private val artemis: ArtemisSessionProvider) { + private val artemis: ArtemisSessionProvider, + private val bridgeMetricsService: BridgeMetricsService?) { companion object { - fun getBridgeName(queueName: String, hostAndPort: NetworkHostAndPort): String = "$queueName -> $hostAndPort" private val log = contextLogger() } @@ -85,8 +80,7 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, maxMessageSize: Int, priva val oldMDC = MDC.getCopyOfContextMap() try { MDC.put("queueName", queueName) - MDC.put("target", target.toString()) - MDC.put("bridgeName", bridgeName) + MDC.put("targets", targets.joinToString(separator = ";") { it.toString() }) MDC.put("legalNames", legalNames.joinToString(separator = ";") { it.toString() }) MDC.put("maxMessageSize", amqpConfig.maxMessageSize.toString()) block() @@ -105,8 +99,7 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, maxMessageSize: Int, priva private fun logWarnWithMDC(msg: String) = withMDC { log.warn(msg) } - val amqpClient = AMQPClient(listOf(target), legalNames, amqpConfig, sharedThreadPool = sharedEventGroup) - val bridgeName: String get() = getBridgeName(queueName, target) + val amqpClient = AMQPClient(targets, legalNames, amqpConfig, sharedThreadPool = sharedEventGroup) private val lock = ReentrantLock() // lock to serialise session level access private var session: ClientSession? = null private var consumer: ClientConsumer? = null @@ -114,7 +107,7 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, maxMessageSize: Int, priva fun start() { logInfoWithMDC("Create new AMQP bridge") - connectedSubscription = amqpClient.onConnection.subscribe({ x -> onSocketConnected(x.connected) }) + connectedSubscription = amqpClient.onConnection.subscribe { x -> onSocketConnected(x.connected) } amqpClient.start() } @@ -138,6 +131,7 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, maxMessageSize: Int, priva synchronized(artemis) { if (connected) { logInfoWithMDC("Bridge Connected") + bridgeMetricsService?.bridgeConnected(targets, legalNames) val sessionFactory = artemis.started!!.sessionFactory val session = sessionFactory.createSession(NODE_P2P_USER, NODE_P2P_USER, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) this.session = session @@ -147,6 +141,7 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, maxMessageSize: Int, priva session.start() } else { logInfoWithMDC("Bridge Disconnected") + bridgeMetricsService?.bridgeDisconnected(targets, legalNames) consumer?.close() consumer = null session?.stop() @@ -158,8 +153,10 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, maxMessageSize: Int, priva private fun clientArtemisMessageHandler(artemisMessage: ClientMessage) { if (artemisMessage.bodySize > amqpConfig.maxMessageSize) { - logWarnWithMDC("Message exceeds maxMessageSize network parameter, maxMessageSize: [${amqpConfig.maxMessageSize}], message size: [${artemisMessage.bodySize}], " + - "dropping message, uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}") + val msg = "Message exceeds maxMessageSize network parameter, maxMessageSize: [${amqpConfig.maxMessageSize}], message size: [${artemisMessage.bodySize}], " + + "dropping message, uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}" + logWarnWithMDC(msg) + bridgeMetricsService?.packetDropEvent(artemisMessage, msg) // Ack the message to prevent same message being sent to us again. artemisMessage.acknowledge() return @@ -195,42 +192,43 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, maxMessageSize: Int, priva } } amqpClient.write(sendableMessage) + bridgeMetricsService?.packetAcceptedEvent(sendableMessage) } } - private fun gatherAddresses(node: NodeInfo): List<ArtemisMessagingComponent.NodeAddress> { - return node.legalIdentitiesAndCerts.map { ArtemisMessagingComponent.NodeAddress(it.party.owningKey, node.addresses[0]) } - } - - override fun deployBridge(queueName: String, target: NetworkHostAndPort, legalNames: Set<CordaX500Name>) { - if (bridgeExists(getBridgeName(queueName, target))) { - return - } - val newBridge = AMQPBridge(queueName, target, legalNames, amqpConfig, sharedEventLoopGroup!!, artemis!!) - lock.withLock { - bridgeNameToBridgeMap[newBridge.bridgeName] = newBridge + override fun deployBridge(queueName: String, targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>) { + val newBridge = lock.withLock { + val bridges = queueNamesToBridgesMap.getOrPut(queueName) { mutableListOf() } + for (target in targets) { + if (bridges.any { it.targets.contains(target) }) { + return + } + } + val newBridge = AMQPBridge(queueName, targets, legalNames, amqpConfig, sharedEventLoopGroup!!, artemis!!, bridgeMetricsService) + bridges += newBridge + bridgeMetricsService?.bridgeCreated(targets, legalNames) + newBridge } newBridge.start() } - override fun destroyBridges(node: NodeInfo) { + override fun destroyBridge(queueName: String, targets: List<NetworkHostAndPort>) { lock.withLock { - gatherAddresses(node).forEach { - val bridge = bridgeNameToBridgeMap.remove(getBridgeName(it.queueName, it.hostAndPort)) - bridge?.stop() + val bridges = queueNamesToBridgesMap[queueName] ?: mutableListOf() + for (target in targets) { + val bridge = bridges.firstOrNull { it.targets.contains(target) } + if (bridge != null) { + bridges -= bridge + if (bridges.isEmpty()) { + queueNamesToBridgesMap.remove(queueName) + } + bridge.stop() + bridgeMetricsService?.bridgeDestroyed(bridge.targets, bridge.legalNames) + } } } } - override fun destroyBridge(queueName: String, hostAndPort: NetworkHostAndPort) { - lock.withLock { - val bridge = bridgeNameToBridgeMap.remove(getBridgeName(queueName, hostAndPort)) - bridge?.stop() - } - } - - override fun bridgeExists(bridgeName: String): Boolean = lock.withLock { bridgeNameToBridgeMap.containsKey(bridgeName) } - override fun start() { sharedEventLoopGroup = NioEventLoopGroup(NUM_BRIDGE_THREADS) val artemis = artemisMessageClientFactory() @@ -242,13 +240,13 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, maxMessageSize: Int, priva override fun close() { lock.withLock { - for (bridge in bridgeNameToBridgeMap.values) { + for (bridge in queueNamesToBridgesMap.values.flatten()) { bridge.stop() } sharedEventLoopGroup?.shutdownGracefully() sharedEventLoopGroup?.terminationFuture()?.sync() sharedEventLoopGroup = null - bridgeNameToBridgeMap.clear() + queueNamesToBridgesMap.clear() artemis?.stop() } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt index 3dc987e6ec..995ce01eba 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt @@ -11,23 +11,25 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_NOT import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX import net.corda.nodeapi.internal.ArtemisSessionProvider -import net.corda.nodeapi.internal.config.NodeSSLConfiguration +import net.corda.nodeapi.internal.config.MutualSslConfiguration import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.client.ClientConsumer import org.apache.activemq.artemis.api.core.client.ClientMessage import java.util.* -class BridgeControlListener(val config: NodeSSLConfiguration, +class BridgeControlListener(val config: MutualSslConfiguration, maxMessageSize: Int, - val artemisMessageClientFactory: () -> ArtemisSessionProvider) : AutoCloseable { + private val artemisMessageClientFactory: () -> ArtemisSessionProvider, + bridgeMetricsService: BridgeMetricsService? = null) : AutoCloseable { private val bridgeId: String = UUID.randomUUID().toString() - private val bridgeManager: BridgeManager = AMQPBridgeManager(config, maxMessageSize, artemisMessageClientFactory) + private val bridgeManager: BridgeManager = AMQPBridgeManager(config, maxMessageSize, + artemisMessageClientFactory, bridgeMetricsService) private val validInboundQueues = mutableSetOf<String>() private var artemis: ArtemisSessionProvider? = null private var controlConsumer: ClientConsumer? = null - constructor(config: NodeSSLConfiguration, + constructor(config: MutualSslConfiguration, p2pAddress: NetworkHostAndPort, maxMessageSize: Int) : this(config, maxMessageSize, { ArtemisMessagingClient(config, p2pAddress, maxMessageSize) }) @@ -98,7 +100,7 @@ class BridgeControlListener(val config: NodeSSLConfiguration, return } for (outQueue in controlMessage.sendQueues) { - bridgeManager.deployBridge(outQueue.queueName, outQueue.targets.first(), outQueue.legalNames.toSet()) + bridgeManager.deployBridge(outQueue.queueName, outQueue.targets, outQueue.legalNames.toSet()) } validInboundQueues.addAll(controlMessage.inboxQueues) } @@ -110,14 +112,14 @@ class BridgeControlListener(val config: NodeSSLConfiguration, log.error("Invalid queue names in control message $controlMessage") return } - bridgeManager.deployBridge(controlMessage.bridgeInfo.queueName, controlMessage.bridgeInfo.targets.first(), controlMessage.bridgeInfo.legalNames.toSet()) + bridgeManager.deployBridge(controlMessage.bridgeInfo.queueName, controlMessage.bridgeInfo.targets, controlMessage.bridgeInfo.legalNames.toSet()) } is BridgeControl.Delete -> { if (!controlMessage.bridgeInfo.queueName.startsWith(PEERS_PREFIX)) { log.error("Invalid queue names in control message $controlMessage") return } - bridgeManager.destroyBridge(controlMessage.bridgeInfo.queueName, controlMessage.bridgeInfo.targets.first()) + bridgeManager.destroyBridge(controlMessage.bridgeInfo.queueName, controlMessage.bridgeInfo.targets) } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeManager.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeManager.kt index cd7db964cf..69b1509550 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeManager.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeManager.kt @@ -2,7 +2,6 @@ package net.corda.nodeapi.internal.bridging import net.corda.core.identity.CordaX500Name import net.corda.core.internal.VisibleForTesting -import net.corda.core.node.NodeInfo import net.corda.core.utilities.NetworkHostAndPort /** @@ -10,13 +9,9 @@ import net.corda.core.utilities.NetworkHostAndPort */ @VisibleForTesting interface BridgeManager : AutoCloseable { - fun deployBridge(queueName: String, target: NetworkHostAndPort, legalNames: Set<CordaX500Name>) + fun deployBridge(queueName: String, targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>) - fun destroyBridges(node: NodeInfo) - - fun destroyBridge(queueName: String, hostAndPort: NetworkHostAndPort) - - fun bridgeExists(bridgeName: String): Boolean + fun destroyBridge(queueName: String, targets: List<NetworkHostAndPort>) fun start() diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeMetricsService.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeMetricsService.kt new file mode 100644 index 0000000000..1998fd4e51 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeMetricsService.kt @@ -0,0 +1,15 @@ +package net.corda.nodeapi.internal.bridging + +import net.corda.core.identity.CordaX500Name +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.nodeapi.internal.protonwrapper.messages.SendableMessage +import org.apache.activemq.artemis.api.core.client.ClientMessage + +interface BridgeMetricsService { + fun bridgeCreated(targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>) + fun bridgeConnected(targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>) + fun packetDropEvent(artemisMessage: ClientMessage, msg: String) + fun packetAcceptedEvent(sendableMessage: SendableMessage) + fun bridgeDisconnected(targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>) + fun bridgeDestroyed(targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>) +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStore.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStore.kt new file mode 100644 index 0000000000..3ca6be6d6b --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStore.kt @@ -0,0 +1,82 @@ +package net.corda.nodeapi.internal.config + +import net.corda.core.internal.outputStream +import net.corda.nodeapi.internal.crypto.X509KeyStore +import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.crypto.addOrReplaceCertificate +import java.io.InputStream +import java.io.OutputStream +import java.nio.file.OpenOption +import java.nio.file.Path +import java.security.cert.X509Certificate + +interface CertificateStore : Iterable<Pair<String, X509Certificate>> { + + companion object { + + fun of(store: X509KeyStore, password: String): CertificateStore = DelegatingCertificateStore(store, password) + + fun fromFile(storePath: Path, password: String, createNew: Boolean): CertificateStore = DelegatingCertificateStore(X509KeyStore.fromFile(storePath, password, createNew), password) + + fun fromInputStream(stream: InputStream, password: String): CertificateStore = DelegatingCertificateStore(X509KeyStore.fromInputStream(stream, password), password) + + fun fromResource(storeResourceName: String, password: String, classLoader: ClassLoader = Thread.currentThread().contextClassLoader): CertificateStore = fromInputStream(classLoader.getResourceAsStream(storeResourceName), password) + } + + val value: X509KeyStore + val password: String + + fun writeTo(stream: OutputStream) = value.internal.store(stream, password.toCharArray()) + + fun writeTo(path: Path, vararg options: OpenOption) = path.outputStream(*options) + + fun update(action: X509KeyStore.() -> Unit) { + val result = action.invoke(value) + value.save() + return result + } + + fun <RESULT> query(action: X509KeyStore.() -> RESULT): RESULT { + return action.invoke(value) + } + + operator fun set(alias: String, certificate: X509Certificate) { + + update { + internal.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, certificate) + } + } + + override fun iterator(): Iterator<Pair<String, X509Certificate>> { + + return query { + aliases() + }.asSequence().map { alias -> alias to get(alias) }.iterator() + } + + fun forEach(action: (alias: String, certificate: X509Certificate) -> Unit) { + + forEach { (alias, certificate) -> action.invoke(alias, certificate) } + } + + /** + * @throws IllegalArgumentException if no certificate for the alias is found, or if the certificate is not an [X509Certificate]. + */ + operator fun get(alias: String): X509Certificate { + + return query { + getCertificate(alias) + } + } + + operator fun contains(alias: String): Boolean = value.contains(alias) + + fun copyTo(certificateStore: CertificateStore) { + + certificateStore.update { + this@CertificateStore.forEach(::setCertificate) + } + } +} + +private class DelegatingCertificateStore(override val value: X509KeyStore, override val password: String) : CertificateStore \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStoreSupplier.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStoreSupplier.kt new file mode 100644 index 0000000000..3703742813 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStoreSupplier.kt @@ -0,0 +1,24 @@ +package net.corda.nodeapi.internal.config + +import java.io.IOException +import java.nio.file.Path + +interface CertificateStoreSupplier { + + fun get(createNew: Boolean = false): CertificateStore + + fun getOptional(): CertificateStore? { + + return try { + get() + } catch (e: IOException) { + null + } + } +} + +// TODO replace reference to FileBasedCertificateStoreSupplier with CertificateStoreSupplier, after coming up with a way of passing certificate stores to Artemis. +class FileBasedCertificateStoreSupplier(val path: Path, val password: String) : CertificateStoreSupplier { + + override fun get(createNew: Boolean) = CertificateStore.fromFile(path, password, createNew) +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SSLConfiguration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SSLConfiguration.kt deleted file mode 100644 index e8c63fc1f3..0000000000 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SSLConfiguration.kt +++ /dev/null @@ -1,33 +0,0 @@ -package net.corda.nodeapi.internal.config - -import net.corda.core.internal.div -import net.corda.nodeapi.internal.crypto.X509KeyStore -import java.nio.file.Path - -interface SSLConfiguration { - val keyStorePassword: String - val trustStorePassword: String - val certificatesDirectory: Path - val sslKeystore: Path get() = certificatesDirectory / "sslkeystore.jks" - // TODO This looks like it should be in NodeSSLConfiguration - val nodeKeystore: Path get() = certificatesDirectory / "nodekeystore.jks" - val trustStoreFile: Path get() = certificatesDirectory / "truststore.jks" - val crlCheckSoftFail: Boolean - - fun loadTrustStore(createNew: Boolean = false): X509KeyStore { - return X509KeyStore.fromFile(trustStoreFile, trustStorePassword, createNew) - } - - fun loadNodeKeyStore(createNew: Boolean = false): X509KeyStore { - return X509KeyStore.fromFile(nodeKeystore, keyStorePassword, createNew) - } - - fun loadSslKeyStore(createNew: Boolean = false): X509KeyStore { - return X509KeyStore.fromFile(sslKeystore, keyStorePassword, createNew) - } -} - -interface NodeSSLConfiguration : SSLConfiguration { - val baseDirectory: Path - override val certificatesDirectory: Path get() = baseDirectory / "certificates" -} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SslConfiguration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SslConfiguration.kt new file mode 100644 index 0000000000..d8349bc80e --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SslConfiguration.kt @@ -0,0 +1,23 @@ +package net.corda.nodeapi.internal.config + +interface SslConfiguration { + + val keyStore: FileBasedCertificateStoreSupplier? + val trustStore: FileBasedCertificateStoreSupplier? + + companion object { + + fun mutual(keyStore: FileBasedCertificateStoreSupplier, trustStore: FileBasedCertificateStoreSupplier): MutualSslConfiguration { + + return MutualSslOptions(keyStore, trustStore) + } + } +} + +interface MutualSslConfiguration : SslConfiguration { + + override val keyStore: FileBasedCertificateStoreSupplier + override val trustStore: FileBasedCertificateStoreSupplier +} + +private class MutualSslOptions(override val keyStore: FileBasedCertificateStoreSupplier, override val trustStore: FileBasedCertificateStoreSupplier) : MutualSslConfiguration \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509KeyStore.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509KeyStore.kt index 1fa56bebea..5c0c4b501b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509KeyStore.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509KeyStore.kt @@ -2,6 +2,7 @@ package net.corda.nodeapi.internal.crypto import net.corda.core.crypto.Crypto import net.corda.core.internal.uncheckedCast +import java.io.InputStream import java.nio.file.Path import java.security.KeyPair import java.security.KeyStore @@ -30,6 +31,14 @@ class X509KeyStore private constructor(val internal: KeyStore, private val store val internal: KeyStore = if (createNew) loadOrCreateKeyStore(keyStoreFile, storePassword) else loadKeyStore(keyStoreFile, storePassword) return X509KeyStore(internal, storePassword, keyStoreFile) } + + /** + * Reads a [KeyStore] from an [InputStream]. + */ + fun fromInputStream(stream: InputStream, storePassword: String): X509KeyStore { + val internal = loadKeyStore(stream, storePassword) + return X509KeyStore(internal, storePassword) + } } operator fun contains(alias: String): Boolean = internal.containsAlias(alias) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt index 1582c5d1aa..dafa767440 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt @@ -41,13 +41,23 @@ object X509Utilities { val DEFAULT_IDENTITY_SIGNATURE_SCHEME = Crypto.EDDSA_ED25519_SHA512 val DEFAULT_TLS_SIGNATURE_SCHEME = Crypto.ECDSA_SECP256R1_SHA256 - // TODO This class is more of a general purpose utility class and as such these constants belong elsewhere + // TODO This class is more of a general purpose utility class and as such these constants belong elsewhere. // Aliases for private keys and certificates. const val CORDA_ROOT_CA = "cordarootca" const val CORDA_INTERMEDIATE_CA = "cordaintermediateca" const val CORDA_CLIENT_TLS = "cordaclienttls" const val CORDA_CLIENT_CA = "cordaclientca" + // TODO These don't need to be prefixes, but can be the full aliases. However, because they are used as key aliases + // we should ensure that: + // a) they always contain valid characters, preferably [A-Za-z0-9] in order to be supported by the majority of + // crypto service implementations (i.e., HSMs). + // b) they are at most 127 chars in length (i.e., as of 2018, Azure Key Vault does not support bigger aliases). + const val NODE_IDENTITY_ALIAS_PREFIX = "identity" + // TODO Hyphen (-) seems to be supported by the major HSM vendors, but we should consider remove it in the + // future and stick to [A-Za-z0-9]. + const val DISTRIBUTED_NOTARY_ALIAS_PREFIX = "distributed-notary" + val DEFAULT_VALIDITY_WINDOW = Pair(0.millis, 3650.days) /** @@ -402,7 +412,7 @@ enum class CertificateType(val keyUsage: KeyUsage, vararg val purposes: KeyPurpo KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true, - role = CertRole.INTERMEDIATE_CA + role = CertRole.DOORMAN_CA ), NETWORK_MAP( diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index 4690b5e325..d0c987d1be 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -147,11 +147,19 @@ internal constructor(private val initSerEnv: Boolean, } } - /** Entry point for Cordform */ + /** Old Entry point for Cordform + * + * TODO: Remove once the gradle plugins are updated to 4.0.30 + */ fun bootstrap(directory: Path, cordappJars: List<Path>) { bootstrap(directory, cordappJars, copyCordapps = true, fromCordform = true) } + /** Entry point for Cordform */ + fun bootstrapCordform(directory: Path, cordappJars: List<Path>) { + bootstrap(directory, cordappJars, copyCordapps = false, fromCordform = true) + } + /** Entry point for the tool */ fun bootstrap(directory: Path, copyCordapps: Boolean) { // Don't accidently include the bootstrapper jar as a CorDapp! diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt index 6b2da4bfeb..ae164aad99 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt @@ -2,13 +2,10 @@ package net.corda.nodeapi.internal.network import net.corda.core.crypto.SecureHash import net.corda.core.internal.CertRole -import net.corda.core.internal.DigitalSignatureWithCert import net.corda.core.internal.SignedDataWithCert -import net.corda.core.internal.signWithCert import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.SerializedBytes import net.corda.nodeapi.internal.crypto.X509Utilities import java.security.cert.X509Certificate import java.time.Instant @@ -57,20 +54,14 @@ data class ParametersUpdate( val updateDeadline: Instant ) -/** Verify that a Network Map certificate is issued by Root CA and its [CertRole] is correct. */ -// TODO: Current implementation works under the assumption that there are no intermediate CAs between Root and -// Network Map. Consider a more flexible implementation without the above assumption. - +/** Verify that a Network Map certificate path and its [CertRole] is correct. */ fun <T : Any> SignedDataWithCert<T>.verifiedNetworkMapCert(rootCert: X509Certificate): T { require(CertRole.extract(sig.by) == CertRole.NETWORK_MAP) { "Incorrect cert role: ${CertRole.extract(sig.by)}" } - X509Utilities.validateCertificateChain(rootCert, sig.by, rootCert) + val path = if (sig.parentCertsChain.isEmpty()) { + listOf(sig.by, rootCert) + } else { + sig.fullCertChain + } + X509Utilities.validateCertificateChain(rootCert, path) return verified() } - -class NetworkMapAndSigned private constructor(val networkMap: NetworkMap, val signed: SignedNetworkMap) { - constructor(networkMap: NetworkMap, signer: (SerializedBytes<NetworkMap>) -> DigitalSignatureWithCert) : this(networkMap, networkMap.signWithCert(signer)) - constructor(signed: SignedNetworkMap) : this(signed.verified(), signed) - - operator fun component1(): NetworkMap = networkMap - operator fun component2(): SignedNetworkMap = signed -} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt index 4cafba895c..e7067a3eac 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt @@ -106,10 +106,16 @@ class SchemaMigration( /** For existing database created before verions 4.0 add Liquibase support - creates DATABASECHANGELOG and DATABASECHANGELOGLOCK tables and mark changesets are executed. */ private fun migrateOlderDatabaseToUseLiquibase(existingCheckpoints: Boolean): Boolean { + //workaround to detect that if Corda finance module is in use then the most recent version with Liquibase migration scripts was deployed + if (schemas.any { schema -> + (schema::class.qualifiedName == "net.corda.finance.schemas.CashSchemaV1" || schema::class.qualifiedName == "net.corda.finance.schemas.CommercialPaperSchemaV1") + && schema.migrationResource == null + }) + throw DatabaseMigrationException("Detected incompatible corda-finance cordapp without database migration scripts, replace the existing corda-finance-VERSION.jar with the latest one.") + val isExistingDBWithoutLiquibase = dataSource.connection.use { - it.metaData.getTables(null, null, "NODE%", null).next() && - !it.metaData.getTables(null, null, "DATABASECHANGELOG", null).next() && - !it.metaData.getTables(null, null, "DATABASECHANGELOGLOCK", null).next() + (it.metaData.getTables(null, null, "NODE%", null).next() && + !it.metaData.getTables(null, null, "DATABASECHANGELOG%", null).next()) } when { isExistingDBWithoutLiquibase && existingCheckpoints -> throw CheckpointsException() @@ -119,29 +125,31 @@ class SchemaMigration( dataSource.connection.use { connection -> // Schema migrations pre release 4.0 - val preV4Baseline = - listOf("migration/common.changelog-init.xml", - "migration/node-info.changelog-init.xml", - "migration/node-info.changelog-v1.xml", - "migration/node-info.changelog-v2.xml", - "migration/node-core.changelog-init.xml", - "migration/node-core.changelog-v3.xml", - "migration/node-core.changelog-v4.xml", - "migration/node-core.changelog-v5.xml", - "migration/node-core.changelog-pkey.xml", - "migration/vault-schema.changelog-init.xml", - "migration/vault-schema.changelog-v3.xml", - "migration/vault-schema.changelog-v4.xml", - "migration/vault-schema.changelog-pkey.xml", - "migration/cash.changelog-init.xml", - "migration/cash.changelog-v1.xml", - "migration/commercial-paper.changelog-init.xml", - "migration/commercial-paper.changelog-v1.xml") + - if (schemas.any { schema -> schema.migrationResource == "node-notary.changelog-master" }) - listOf("migration/node-notary.changelog-init.xml", - "migration/node-notary.changelog-v1.xml", - "migration/vault-schema.changelog-pkey.xml") - else emptyList() + val preV4Baseline = mutableListOf("migration/common.changelog-init.xml", + "migration/node-info.changelog-init.xml", + "migration/node-info.changelog-v1.xml", + "migration/node-info.changelog-v2.xml", + "migration/node-core.changelog-init.xml", + "migration/node-core.changelog-v3.xml", + "migration/node-core.changelog-v4.xml", + "migration/node-core.changelog-v5.xml", + "migration/node-core.changelog-pkey.xml", + "migration/vault-schema.changelog-init.xml", + "migration/vault-schema.changelog-v3.xml", + "migration/vault-schema.changelog-v4.xml", + "migration/vault-schema.changelog-pkey.xml") + + if (schemas.any { schema -> schema.migrationResource == "cash.changelog-master" }) + preV4Baseline.addAll(listOf("migration/cash.changelog-init.xml", + "migration/cash.changelog-v1.xml")) + + if (schemas.any { schema -> schema.migrationResource == "commercial-paper.changelog-master" }) + preV4Baseline.addAll(listOf("migration/commercial-paper.changelog-init.xml", + "migration/commercial-paper.changelog-v1.xml")) + + if (schemas.any { schema -> schema.migrationResource == "node-notary.changelog-master" }) + preV4Baseline.addAll(listOf("migration/node-notary.changelog-init.xml", + "migration/node-notary.changelog-v1.xml")) val customResourceAccessor = CustomResourceAccessor(dynamicInclude, preV4Baseline, classLoader) val liquibase = Liquibase(dynamicInclude, customResourceAccessor, getLiquibaseDatabase(JdbcConnection(connection))) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPClient.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPClient.kt index 5c010014be..a9f16b8e77 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPClient.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPClient.kt @@ -116,14 +116,14 @@ class AMQPClient(val targets: List<NetworkHostAndPort>, private val conf = parent.configuration init { - keyManagerFactory.init(conf.keyStore, conf.keyStorePrivateKeyPassword) + keyManagerFactory.init(conf.keyStore) trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(conf.trustStore, conf.crlCheckSoftFail)) } override fun initChannel(ch: SocketChannel) { val pipeline = ch.pipeline() val target = parent.currentTarget - val handler = createClientSslHelper(target, keyManagerFactory, trustManagerFactory) + val handler = createClientSslHelper(target, parent.allowedRemoteLegalNames, keyManagerFactory, trustManagerFactory) pipeline.addLast("sslHandler", handler) if (conf.trace) pipeline.addLast("logger", LoggingHandler(LogLevel.INFO)) pipeline.addLast(AMQPChannelHandler(false, diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPConfiguration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPConfiguration.kt index c2ba54286a..3b7289a8c5 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPConfiguration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPConfiguration.kt @@ -1,6 +1,7 @@ package net.corda.nodeapi.internal.protonwrapper.netty import net.corda.nodeapi.internal.ArtemisMessagingComponent +import net.corda.nodeapi.internal.config.CertificateStore import java.security.KeyStore interface AMQPConfiguration { @@ -21,19 +22,14 @@ interface AMQPConfiguration { get() = ArtemisMessagingComponent.PEER_USER /** - * The keystore used for TLS connections + * The key store used for TLS connections */ - val keyStore: KeyStore + val keyStore: CertificateStore /** - * Password used to unlock TLS private keys in the KeyStore. + * The trust root key store to validate the peer certificates against */ - val keyStorePrivateKeyPassword: CharArray - - /** - * The trust root KeyStore to validate the peer certificates against - */ - val trustStore: KeyStore + val trustStore: CertificateStore /** * Setting crlCheckSoftFail to true allows certificate paths where some leaf certificates do not contain cRLDistributionPoints diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt index 7c16a0547e..56c8b8bfda 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt @@ -60,7 +60,7 @@ class AMQPServer(val hostName: String, private val conf = parent.configuration init { - keyManagerFactory.init(conf.keyStore, conf.keyStorePrivateKeyPassword) + keyManagerFactory.init(conf.keyStore) trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(conf.trustStore, conf.crlCheckSoftFail)) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt index 1f4328a8ee..9850e6a8ce 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/SSLHelper.kt @@ -1,22 +1,25 @@ package net.corda.nodeapi.internal.protonwrapper.netty import io.netty.handler.ssl.SslHandler +import net.corda.core.crypto.SecureHash import net.corda.core.crypto.newSecureRandom +import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.core.utilities.toHex -import net.corda.nodeapi.ArtemisTcpTransport +import net.corda.nodeapi.internal.ArtemisTcpTransport +import net.corda.nodeapi.internal.config.CertificateStore import net.corda.nodeapi.internal.crypto.toBc import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier import org.bouncycastle.asn1.x509.Extension import org.bouncycastle.asn1.x509.SubjectKeyIdentifier import java.net.Socket -import java.security.KeyStore -import java.security.SecureRandom import java.security.cert.* import java.util.* import javax.net.ssl.* +private const val HOSTNAME_FORMAT = "%s.corda.net" + internal class LoggingTrustManagerWrapper(val wrapped: X509ExtendedTrustManager) : X509ExtendedTrustManager() { companion object { val log = contextLogger() @@ -103,6 +106,7 @@ internal class LoggingTrustManagerWrapper(val wrapped: X509ExtendedTrustManager) } internal fun createClientSslHelper(target: NetworkHostAndPort, + expectedRemoteLegalNames: Set<CordaX500Name>, keyManagerFactory: KeyManagerFactory, trustManagerFactory: TrustManagerFactory): SslHandler { val sslContext = SSLContext.getInstance("TLS") @@ -114,6 +118,11 @@ internal fun createClientSslHelper(target: NetworkHostAndPort, sslEngine.enabledProtocols = ArtemisTcpTransport.TLS_VERSIONS.toTypedArray() sslEngine.enabledCipherSuites = ArtemisTcpTransport.CIPHER_SUITES.toTypedArray() sslEngine.enableSessionCreation = true + if (expectedRemoteLegalNames.size == 1) { + val sslParameters = sslEngine.sslParameters + sslParameters.serverNames = listOf(SNIHostName(x500toHostName(expectedRemoteLegalNames.single()))) + sslEngine.sslParameters = sslParameters + } return SslHandler(sslEngine) } @@ -132,7 +141,7 @@ internal fun createServerSslHelper(keyManagerFactory: KeyManagerFactory, return SslHandler(sslEngine) } -internal fun initialiseTrustStoreAndEnableCrlChecking(trustStore: KeyStore, crlCheckSoftFail: Boolean): ManagerFactoryParameters { +internal fun initialiseTrustStoreAndEnableCrlChecking(trustStore: CertificateStore, crlCheckSoftFail: Boolean): ManagerFactoryParameters { val certPathBuilder = CertPathBuilder.getInstance("PKIX") val revocationChecker = certPathBuilder.revocationChecker as PKIXRevocationChecker revocationChecker.options = EnumSet.of( @@ -145,7 +154,18 @@ internal fun initialiseTrustStoreAndEnableCrlChecking(trustStore: KeyStore, crlC // the following reasons: The CRL or OCSP response cannot be obtained because of a network error. revocationChecker.options = revocationChecker.options + PKIXRevocationChecker.Option.SOFT_FAIL } - val pkixParams = PKIXBuilderParameters(trustStore, X509CertSelector()) + val pkixParams = PKIXBuilderParameters(trustStore.value.internal, X509CertSelector()) pkixParams.addCertPathChecker(revocationChecker) return CertPathTrustManagerParameters(pkixParams) } + +fun KeyManagerFactory.init(keyStore: CertificateStore) = init(keyStore.value.internal, keyStore.password.toCharArray()) + +fun TrustManagerFactory.init(trustStore: CertificateStore) = init(trustStore.value.internal) + +internal fun x500toHostName(x500Name: CordaX500Name): String { + val secureHash = SecureHash.sha256(x500Name.toString()) + // RFC 1035 specifies a limit 255 bytes for hostnames with each label being 63 bytes or less. Due to this, the string + // representation of the SHA256 hash is truncated to 32 characters. + return String.format(HOSTNAME_FORMAT, secureHash.toString().substring(0..32).toLowerCase()) +} diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt index 0909707ac4..c699439e19 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt @@ -15,6 +15,7 @@ import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder +import net.corda.node.VersionInfo import net.corda.node.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.node.internal.cordapp.JarScanningCordappLoader @@ -110,7 +111,7 @@ class AttachmentsClassLoaderStaticContractTests { val cordapps = cordappsForPackages(packages) return testDirectory().let { directory -> cordapps.packageInDirectory(directory) - JarScanningCordappLoader.fromDirectories(listOf(directory)) + JarScanningCordappLoader.fromDirectories(listOf(directory), VersionInfo.UNKNOWN) } } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/DevCertificatesTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/DevCertificatesTest.kt index 4fdc43dd2d..e586a6c642 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/DevCertificatesTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/DevCertificatesTest.kt @@ -1,8 +1,7 @@ package net.corda.nodeapi.internal.crypto import net.corda.core.internal.validate -import net.corda.nodeapi.internal.DEV_CA_TRUST_STORE_FILE -import net.corda.nodeapi.internal.DEV_CA_TRUST_STORE_PASS +import net.corda.nodeapi.internal.loadDevCaTrustStore import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder @@ -22,8 +21,8 @@ class DevCertificatesTest { @Test fun `create server certificate in keystore for SSL`() { // given - val newTrustStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/$DEV_CA_TRUST_STORE_FILE"), DEV_CA_TRUST_STORE_PASS) - val newTrustRoot = newTrustStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA) + val newTrustStore = loadDevCaTrustStore() + val newTrustRoot = newTrustStore[X509Utilities.CORDA_ROOT_CA] val newTrustAnchor = TrustAnchor(newTrustRoot, null) val oldNodeCaKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("regression-test/$OLD_NODE_DEV_KEYSTORE_FILE_NAME"), OLD_DEV_KEYSTORE_PASS) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt index 1df89e1070..3db9072041 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt @@ -1,17 +1,26 @@ package net.corda.nodeapi.internal.crypto -import net.corda.core.crypto.Crypto +import net.corda.core.crypto.* +import net.corda.core.crypto.Crypto.COMPOSITE_KEY +import net.corda.core.crypto.Crypto.ECDSA_SECP256K1_SHA256 +import net.corda.core.crypto.Crypto.ECDSA_SECP256R1_SHA256 import net.corda.core.crypto.Crypto.EDDSA_ED25519_SHA512 +import net.corda.core.crypto.Crypto.RSA_SHA256 +import net.corda.core.crypto.Crypto.SPHINCS256_SHA256 import net.corda.core.crypto.Crypto.generateKeyPair -import net.corda.core.crypto.newSecureRandom import net.corda.core.identity.CordaX500Name import net.corda.core.internal.div import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.node.serialization.amqp.AMQPServerSerializationScheme -import net.corda.nodeapi.internal.config.SSLConfiguration -import net.corda.nodeapi.internal.createDevKeyStores +import net.corda.nodeapi.internal.config.MutualSslConfiguration +import net.corda.nodeapi.internal.createDevNodeCa +import net.corda.nodeapi.internal.crypto.X509Utilities.DEFAULT_IDENTITY_SIGNATURE_SCHEME +import net.corda.nodeapi.internal.crypto.X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME +import net.corda.nodeapi.internal.protonwrapper.netty.init +import net.corda.nodeapi.internal.registerDevP2pCertificates +import net.corda.nodeapi.internal.registerDevSigningCertificates import net.corda.serialization.internal.AllWhitelist import net.corda.serialization.internal.SerializationContextImpl import net.corda.serialization.internal.SerializationFactoryImpl @@ -19,19 +28,26 @@ import net.corda.serialization.internal.amqp.amqpMagic import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.TestIdentity +import net.corda.testing.internal.stubs.CertificateStoreStubs import net.corda.testing.internal.createDevIntermediateCaCertPath +import net.i2p.crypto.eddsa.EdDSAPrivateKey import org.assertj.core.api.Assertions.assertThat import org.bouncycastle.asn1.x509.* +import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPrivateCrtKey +import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PrivateKey import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder +import sun.security.rsa.RSAPrivateCrtKeyImpl import java.io.DataInputStream import java.io.DataOutputStream import java.io.IOException import java.net.InetAddress import java.net.InetSocketAddress import java.nio.file.Path -import java.security.SecureRandom +import java.security.Key +import java.security.KeyPair +import java.security.PrivateKey import java.security.cert.CertPath import java.security.cert.X509Certificate import java.util.* @@ -47,8 +63,29 @@ class X509UtilitiesTest { val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party val CIPHER_SUITES = arrayOf( "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256" + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + ) + // We ensure that all of the algorithms are both used (at least once) as first and second in the following [Pair]s. + // We also add [DEFAULT_TLS_SIGNATURE_SCHEME] and [DEFAULT_IDENTITY_SIGNATURE_SCHEME] combinations for consistency. + val certChainSchemeCombinations = listOf( + Pair(DEFAULT_TLS_SIGNATURE_SCHEME, DEFAULT_TLS_SIGNATURE_SCHEME), + Pair(DEFAULT_IDENTITY_SIGNATURE_SCHEME, DEFAULT_IDENTITY_SIGNATURE_SCHEME), + Pair(DEFAULT_TLS_SIGNATURE_SCHEME, DEFAULT_IDENTITY_SIGNATURE_SCHEME), + Pair(ECDSA_SECP256R1_SHA256, SPHINCS256_SHA256), + Pair(ECDSA_SECP256K1_SHA256, RSA_SHA256), + Pair(EDDSA_ED25519_SHA512, ECDSA_SECP256K1_SHA256), + Pair(RSA_SHA256, EDDSA_ED25519_SHA512), + Pair(SPHINCS256_SHA256, ECDSA_SECP256R1_SHA256) + ) + + val schemeToKeyTypes = listOf( + // By default, JKS returns SUN EC key. + Triple(ECDSA_SECP256R1_SHA256,java.security.interfaces.ECPrivateKey::class.java, org.bouncycastle.jce.interfaces.ECPrivateKey::class.java), + Triple(ECDSA_SECP256K1_SHA256,java.security.interfaces.ECPrivateKey::class.java, org.bouncycastle.jce.interfaces.ECPrivateKey::class.java), + Triple(EDDSA_ED25519_SHA512, EdDSAPrivateKey::class.java, EdDSAPrivateKey::class.java), + // By default, JKS returns SUN RSA key. + Triple(RSA_SHA256, RSAPrivateCrtKeyImpl::class.java, BCRSAPrivateCrtKey::class.java), + Triple(SPHINCS256_SHA256, BCSphincs256PrivateKey::class.java, BCSphincs256PrivateKey::class.java) ) } @@ -58,7 +95,11 @@ class X509UtilitiesTest { @Test fun `create valid self-signed CA certificate`() { - val caKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + Crypto.supportedSignatureSchemes().filter { it != COMPOSITE_KEY }.forEach { validSelfSignedCertificate(it) } + } + + private fun validSelfSignedCertificate(signatureScheme: SignatureScheme) { + val caKey = generateKeyPair(signatureScheme) val subject = X500Principal("CN=Test Cert,O=R3 Ltd,L=London,C=GB") val caCert = X509Utilities.createSelfSignedCACertificate(subject, caKey) assertEquals(subject, caCert.subjectX500Principal) // using our subject common name @@ -75,8 +116,12 @@ class X509UtilitiesTest { @Test fun `load and save a PEM file certificate`() { + Crypto.supportedSignatureSchemes().filter { it != COMPOSITE_KEY }.forEach { loadSavePEMCert(it) } + } + + private fun loadSavePEMCert(signatureScheme: SignatureScheme) { val tmpCertificateFile = tempFile("cacert.pem") - val caKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val caKey = generateKeyPair(signatureScheme) val caCert = X509Utilities.createSelfSignedCACertificate(X500Principal("CN=Test Cert,O=R3 Ltd,L=London,C=GB"), caKey) X509Utilities.saveCertificateAsPEMFile(caCert, tmpCertificateFile) val readCertificate = X509Utilities.loadCertificateFromPEMFile(tmpCertificateFile) @@ -85,29 +130,52 @@ class X509UtilitiesTest { @Test fun `create valid server certificate chain`() { - val caKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val caCert = X509Utilities.createSelfSignedCACertificate(X500Principal("CN=Test CA Cert,O=R3 Ltd,L=London,C=GB"), caKey) - val subject = X500Principal("CN=Server Cert,O=R3 Ltd,L=London,C=GB") - val keyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val serverCert = X509Utilities.createCertificate(CertificateType.TLS, caCert, caKey, subject, keyPair.public) - assertEquals(subject, serverCert.subjectX500Principal) // using our subject common name - assertEquals(caCert.issuerX500Principal, serverCert.issuerX500Principal) // Issued by our CA cert - serverCert.checkValidity(Date()) // throws on verification problems - serverCert.verify(caKey.public) // throws on verification problems - serverCert.toBc().run { + certChainSchemeCombinations.forEach { createValidServerCertChain(it.first, it.second) } + } + + private fun createValidServerCertChain(signatureSchemeRoot: SignatureScheme, signatureSchemeChild: SignatureScheme) { + val (caKeyPair, caCert, _, childCert, _, childSubject) + = genCaAndChildKeysCertsAndSubjects(signatureSchemeRoot, signatureSchemeChild) + assertEquals(childSubject, childCert.subjectX500Principal) // Using our subject common name. + assertEquals(caCert.issuerX500Principal, childCert.issuerX500Principal) // Issued by our CA cert. + childCert.checkValidity(Date()) // Throws on verification problems. + childCert.verify(caKeyPair.public) // Throws on verification problems. + childCert.toBc().run { val basicConstraints = BasicConstraints.getInstance(getExtension(Extension.basicConstraints).parsedValue) val keyUsage = KeyUsage.getInstance(getExtension(Extension.keyUsage).parsedValue) - assertFalse { keyUsage.hasUsages(5) } // Bit 5 == keyCertSign according to ASN.1 spec (see full comment on KeyUsage property) - assertNull(basicConstraints.pathLenConstraint) // Non-CA certificate + assertFalse { keyUsage.hasUsages(5) } // Bit 5 == keyCertSign according to ASN.1 spec (see full comment on KeyUsage property). + assertNull(basicConstraints.pathLenConstraint) // Non-CA certificate. } } + private data class CaAndChildKeysCertsAndSubjects(val caKeyPair: KeyPair, + val caCert: X509Certificate, + val childKeyPair: KeyPair, + val childCert: X509Certificate, + val caSubject: X500Principal, + val childSubject: X500Principal) + + private fun genCaAndChildKeysCertsAndSubjects(signatureSchemeRoot: SignatureScheme, + signatureSchemeChild: SignatureScheme, + rootSubject: X500Principal = X500Principal("CN=Test CA Cert,O=R3 Ltd,L=London,C=GB"), + childSubject: X500Principal = X500Principal("CN=Test Child Cert,O=R3 Ltd,L=London,C=GB")): CaAndChildKeysCertsAndSubjects { + val caKeyPair = generateKeyPair(signatureSchemeRoot) + val caCert = X509Utilities.createSelfSignedCACertificate(rootSubject, caKeyPair) + val childKeyPair = generateKeyPair(signatureSchemeChild) + val childCert = X509Utilities.createCertificate(CertificateType.TLS, caCert, caKeyPair, childSubject, childKeyPair.public) + return CaAndChildKeysCertsAndSubjects(caKeyPair, caCert, childKeyPair, childCert, rootSubject, childSubject) + } + @Test fun `create valid server certificate chain includes CRL info`() { - val caKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + certChainSchemeCombinations.forEach { createValidServerCertIncludeCRL(it.first, it.second) } + } + + private fun createValidServerCertIncludeCRL(signatureSchemeRoot: SignatureScheme, signatureSchemeChild: SignatureScheme) { + val caKey = generateKeyPair(signatureSchemeRoot) val caCert = X509Utilities.createSelfSignedCACertificate(X500Principal("CN=Test CA Cert,O=R3 Ltd,L=London,C=GB"), caKey) val caSubjectKeyIdentifier = SubjectKeyIdentifier.getInstance(caCert.toBc().getExtension(Extension.subjectKeyIdentifier).parsedValue) - val keyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val keyPair = generateKeyPair(signatureSchemeChild) val crlDistPoint = "http://test.com" val serverCert = X509Utilities.createCertificate( CertificateType.TLS, @@ -125,84 +193,58 @@ class X509UtilitiesTest { } @Test - fun `storing EdDSA key in java keystore`() { + fun `storing all supported key types in java keystore`() { + Crypto.supportedSignatureSchemes().filter { it != COMPOSITE_KEY }.forEach { storeKeyToKeystore(it) } + } + + private fun storeKeyToKeystore(signatureScheme: SignatureScheme) { val tmpKeyStore = tempFile("keystore.jks") - val keyPair = generateKeyPair(EDDSA_ED25519_SHA512) + val keyPair = generateKeyPair(signatureScheme) val testName = X500Principal("CN=Test,O=R3 Ltd,L=London,C=GB") val selfSignCert = X509Utilities.createSelfSignedCACertificate(testName, keyPair) assertTrue(Arrays.equals(selfSignCert.publicKey.encoded, keyPair.public.encoded)) - // Save the EdDSA private key with self sign cert in the keystore. + // Save the private key with self sign cert in the keystore. val keyStore = loadOrCreateKeyStore(tmpKeyStore, "keystorepass") keyStore.setKeyEntry("Key", keyPair.private, "password".toCharArray(), arrayOf(selfSignCert)) keyStore.save(tmpKeyStore, "keystorepass") // Load the keystore from file and make sure keys are intact. - val keyStore2 = loadOrCreateKeyStore(tmpKeyStore, "keystorepass") - val privateKey = keyStore2.getKey("Key", "password".toCharArray()) - val pubKey = keyStore2.getCertificate("Key").publicKey + val reloadedKeystore = loadOrCreateKeyStore(tmpKeyStore, "keystorepass") + val reloadedPrivateKey = reloadedKeystore.getKey("Key", "password".toCharArray()) + val reloadedPublicKey = reloadedKeystore.getCertificate("Key").publicKey - assertNotNull(pubKey) - assertNotNull(privateKey) - assertEquals(keyPair.public, pubKey) - assertEquals(keyPair.private, privateKey) - } - - @Test - fun `signing EdDSA key with EcDSA certificate`() { - val tmpKeyStore = tempFile("keystore.jks") - val ecDSAKey = generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) - val testName = X500Principal("CN=Test,O=R3 Ltd,L=London,C=GB") - val ecDSACert = X509Utilities.createSelfSignedCACertificate(testName, ecDSAKey) - val edDSAKeypair = generateKeyPair(EDDSA_ED25519_SHA512) - val edDSACert = X509Utilities.createCertificate(CertificateType.TLS, ecDSACert, ecDSAKey, BOB.name.x500Principal, edDSAKeypair.public) - - // Save the EdDSA private key with cert chains. - val keyStore = loadOrCreateKeyStore(tmpKeyStore, "keystorepass") - keyStore.setKeyEntry("Key", edDSAKeypair.private, "password".toCharArray(), arrayOf(ecDSACert, edDSACert)) - keyStore.save(tmpKeyStore, "keystorepass") - - // Load the keystore from file and make sure keys are intact. - val keyStore2 = loadOrCreateKeyStore(tmpKeyStore, "keystorepass") - val privateKey = keyStore2.getKey("Key", "password".toCharArray()) - val certs = keyStore2.getCertificateChain("Key") - - val pubKey = certs.last().publicKey - - assertEquals(2, certs.size) - assertNotNull(pubKey) - assertNotNull(privateKey) - assertEquals(edDSAKeypair.public, pubKey) - assertEquals(edDSAKeypair.private, privateKey) + assertNotNull(reloadedPublicKey) + assertNotNull(reloadedPrivateKey) + assertEquals(keyPair.public, reloadedPublicKey) + assertEquals(keyPair.private, reloadedPrivateKey) } @Test fun `create server certificate in keystore for SSL`() { - val sslConfig = object : SSLConfiguration { - override val certificatesDirectory = tempFolder.root.toPath() - override val keyStorePassword = "serverstorepass" - override val trustStorePassword = "trustpass" - override val crlCheckSoftFail: Boolean = true - } + val certificatesDirectory = tempFolder.root.toPath() + val signingCertStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory, "serverstorepass") + val p2pSslConfig = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory, keyStorePassword = "serverstorepass") val (rootCa, intermediateCa) = createDevIntermediateCaCertPath() // Generate server cert and private key and populate another keystore suitable for SSL - sslConfig.createDevKeyStores(MEGA_CORP.name, rootCa.certificate, intermediateCa) - + val nodeCa = createDevNodeCa(intermediateCa, MEGA_CORP.name) + signingCertStore.get(createNew = true).also { it.registerDevSigningCertificates(MEGA_CORP.name, rootCa.certificate, intermediateCa, nodeCa) } + p2pSslConfig.keyStore.get(createNew = true).also { it.registerDevP2pCertificates(MEGA_CORP.name, rootCa.certificate, intermediateCa, nodeCa) } // Load back server certificate - val serverKeyStore = loadKeyStore(sslConfig.nodeKeystore, sslConfig.keyStorePassword) - val (serverCert, serverKeyPair) = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, sslConfig.keyStorePassword) + val serverKeyStore = signingCertStore.get().value + val (serverCert, serverKeyPair) = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) serverCert.checkValidity() serverCert.verify(intermediateCa.certificate.publicKey) assertThat(CordaX500Name.build(serverCert.subjectX500Principal)).isEqualTo(MEGA_CORP.name) // Load back SSL certificate - val sslKeyStore = loadKeyStore(sslConfig.sslKeystore, sslConfig.keyStorePassword) - val (sslCert) = sslKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS, sslConfig.keyStorePassword) + val sslKeyStoreReloaded = p2pSslConfig.keyStore.get() + val (sslCert) = sslKeyStoreReloaded.query { getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS, p2pSslConfig.keyStore.password) } sslCert.checkValidity() sslCert.verify(serverCert.publicKey) @@ -216,25 +258,20 @@ class X509UtilitiesTest { @Test fun `create server cert and use in SSL socket`() { - val sslConfig = object : SSLConfiguration { - override val certificatesDirectory = tempFolder.root.toPath() - override val keyStorePassword = "serverstorepass" - override val trustStorePassword = "trustpass" - override val crlCheckSoftFail: Boolean = true - } + val sslConfig = CertificateStoreStubs.P2P.withCertificatesDirectory(tempFolder.root.toPath(), keyStorePassword = "serverstorepass") val (rootCa, intermediateCa) = createDevIntermediateCaCertPath() // Generate server cert and private key and populate another keystore suitable for SSL - sslConfig.createDevKeyStores(MEGA_CORP.name, rootCa.certificate, intermediateCa) + sslConfig.keyStore.get(true).registerDevP2pCertificates(MEGA_CORP.name, rootCa.certificate, intermediateCa) sslConfig.createTrustStore(rootCa.certificate) - val keyStore = loadKeyStore(sslConfig.sslKeystore, sslConfig.keyStorePassword) - val trustStore = loadKeyStore(sslConfig.trustStoreFile, sslConfig.trustStorePassword) + val keyStore = sslConfig.keyStore.get() + val trustStore = sslConfig.trustStore.get() val context = SSLContext.getInstance("TLS") val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) - keyManagerFactory.init(keyStore, sslConfig.keyStorePassword.toCharArray()) + keyManagerFactory.init(keyStore) val keyManagers = keyManagerFactory.keyManagers val trustMgrFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) trustMgrFactory.init(trustStore) @@ -313,15 +350,24 @@ class X509UtilitiesTest { private fun tempFile(name: String): Path = tempFolder.root.toPath() / name - private fun SSLConfiguration.createTrustStore(rootCert: X509Certificate) { - val trustStore = loadOrCreateKeyStore(trustStoreFile, trustStorePassword) - trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert) - trustStore.save(trustStoreFile, trustStorePassword) + private fun MutualSslConfiguration.createTrustStore(rootCert: X509Certificate) { + val trustStore = this.trustStore.get(true) + trustStore[X509Utilities.CORDA_ROOT_CA] = rootCert } @Test fun `get correct private key type from Keystore`() { - val keyPair = generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) + schemeToKeyTypes.forEach { getCorrectKeyFromKeystore(it.first, it.second, it.third) } + } + + private fun <U, C> getCorrectKeyFromKeystore(signatureScheme: SignatureScheme, uncastedClass: Class<U>, castedClass: Class<C>) { + val keyPair = generateKeyPair(signatureScheme) + val (keyFromKeystore, keyFromKeystoreCasted) = storeAndGetKeysFromKeystore(keyPair) + assertThat(keyFromKeystore).isInstanceOf(uncastedClass) + assertThat(keyFromKeystoreCasted).isInstanceOf(castedClass) + } + + private fun storeAndGetKeysFromKeystore(keyPair: KeyPair): Pair<Key, PrivateKey> { val testName = X500Principal("CN=Test,O=R3 Ltd,L=London,C=GB") val selfSignCert = X509Utilities.createSelfSignedCACertificate(testName, keyPair) val keyStore = loadOrCreateKeyStore(tempFile("testKeystore.jks"), "keystorepassword") @@ -329,13 +375,15 @@ class X509UtilitiesTest { val keyFromKeystore = keyStore.getKey("Key", "keypassword".toCharArray()) val keyFromKeystoreCasted = keyStore.getSupportedKey("Key", "keypassword") - - assertTrue(keyFromKeystore is java.security.interfaces.ECPrivateKey) // by default JKS returns SUN EC key - assertTrue(keyFromKeystoreCasted is org.bouncycastle.jce.interfaces.ECPrivateKey) + return Pair(keyFromKeystore, keyFromKeystoreCasted) } @Test - fun `serialize - deserialize X509Certififcate`() { + fun `serialize - deserialize X509Certificate`() { + Crypto.supportedSignatureSchemes().filter { it != COMPOSITE_KEY }.forEach { serializeDeserializeX509Cert(it) } + } + + private fun serializeDeserializeX509Cert(signatureScheme: SignatureScheme) { val factory = SerializationFactoryImpl().apply { registerScheme(AMQPServerSerializationScheme()) } val context = SerializationContextImpl(amqpMagic, javaClass.classLoader, @@ -344,7 +392,7 @@ class X509UtilitiesTest { true, SerializationContext.UseCase.P2P, null) - val expected = X509Utilities.createSelfSignedCACertificate(ALICE.name.x500Principal, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) + val expected = X509Utilities.createSelfSignedCACertificate(ALICE.name.x500Principal, generateKeyPair(signatureScheme)) val serialized = expected.serialize(factory, context).bytes val actual = serialized.deserialize<X509Certificate>(factory, context) assertEquals(expected, actual) @@ -352,6 +400,10 @@ class X509UtilitiesTest { @Test fun `serialize - deserialize X509CertPath`() { + Crypto.supportedSignatureSchemes().filter { it != COMPOSITE_KEY }.forEach { serializeDeserializeX509CertPath(it) } + } + + private fun serializeDeserializeX509CertPath(signatureScheme: SignatureScheme) { val factory = SerializationFactoryImpl().apply { registerScheme(AMQPServerSerializationScheme()) } val context = SerializationContextImpl( amqpMagic, @@ -362,7 +414,7 @@ class X509UtilitiesTest { SerializationContext.UseCase.P2P, null ) - val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val rootCAKey = generateKeyPair(signatureScheme) val rootCACert = X509Utilities.createSelfSignedCACertificate(ALICE_NAME.x500Principal, rootCAKey) val certificate = X509Utilities.createCertificate(CertificateType.TLS, rootCACert, rootCAKey, BOB_NAME.x500Principal, BOB.publicKey) val expected = X509Utilities.buildCertPath(certificate, rootCACert) @@ -370,4 +422,33 @@ class X509UtilitiesTest { val actual: CertPath = serialized.deserialize(factory, context) assertEquals(expected, actual) } + + @Test + fun `signing a key type with another key type certificate then store and reload correctly from keystore`() { + certChainSchemeCombinations.forEach { signCertWithOtherKeyTypeAndTestKeystoreReload(it.first, it.second) } + } + + private fun signCertWithOtherKeyTypeAndTestKeystoreReload(signatureSchemeRoot: SignatureScheme, signatureSchemeChild: SignatureScheme) { + val tmpKeyStore = tempFile("keystore.jks") + + val (_, caCert, childKeyPair, childCert) = genCaAndChildKeysCertsAndSubjects(signatureSchemeRoot, signatureSchemeChild) + + // Save the child private key with cert chains. + val keyStore = loadOrCreateKeyStore(tmpKeyStore, "keystorepass") + keyStore.setKeyEntry("Key", childKeyPair.private, "password".toCharArray(), arrayOf(caCert, childCert)) + keyStore.save(tmpKeyStore, "keystorepass") + + // Load the keystore from file and make sure keys are intact. + val reloadedKeystore = loadOrCreateKeyStore(tmpKeyStore, "keystorepass") + val reloadedPrivateKey = reloadedKeystore.getKey("Key", "password".toCharArray()) + val reloadedCerts = reloadedKeystore.getCertificateChain("Key") + + val reloadedPublicKey = reloadedCerts.last().publicKey + + assertEquals(2, reloadedCerts.size) + assertNotNull(reloadedPublicKey) + assertNotNull(reloadedPrivateKey) + assertEquals(childKeyPair.public, reloadedPublicKey) + assertEquals(childKeyPair.private, reloadedPrivateKey) + } } diff --git a/node/build.gradle b/node/build.gradle index 5ba7afdc42..3a344867a3 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -71,6 +71,7 @@ dependencies { compile project(":confidential-identities") compile project(':client:rpc') compile project(':tools:shell') + compile project(':tools:cliutils') // Log4J: logging framework (with SLF4J bindings) compile "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}" @@ -90,6 +91,9 @@ dependencies { // For caches rather than guava compile "com.github.ben-manes.caffeine:caffeine:$caffeine_version" + // For async logging + compile "com.lmax:disruptor:$disruptor_version" + // JOpt: for command line flags. compile "net.sf.jopt-simple:jopt-simple:$jopt_simple_version" @@ -104,9 +108,6 @@ dependencies { exclude group: 'org.apache.qpid', module: 'proton-j' } - // JAnsi: for drawing things to the terminal in nicely coloured ways. - compile "org.fusesource.jansi:jansi:$jansi_version" - // Manifests: for reading stuff from the manifest file compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version" @@ -191,6 +192,13 @@ dependencies { compile "org.jolokia:jolokia-jvm:${jolokia_version}:agent" // Optional New Relic JVM reporter, used to push metrics to the configured account associated with a newrelic.yml configuration. See https://mvnrepository.com/artifact/com.palominolabs.metrics/metrics-new-relic compile "com.palominolabs.metrics:metrics-new-relic:${metrics_new_relic_version}" + + testCompile(project(':test-cli')) +} + +tasks.withType(JavaCompile) { + // Resolves a Gradle warning about not scanning for pre-processors. + options.compilerArgs << '-proc:none' } task integrationTest(type: Test) { diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle index 17cd8d6c0b..09df16e909 100644 --- a/node/capsule/build.gradle +++ b/node/capsule/build.gradle @@ -9,7 +9,7 @@ apply plugin: 'com.jfrog.artifactory' description 'Corda standalone node' configurations { - runtimeArtifacts.extendsFrom runtime + runtimeArtifacts.extendsFrom runtimeClasspath capsuleRuntime } @@ -26,25 +26,31 @@ dependencies { sourceCompatibility = 1.6 targetCompatibility = 1.6 -task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) { +jar.enabled = false + +capsule { + version capsule_version +} + +task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').tasks.jar) { applicationClass 'net.corda.node.Corda' archiveName "corda-${corda_release_version}.jar" applicationSource = files( - project(':node').configurations.runtime, - project(':node').jar, - project(':node').sourceSets.main.java.outputDir.toString() + '/CordaCaplet.class', - project(':node').sourceSets.main.java.outputDir.toString() + '/CordaCaplet$1.class', - "$rootDir/config/dev/log4j2.xml", - "$rootDir/node/build/resources/main/reference.conf" + project(':node').configurations.runtimeClasspath, + project(':node').tasks.jar, + project(':node').sourceSets.main.java.outputDir.toString() + '/CordaCaplet.class', + project(':node').sourceSets.main.java.outputDir.toString() + '/CordaCaplet$1.class', + project(':node').buildDir.toString() + '/resources/main/reference.conf', + "$rootDir/config/dev/log4j2.xml", + 'NOTICE' // Copy CDDL notice ) - from 'NOTICE' // Copy CDDL notice from configurations.capsuleRuntime.files.collect { zipTree(it) } capsuleManifest { applicationVersion = corda_release_version // See experimental/quasar-hook/README.md for how to generate. - def quasarExcludeExpression = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**)" + def quasarExcludeExpression = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**)" javaAgents = ["quasar-core-${quasar_version}-jdk8.jar=${quasarExcludeExpression}"] systemProperties['visualvm.display.name'] = 'Corda' minJavaVersion = '1.8.0' @@ -61,12 +67,12 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) { } } -build.dependsOn buildCordaJAR +assemble.dependsOn buildCordaJAR artifacts { runtimeArtifacts buildCordaJAR publish buildCordaJAR { - classifier "" + classifier '' } } diff --git a/node/src/integration-test/kotlin/net/corda/node/AuthDBTests.kt b/node/src/integration-test/kotlin/net/corda/node/AuthDBTests.kt index 9c45d852aa..fb71884e8e 100644 --- a/node/src/integration-test/kotlin/net/corda/node/AuthDBTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/AuthDBTests.kt @@ -13,8 +13,8 @@ import net.corda.node.internal.DataSourceFactory import net.corda.node.internal.NodeWithInfo import net.corda.node.services.Permissions import net.corda.node.services.config.PasswordEncryption -import net.corda.testing.node.internal.NodeBasedTest import net.corda.testing.core.ALICE_NAME +import net.corda.testing.node.internal.NodeBasedTest import org.apache.activemq.artemis.api.core.ActiveMQSecurityException import org.apache.shiro.authc.credential.DefaultPasswordService import org.junit.After @@ -33,7 +33,6 @@ import kotlin.test.assertFailsWith */ @RunWith(Parameterized::class) class AuthDBTests : NodeBasedTest() { - private lateinit var node: NodeWithInfo private lateinit var client: CordaRPCClient private lateinit var db: UsersDB diff --git a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt index 23ac0b2cd4..59725970db 100644 --- a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt @@ -12,10 +12,15 @@ import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow import net.corda.node.internal.NodeStartup import net.corda.node.services.Permissions.Companion.startFlow +import net.corda.nodeapi.exceptions.InternalNodeException import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.NodeHandle +import net.corda.testing.driver.NodeParameters import net.corda.testing.driver.driver import net.corda.testing.node.User +import net.corda.testing.node.internal.startNode import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Test import java.io.* @@ -24,12 +29,22 @@ import kotlin.test.assertEquals class BootTests { @Test fun `java deserialization is disabled`() { - driver(DriverParameters(notarySpecs = emptyList())) { - val user = User("u", "p", setOf(startFlow<ObjectInputStreamFlow>())) - val future = CordaRPCClient(startNode(rpcUsers = listOf(user)).getOrThrow().rpcAddress). - start(user.username, user.password).proxy.startFlow(::ObjectInputStreamFlow).returnValue - assertThatThrownBy { future.getOrThrow() } - .isInstanceOf(CordaRuntimeException::class.java) + val user = User("u", "p", setOf(startFlow<ObjectInputStreamFlow>())) + val devParams = NodeParameters(providedName = BOB_NAME, rpcUsers = listOf(user)) + val params = NodeParameters(rpcUsers = listOf(user)) + + fun NodeHandle.attemptJavaDeserialization() { + CordaRPCClient(rpcAddress).use(user.username, user.password) { connection -> + connection.proxy + rpc.startFlow(::ObjectInputStreamFlow).returnValue.getOrThrow() + } + } + driver { + val devModeNode = startNode(devParams).getOrThrow() + val node = startNode(ALICE_NAME, devMode = false, parameters = params).getOrThrow() + + assertThatThrownBy { devModeNode.attemptJavaDeserialization() }.isInstanceOf(CordaRuntimeException::class.java) + assertThatThrownBy { node.attemptJavaDeserialization() }.isInstanceOf(InternalNodeException::class.java) } } diff --git a/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt b/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt index 7800bf67b2..cf438f16f8 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt @@ -4,15 +4,14 @@ import net.corda.core.crypto.Crypto import net.corda.core.internal.div import net.corda.core.utilities.getOrThrow import net.corda.node.services.config.configureDevKeyAndTrustStores -import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.testing.core.ALICE_NAME import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver +import net.corda.testing.internal.stubs.CertificateStoreStubs import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Test -import java.nio.file.Path import javax.security.auth.x500.X500Principal class NodeKeystoreCheckTest { @@ -21,22 +20,20 @@ class NodeKeystoreCheckTest { driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { assertThatThrownBy { startNode(customOverrides = mapOf("devMode" to false)).getOrThrow() - }.hasMessageContaining("Identity certificate not found") + }.hasMessageContaining("One or more keyStores (identity or TLS) or trustStore not found.") } } @Test fun `node should throw exception if cert path doesn't chain to the trust root`() { driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { - // Create keystores + // Create keystores. val keystorePassword = "password" - val config = object : SSLConfiguration { - override val keyStorePassword: String = keystorePassword - override val trustStorePassword: String = keystorePassword - override val certificatesDirectory: Path = baseDirectory(ALICE_NAME) / "certificates" - override val crlCheckSoftFail: Boolean = true - } - config.configureDevKeyAndTrustStores(ALICE_NAME) + val certificatesDirectory = baseDirectory(ALICE_NAME) / "certificates" + val signingCertStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory, keystorePassword) + val p2pSslConfig = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory, keyStorePassword = keystorePassword, trustStorePassword = keystorePassword) + + p2pSslConfig.configureDevKeyAndTrustStores(ALICE_NAME, signingCertStore, certificatesDirectory) // This should pass with correct keystore. val node = startNode( @@ -48,8 +45,8 @@ class NodeKeystoreCheckTest { node.stop() // Fiddle with node keystore. - config.loadNodeKeyStore().update { - // Self signed root + signingCertStore.get().update { + // Self signed root. val badRootKeyPair = Crypto.generateKeyPair() val badRoot = X509Utilities.createSelfSignedCACertificate(X500Principal("O=Bad Root,L=Lodnon,C=GB"), badRootKeyPair) val nodeCA = getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt index 1d7c8a4112..7cc77b2d96 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt @@ -19,6 +19,7 @@ import net.corda.testing.core.BOB_NAME import net.corda.testing.core.MAX_MESSAGE_SIZE import net.corda.testing.core.TestIdentity import net.corda.testing.driver.PortAllocation +import net.corda.testing.internal.stubs.CertificateStoreStubs import net.corda.testing.internal.rigorousMock import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID import org.apache.activemq.artemis.api.core.RoutingType @@ -27,7 +28,6 @@ import org.junit.Assert.assertArrayEquals import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder -import java.security.KeyStore import java.util.* import kotlin.test.assertEquals @@ -168,44 +168,54 @@ class AMQPBridgeTest { } private fun createArtemis(sourceQueueName: String?): Triple<ArtemisMessagingServer, ArtemisMessagingClient, BridgeManager> { + val baseDir = temporaryFolder.root.toPath() / "artemis" + val certificatesDirectory = baseDir / "certificates" + val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) val artemisConfig = rigorousMock<AbstractNodeConfiguration>().also { - doReturn(temporaryFolder.root.toPath() / "artemis").whenever(it).baseDirectory + doReturn(baseDir).whenever(it).baseDirectory doReturn(ALICE_NAME).whenever(it).myLegalName - doReturn("trustpass").whenever(it).trustStorePassword + doReturn(certificatesDirectory).whenever(it).certificatesDirectory + doReturn(signingCertificateStore).whenever(it).signingCertificateStore + doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions doReturn(true).whenever(it).crlCheckSoftFail - doReturn("cordacadevpass").whenever(it).keyStorePassword doReturn(artemisAddress).whenever(it).p2pAddress doReturn(null).whenever(it).jmxMonitoringHttpPort } artemisConfig.configureWithDevSSLCertificate() val artemisServer = ArtemisMessagingServer(artemisConfig, artemisAddress.copy(host = "0.0.0.0"), MAX_MESSAGE_SIZE) - val artemisClient = ArtemisMessagingClient(artemisConfig, artemisAddress, MAX_MESSAGE_SIZE) + val artemisClient = ArtemisMessagingClient(artemisConfig.p2pSslOptions, artemisAddress, MAX_MESSAGE_SIZE) artemisServer.start() artemisClient.start() - val bridgeManager = AMQPBridgeManager(artemisConfig, artemisAddress, MAX_MESSAGE_SIZE) + val bridgeManager = AMQPBridgeManager(artemisConfig.p2pSslOptions, artemisAddress, MAX_MESSAGE_SIZE) bridgeManager.start() val artemis = artemisClient.started!! if (sourceQueueName != null) { // Local queue for outgoing messages artemis.session.createQueue(sourceQueueName, RoutingType.ANYCAST, sourceQueueName, true) - bridgeManager.deployBridge(sourceQueueName, amqpAddress, setOf(BOB.name)) + bridgeManager.deployBridge(sourceQueueName, listOf(amqpAddress), setOf(BOB.name)) } return Triple(artemisServer, artemisClient, bridgeManager) } private fun createAMQPServer(maxMessageSize: Int = MAX_MESSAGE_SIZE): AMQPServer { + val baseDir = temporaryFolder.root.toPath() / "server" + val certificatesDirectory = baseDir / "certificates" + val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) val serverConfig = rigorousMock<AbstractNodeConfiguration>().also { doReturn(temporaryFolder.root.toPath() / "server").whenever(it).baseDirectory doReturn(BOB_NAME).whenever(it).myLegalName - doReturn("trustpass").whenever(it).trustStorePassword - doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn(certificatesDirectory).whenever(it).certificatesDirectory + doReturn(signingCertificateStore).whenever(it).signingCertificateStore + doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions } serverConfig.configureWithDevSSLCertificate() + val keyStore = serverConfig.p2pSslOptions.keyStore.get() val amqpConfig = object : AMQPConfiguration { - override val keyStore: KeyStore = serverConfig.loadSslKeyStore().internal - override val keyStorePrivateKeyPassword: CharArray = serverConfig.keyStorePassword.toCharArray() - override val trustStore: KeyStore = serverConfig.loadTrustStore().internal + override val keyStore = keyStore + override val trustStore = serverConfig.p2pSslOptions.trustStore.get() override val trace: Boolean = true override val maxMessageSize: Int = maxMessageSize } diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/CertificateRevocationListNodeTests.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/CertificateRevocationListNodeTests.kt index cb766bbcf8..66682cffa4 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/CertificateRevocationListNodeTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/CertificateRevocationListNodeTests.kt @@ -13,7 +13,8 @@ import net.corda.core.utilities.seconds import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.config.CertificateStoreSupplier +import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.crypto.* import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient @@ -27,6 +28,7 @@ import net.corda.testing.driver.PortAllocation import net.corda.testing.internal.DEV_INTERMEDIATE_CA import net.corda.testing.internal.DEV_ROOT_CA import net.corda.testing.internal.rigorousMock +import net.corda.testing.internal.stubs.CertificateStoreStubs import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.* import org.bouncycastle.cert.jcajce.JcaX509CRLConverter @@ -50,7 +52,6 @@ import java.io.Closeable import java.math.BigInteger import java.net.InetSocketAddress import java.security.KeyPair -import java.security.KeyStore import java.security.PrivateKey import java.security.Security import java.security.cert.X509CRL @@ -61,6 +62,7 @@ import javax.ws.rs.Path import javax.ws.rs.Produces import javax.ws.rs.core.Response import kotlin.test.assertEquals +import kotlin.test.assertFailsWith class CertificateRevocationListNodeTests { @Rule @@ -80,6 +82,32 @@ class CertificateRevocationListNodeTests { private abstract class AbstractNodeConfiguration : NodeConfiguration + companion object { + fun createRevocationList(clrServer: CrlServer, signatureAlgorithm: String, caCertificate: X509Certificate, + caPrivateKey: PrivateKey, + endpoint: String, + indirect: Boolean, + vararg serialNumbers: BigInteger): X509CRL { + println("Generating CRL for $endpoint") + val builder = JcaX509v2CRLBuilder(caCertificate.subjectX500Principal, Date(System.currentTimeMillis() - 1.minutes.toMillis())) + val extensionUtils = JcaX509ExtensionUtils() + builder.addExtension(Extension.authorityKeyIdentifier, + false, extensionUtils.createAuthorityKeyIdentifier(caCertificate)) + val issuingDistPointName = GeneralName( + GeneralName.uniformResourceIdentifier, + "http://${clrServer.hostAndPort.host}:${clrServer.hostAndPort.port}/crl/$endpoint") + // This is required and needs to match the certificate settings with respect to being indirect + val issuingDistPoint = IssuingDistributionPoint(DistributionPointName(GeneralNames(issuingDistPointName)), indirect, false) + builder.addExtension(Extension.issuingDistributionPoint, true, issuingDistPoint) + builder.setNextUpdate(Date(System.currentTimeMillis() + 1.seconds.toMillis())) + serialNumbers.forEach { + builder.addCRLEntry(it, Date(System.currentTimeMillis() - 10.minutes.toMillis()), ReasonFlags.certificateHold) + } + val signer = JcaContentSignerBuilder(signatureAlgorithm).setProvider(Crypto.findProvider("BC")).build(caPrivateKey) + return JcaX509CRLConverter().setProvider(Crypto.findProvider("BC")).getCRL(builder.build(signer)) + } + } + @Before fun setUp() { Security.addProvider(BouncyCastleProvider()) @@ -330,22 +358,25 @@ class CertificateRevocationListNodeTests { nodeCrlDistPoint: String = "http://${server.hostAndPort}/crl/node.crl", tlsCrlDistPoint: String? = "http://${server.hostAndPort}/crl/empty.crl", maxMessageSize: Int = MAX_MESSAGE_SIZE): Pair<AMQPClient, X509Certificate> { + val baseDirectory = temporaryFolder.root.toPath() / "client" + val certificatesDirectory = baseDirectory / "certificates" + val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) val clientConfig = rigorousMock<AbstractNodeConfiguration>().also { - doReturn(temporaryFolder.root.toPath() / "client").whenever(it).baseDirectory + doReturn(baseDirectory).whenever(it).baseDirectory + doReturn(certificatesDirectory).whenever(it).certificatesDirectory doReturn(BOB_NAME).whenever(it).myLegalName - doReturn("trustpass").whenever(it).trustStorePassword - doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions + doReturn(signingCertificateStore).whenever(it).signingCertificateStore doReturn(crlCheckSoftFail).whenever(it).crlCheckSoftFail } clientConfig.configureWithDevSSLCertificate() - val nodeCert = clientConfig.recreateNodeCaAndTlsCertificates(nodeCrlDistPoint, tlsCrlDistPoint) - val clientTruststore = clientConfig.loadTrustStore().internal - val clientKeystore = clientConfig.loadSslKeyStore().internal + val nodeCert = (signingCertificateStore to p2pSslConfiguration).recreateNodeCaAndTlsCertificates(nodeCrlDistPoint, tlsCrlDistPoint) + val keyStore = clientConfig.p2pSslOptions.keyStore.get() val amqpConfig = object : AMQPConfiguration { - override val keyStore: KeyStore = clientKeystore - override val keyStorePrivateKeyPassword: CharArray = clientConfig.keyStorePassword.toCharArray() - override val trustStore: KeyStore = clientTruststore + override val keyStore = keyStore + override val trustStore = clientConfig.p2pSslOptions.trustStore.get() override val crlCheckSoftFail: Boolean = crlCheckSoftFail override val maxMessageSize: Int = maxMessageSize } @@ -360,21 +391,24 @@ class CertificateRevocationListNodeTests { nodeCrlDistPoint: String = "http://${server.hostAndPort}/crl/node.crl", tlsCrlDistPoint: String? = "http://${server.hostAndPort}/crl/empty.crl", maxMessageSize: Int = MAX_MESSAGE_SIZE): Pair<AMQPServer, X509Certificate> { + val baseDirectory = temporaryFolder.root.toPath() / "server" + val certificatesDirectory = baseDirectory / "certificates" + val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) val serverConfig = rigorousMock<AbstractNodeConfiguration>().also { - doReturn(temporaryFolder.root.toPath() / "server").whenever(it).baseDirectory + doReturn(baseDirectory).whenever(it).baseDirectory + doReturn(certificatesDirectory).whenever(it).certificatesDirectory doReturn(name).whenever(it).myLegalName - doReturn("trustpass").whenever(it).trustStorePassword - doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions + doReturn(signingCertificateStore).whenever(it).signingCertificateStore doReturn(crlCheckSoftFail).whenever(it).crlCheckSoftFail } serverConfig.configureWithDevSSLCertificate() - val nodeCert = serverConfig.recreateNodeCaAndTlsCertificates(nodeCrlDistPoint, tlsCrlDistPoint) - val serverTruststore = serverConfig.loadTrustStore().internal - val serverKeystore = serverConfig.loadSslKeyStore().internal + val nodeCert = (signingCertificateStore to p2pSslConfiguration).recreateNodeCaAndTlsCertificates(nodeCrlDistPoint, tlsCrlDistPoint) + val keyStore = serverConfig.p2pSslOptions.keyStore.get() val amqpConfig = object : AMQPConfiguration { - override val keyStore: KeyStore = serverKeystore - override val keyStorePrivateKeyPassword: CharArray = serverConfig.keyStorePassword.toCharArray() - override val trustStore: KeyStore = serverTruststore + override val keyStore = keyStore + override val trustStore = serverConfig.p2pSslOptions.trustStore.get() override val crlCheckSoftFail: Boolean = crlCheckSoftFail override val maxMessageSize: Int = maxMessageSize } @@ -384,22 +418,28 @@ class CertificateRevocationListNodeTests { amqpConfig), nodeCert) } - private fun SSLConfiguration.recreateNodeCaAndTlsCertificates(nodeCaCrlDistPoint: String, tlsCrlDistPoint: String?): X509Certificate { - val nodeKeyStore = loadNodeKeyStore() - val (nodeCert, nodeKeys) = nodeKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) + private fun Pair<CertificateStoreSupplier, MutualSslConfiguration>.recreateNodeCaAndTlsCertificates(nodeCaCrlDistPoint: String, tlsCrlDistPoint: String?): X509Certificate { + + val signingCertificateStore = first + val p2pSslConfiguration = second + val nodeKeyStore = signingCertificateStore.get() + val (nodeCert, nodeKeys) = nodeKeyStore.query { getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) } val newNodeCert = replaceCrlDistPointCaCertificate(nodeCert, CertificateType.NODE_CA, INTERMEDIATE_CA.keyPair, nodeCaCrlDistPoint) - val nodeCertChain = listOf(newNodeCert, INTERMEDIATE_CA.certificate, *nodeKeyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA).drop(2).toTypedArray()) - nodeKeyStore.internal.deleteEntry(X509Utilities.CORDA_CLIENT_CA) - nodeKeyStore.save() + val nodeCertChain = listOf(newNodeCert, INTERMEDIATE_CA.certificate, *nodeKeyStore.query { getCertificateChain(X509Utilities.CORDA_CLIENT_CA) }.drop(2).toTypedArray()) + nodeKeyStore.update { + internal.deleteEntry(X509Utilities.CORDA_CLIENT_CA) + } nodeKeyStore.update { setPrivateKey(X509Utilities.CORDA_CLIENT_CA, nodeKeys.private, nodeCertChain) } - val sslKeyStore = loadSslKeyStore() - val (tlsCert, tlsKeys) = sslKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS) + val sslKeyStore = p2pSslConfiguration.keyStore.get() + val (tlsCert, tlsKeys) = sslKeyStore.query { getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS) } val newTlsCert = replaceCrlDistPointCaCertificate(tlsCert, CertificateType.TLS, nodeKeys, tlsCrlDistPoint, X500Name.getInstance(ROOT_CA.certificate.subjectX500Principal.encoded)) - val sslCertChain = listOf(newTlsCert, newNodeCert, INTERMEDIATE_CA.certificate, *sslKeyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).drop(3).toTypedArray()) - sslKeyStore.internal.deleteEntry(X509Utilities.CORDA_CLIENT_TLS) - sslKeyStore.save() + val sslCertChain = listOf(newTlsCert, newNodeCert, INTERMEDIATE_CA.certificate, *sslKeyStore.query { getCertificateChain(X509Utilities.CORDA_CLIENT_TLS) }.drop(3).toTypedArray()) + + sslKeyStore.update { + internal.deleteEntry(X509Utilities.CORDA_CLIENT_TLS) + } sslKeyStore.update { setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, tlsKeys.private, sslCertChain) } @@ -442,12 +482,15 @@ class CertificateRevocationListNodeTests { @Path("node.crl") @Produces("application/pkcs7-crl") fun getNodeCRL(): Response { - return Response.ok(createRevocationList( + return Response.ok(CertificateRevocationListNodeTests.createRevocationList( + server, + SIGNATURE_ALGORITHM, INTERMEDIATE_CA.certificate, INTERMEDIATE_CA.keyPair.private, NODE_CRL, false, - *revokedNodeCerts.toTypedArray()).encoded).build() + *revokedNodeCerts.toTypedArray()).encoded) + .build() } @GET @@ -455,11 +498,14 @@ class CertificateRevocationListNodeTests { @Produces("application/pkcs7-crl") fun getIntermediateCRL(): Response { return Response.ok(createRevocationList( + server, + SIGNATURE_ALGORITHM, ROOT_CA.certificate, ROOT_CA.keyPair.private, INTEMEDIATE_CRL, false, - *revokedIntermediateCerts.toTypedArray()).encoded).build() + *revokedIntermediateCerts.toTypedArray()).encoded) + .build() } @GET @@ -467,33 +513,13 @@ class CertificateRevocationListNodeTests { @Produces("application/pkcs7-crl") fun getEmptyCRL(): Response { return Response.ok(createRevocationList( + server, + SIGNATURE_ALGORITHM, ROOT_CA.certificate, ROOT_CA.keyPair.private, - EMPTY_CRL, true).encoded).build() - } - - private fun createRevocationList(caCertificate: X509Certificate, - caPrivateKey: PrivateKey, - endpoint: String, - indirect: Boolean, - vararg serialNumbers: BigInteger): X509CRL { - println("Generating CRL for $endpoint") - val builder = JcaX509v2CRLBuilder(caCertificate.subjectX500Principal, Date(System.currentTimeMillis() - 1.minutes.toMillis())) - val extensionUtils = JcaX509ExtensionUtils() - builder.addExtension(Extension.authorityKeyIdentifier, - false, extensionUtils.createAuthorityKeyIdentifier(caCertificate)) - val issuingDistPointName = GeneralName( - GeneralName.uniformResourceIdentifier, - "http://${server.hostAndPort.host}:${server.hostAndPort.port}/crl/$endpoint") - // This is required and needs to match the certificate settings with respect to being indirect - val issuingDistPoint = IssuingDistributionPoint(DistributionPointName(GeneralNames(issuingDistPointName)), indirect, false) - builder.addExtension(Extension.issuingDistributionPoint, true, issuingDistPoint) - builder.setNextUpdate(Date(System.currentTimeMillis() + 1.seconds.toMillis())) - serialNumbers.forEach { - builder.addCRLEntry(it, Date(System.currentTimeMillis() - 10.minutes.toMillis()), ReasonFlags.certificateHold) - } - val signer = JcaContentSignerBuilder(SIGNATURE_ALGORITHM).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(caPrivateKey) - return JcaX509CRLConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCRL(builder.build(signer)) + EMPTY_CRL, + true).encoded) + .build() } } @@ -533,4 +559,32 @@ class CertificateRevocationListNodeTests { } } } + + @Test + fun `verify CRL algorithms`() { + val ECDSA_ALGORITHM = "SHA256withECDSA" + val EC_ALGORITHM = "EC" + val EMPTY_CRL = "empty.crl" + + val crl = createRevocationList( + server, + ECDSA_ALGORITHM, + ROOT_CA.certificate, + ROOT_CA.keyPair.private, + EMPTY_CRL, + true) + // This should pass. + crl.verify(ROOT_CA.keyPair.public) + + // Try changing the algorithm to EC will fail. + assertFailsWith<IllegalArgumentException>("Unknown signature type requested: EC") { + createRevocationList( + server, + EC_ALGORITHM, + ROOT_CA.certificate, + ROOT_CA.keyPair.private, + EMPTY_CRL, + true) + } + } } diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt index 93016870ce..bc717e7109 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt @@ -12,16 +12,18 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.messaging.ArtemisMessagingServer -import net.corda.nodeapi.ArtemisTcpTransport.Companion.CIPHER_SUITES import net.corda.nodeapi.internal.ArtemisMessagingClient import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX -import net.corda.nodeapi.internal.config.SSLConfiguration -import net.corda.nodeapi.internal.createDevKeyStores -import net.corda.nodeapi.internal.crypto.* +import net.corda.nodeapi.internal.ArtemisTcpTransport +import net.corda.nodeapi.internal.config.MutualSslConfiguration +import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient import net.corda.nodeapi.internal.protonwrapper.netty.AMQPConfiguration import net.corda.nodeapi.internal.protonwrapper.netty.AMQPServer +import net.corda.nodeapi.internal.protonwrapper.netty.init +import net.corda.nodeapi.internal.registerDevP2pCertificates +import net.corda.nodeapi.internal.registerDevSigningCertificates import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.CHARLIE_NAME @@ -29,13 +31,13 @@ import net.corda.testing.core.MAX_MESSAGE_SIZE import net.corda.testing.driver.PortAllocation import net.corda.testing.internal.createDevIntermediateCaCertPath import net.corda.testing.internal.rigorousMock +import net.corda.testing.internal.stubs.CertificateStoreStubs import org.apache.activemq.artemis.api.core.RoutingType import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Assert.assertArrayEquals import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder -import java.security.KeyStore import java.security.cert.X509Certificate import javax.net.ssl.* import kotlin.concurrent.thread @@ -87,6 +89,22 @@ class ProtonWrapperTests { } } + @Test + fun `AMPQ Client fails to connect when crl soft fail check is disabled`() { + val amqpServer = createServer(serverPort, CordaX500Name("Rogue 1", "London", "GB"), + maxMessageSize = MAX_MESSAGE_SIZE, crlCheckSoftFail = false) + amqpServer.use { + amqpServer.start() + val amqpClient = createClient() + amqpClient.use { + val clientConnected = amqpClient.onConnection.toFuture() + amqpClient.start() + val clientConnect = clientConnected.get() + assertEquals(false, clientConnect.connected) + } + } + } + @Test fun `AMPQ Client refuses to connect to unexpected server`() { val amqpServer = createServer(serverPort, CordaX500Name("Rogue 1", "London", "GB")) @@ -102,34 +120,30 @@ class ProtonWrapperTests { } } - private fun SSLConfiguration.createTrustStore(rootCert: X509Certificate) { - val trustStore = loadOrCreateKeyStore(trustStoreFile, trustStorePassword) - trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert) - trustStore.save(trustStoreFile, trustStorePassword) - } + private fun MutualSslConfiguration.createTrustStore(rootCert: X509Certificate) { + trustStore.get(true)[X509Utilities.CORDA_ROOT_CA] = rootCert + } @Test fun `Test AMQP Client with invalid root certificate`() { - val sslConfig = object : SSLConfiguration { - override val certificatesDirectory = temporaryFolder.root.toPath() - override val keyStorePassword = "serverstorepass" - override val trustStorePassword = "trustpass" - override val crlCheckSoftFail: Boolean = true - } + val certificatesDirectory = temporaryFolder.root.toPath() + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory, "serverstorepass") + val sslConfig = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory, keyStorePassword = "serverstorepass") val (rootCa, intermediateCa) = createDevIntermediateCaCertPath() // Generate server cert and private key and populate another keystore suitable for SSL - sslConfig.createDevKeyStores(ALICE_NAME, rootCa.certificate, intermediateCa) + signingCertificateStore.get(true).also { it.registerDevSigningCertificates(ALICE_NAME, rootCa.certificate, intermediateCa) } + sslConfig.keyStore.get(true).also { it.registerDevP2pCertificates(ALICE_NAME, rootCa.certificate, intermediateCa) } sslConfig.createTrustStore(rootCa.certificate) - val keyStore = loadKeyStore(sslConfig.sslKeystore, sslConfig.keyStorePassword) - val trustStore = loadKeyStore(sslConfig.trustStoreFile, sslConfig.trustStorePassword) + val keyStore = sslConfig.keyStore.get() + val trustStore = sslConfig.trustStore.get() val context = SSLContext.getInstance("TLS") val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) - keyManagerFactory.init(keyStore, sslConfig.keyStorePassword.toCharArray()) + keyManagerFactory.init(keyStore) val keyManagers = keyManagerFactory.keyManagers val trustMgrFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) trustMgrFactory.init(trustStore) @@ -139,7 +153,7 @@ class ProtonWrapperTests { val serverSocketFactory = context.serverSocketFactory val serverSocket = serverSocketFactory.createServerSocket(serverPort) as SSLServerSocket - val serverParams = SSLParameters(CIPHER_SUITES.toTypedArray(), + val serverParams = SSLParameters(ArtemisTcpTransport.CIPHER_SUITES.toTypedArray(), arrayOf("TLSv1.2")) serverParams.wantClientAuth = true serverParams.needClientAuth = true @@ -388,11 +402,16 @@ class ProtonWrapperTests { } private fun createArtemisServerAndClient(maxMessageSize: Int = MAX_MESSAGE_SIZE): Pair<ArtemisMessagingServer, ArtemisMessagingClient> { + val baseDirectory = temporaryFolder.root.toPath() / "artemis" + val certificatesDirectory = baseDirectory / "certificates" + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) + val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) val artemisConfig = rigorousMock<AbstractNodeConfiguration>().also { - doReturn(temporaryFolder.root.toPath() / "artemis").whenever(it).baseDirectory + doReturn(baseDirectory).whenever(it).baseDirectory + doReturn(certificatesDirectory).whenever(it).certificatesDirectory doReturn(CHARLIE_NAME).whenever(it).myLegalName - doReturn("trustpass").whenever(it).trustStorePassword - doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn(signingCertificateStore).whenever(it).signingCertificateStore + doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions doReturn(NetworkHostAndPort("0.0.0.0", artemisPort)).whenever(it).p2pAddress doReturn(null).whenever(it).jmxMonitoringHttpPort doReturn(true).whenever(it).crlCheckSoftFail @@ -400,28 +419,32 @@ class ProtonWrapperTests { artemisConfig.configureWithDevSSLCertificate() val server = ArtemisMessagingServer(artemisConfig, NetworkHostAndPort("0.0.0.0", artemisPort), maxMessageSize) - val client = ArtemisMessagingClient(artemisConfig, NetworkHostAndPort("localhost", artemisPort), maxMessageSize) + val client = ArtemisMessagingClient(artemisConfig.p2pSslOptions, NetworkHostAndPort("localhost", artemisPort), maxMessageSize) server.start() client.start() return Pair(server, client) } private fun createClient(maxMessageSize: Int = MAX_MESSAGE_SIZE): AMQPClient { + val baseDirectory = temporaryFolder.root.toPath() / "client" + val certificatesDirectory = baseDirectory / "certificates" + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) + val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) val clientConfig = rigorousMock<AbstractNodeConfiguration>().also { - doReturn(temporaryFolder.root.toPath() / "client").whenever(it).baseDirectory + doReturn(baseDirectory).whenever(it).baseDirectory + doReturn(certificatesDirectory).whenever(it).certificatesDirectory doReturn(BOB_NAME).whenever(it).myLegalName - doReturn("trustpass").whenever(it).trustStorePassword - doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn(signingCertificateStore).whenever(it).signingCertificateStore + doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions doReturn(true).whenever(it).crlCheckSoftFail } clientConfig.configureWithDevSSLCertificate() - val clientTruststore = clientConfig.loadTrustStore().internal - val clientKeystore = clientConfig.loadSslKeyStore().internal + val clientTruststore = clientConfig.p2pSslOptions.trustStore.get() + val clientKeystore = clientConfig.p2pSslOptions.keyStore.get() val amqpConfig = object : AMQPConfiguration { - override val keyStore: KeyStore = clientKeystore - override val keyStorePrivateKeyPassword: CharArray = clientConfig.keyStorePassword.toCharArray() - override val trustStore: KeyStore = clientTruststore + override val keyStore = clientKeystore + override val trustStore = clientTruststore override val trace: Boolean = true override val maxMessageSize: Int = maxMessageSize } @@ -434,21 +457,25 @@ class ProtonWrapperTests { } private fun createSharedThreadsClient(sharedEventGroup: EventLoopGroup, id: Int, maxMessageSize: Int = MAX_MESSAGE_SIZE): AMQPClient { + val baseDirectory = temporaryFolder.root.toPath() / "client_%$id" + val certificatesDirectory = baseDirectory / "certificates" + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) + val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) val clientConfig = rigorousMock<AbstractNodeConfiguration>().also { - doReturn(temporaryFolder.root.toPath() / "client_%$id").whenever(it).baseDirectory + doReturn(baseDirectory).whenever(it).baseDirectory + doReturn(certificatesDirectory).whenever(it).certificatesDirectory doReturn(CordaX500Name(null, "client $id", "Corda", "London", null, "GB")).whenever(it).myLegalName - doReturn("trustpass").whenever(it).trustStorePassword - doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn(signingCertificateStore).whenever(it).signingCertificateStore + doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions doReturn(true).whenever(it).crlCheckSoftFail } clientConfig.configureWithDevSSLCertificate() - val clientTruststore = clientConfig.loadTrustStore().internal - val clientKeystore = clientConfig.loadSslKeyStore().internal + val clientTruststore = clientConfig.p2pSslOptions.trustStore.get() + val clientKeystore = clientConfig.p2pSslOptions.keyStore.get() val amqpConfig = object : AMQPConfiguration { - override val keyStore: KeyStore = clientKeystore - override val keyStorePrivateKeyPassword: CharArray = clientConfig.keyStorePassword.toCharArray() - override val trustStore: KeyStore = clientTruststore + override val keyStore = clientKeystore + override val trustStore = clientTruststore override val trace: Boolean = true override val maxMessageSize: Int = maxMessageSize } @@ -459,22 +486,29 @@ class ProtonWrapperTests { sharedThreadPool = sharedEventGroup) } - private fun createServer(port: Int, name: CordaX500Name = ALICE_NAME, maxMessageSize: Int = MAX_MESSAGE_SIZE): AMQPServer { + private fun createServer(port: Int, + name: CordaX500Name = ALICE_NAME, + maxMessageSize: Int = MAX_MESSAGE_SIZE, + crlCheckSoftFail: Boolean = true): AMQPServer { + val baseDirectory = temporaryFolder.root.toPath() / "server" + val certificatesDirectory = baseDirectory / "certificates" + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) + val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) val serverConfig = rigorousMock<AbstractNodeConfiguration>().also { - doReturn(temporaryFolder.root.toPath() / "server").whenever(it).baseDirectory + doReturn(baseDirectory).whenever(it).baseDirectory + doReturn(certificatesDirectory).whenever(it).certificatesDirectory doReturn(name).whenever(it).myLegalName - doReturn("trustpass").whenever(it).trustStorePassword - doReturn("cordacadevpass").whenever(it).keyStorePassword - doReturn(true).whenever(it).crlCheckSoftFail + doReturn(signingCertificateStore).whenever(it).signingCertificateStore + doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions + doReturn(crlCheckSoftFail).whenever(it).crlCheckSoftFail } serverConfig.configureWithDevSSLCertificate() - val serverTruststore = serverConfig.loadTrustStore().internal - val serverKeystore = serverConfig.loadSslKeyStore().internal + val serverTruststore = serverConfig.p2pSslOptions.trustStore.get() + val serverKeystore = serverConfig.p2pSslOptions.keyStore.get() val amqpConfig = object : AMQPConfiguration { - override val keyStore: KeyStore = serverKeystore - override val keyStorePrivateKeyPassword: CharArray = serverConfig.keyStorePassword.toCharArray() - override val trustStore: KeyStore = serverTruststore + override val keyStore = serverKeystore + override val trustStore = serverTruststore override val trace: Boolean = true override val maxMessageSize: Int = maxMessageSize } diff --git a/node/src/integration-test/kotlin/net/corda/node/flows/AsymmetricCorDappsTests.kt b/node/src/integration-test/kotlin/net/corda/node/flows/AsymmetricCorDappsTests.kt index 7034174181..2875c21527 100644 --- a/node/src/integration-test/kotlin/net/corda/node/flows/AsymmetricCorDappsTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/flows/AsymmetricCorDappsTests.kt @@ -8,6 +8,8 @@ import net.corda.core.internal.packageName import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow 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.TestCorDapp @@ -48,8 +50,8 @@ class AsymmetricCorDappsTests { driver(DriverParameters(startNodesInProcess = false, cordappsForAllNodes = emptySet())) { - val nodeA = startNode(additionalCordapps = setOf(TestCorDapp.Factory.create("Szymon CorDapp", "1.0", classes = setOf(Ping::class.java)))).getOrThrow() - val nodeB = startNode(additionalCordapps = setOf(TestCorDapp.Factory.create("Szymon CorDapp", "1.0", classes = setOf(Ping::class.java, Pong::class.java)))).getOrThrow() + val nodeA = startNode(providedName = ALICE_NAME, additionalCordapps = setOf(TestCorDapp.Factory.create("Szymon CorDapp", "1.0", classes = setOf(Ping::class.java)))).getOrThrow() + val nodeB = startNode(providedName = BOB_NAME, additionalCordapps = setOf(TestCorDapp.Factory.create("Szymon CorDapp", "1.0", classes = setOf(Ping::class.java, Pong::class.java)))).getOrThrow() nodeA.rpc.startFlow(::Ping, nodeB.nodeInfo.singleIdentity(), 1).returnValue.getOrThrow() } } @@ -63,7 +65,7 @@ class AsymmetricCorDappsTests { val cordappForNodeB = TestCorDapp.Factory.create("nodeB_only", "1.0", classes = setOf(Pong::class.java)) driver(DriverParameters(startNodesInProcess = false, cordappsForAllNodes = setOf(sharedCordapp))) { - val (nodeA, nodeB) = listOf(startNode(), startNode(additionalCordapps = setOf(cordappForNodeB))).transpose().getOrThrow() + val (nodeA, nodeB) = listOf(startNode(providedName = ALICE_NAME), startNode(providedName = BOB_NAME, additionalCordapps = setOf(cordappForNodeB))).transpose().getOrThrow() nodeA.rpc.startFlow(::Ping, nodeB.nodeInfo.singleIdentity(), 1).returnValue.getOrThrow() } } @@ -77,7 +79,7 @@ class AsymmetricCorDappsTests { val cordappForNodeB = TestCorDapp.Factory.create("nodeB_only", "1.0", classes = setOf(Pong::class.java)) driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = setOf(sharedCordapp))) { - val (nodeA, nodeB) = listOf(startNode(), startNode(additionalCordapps = setOf(cordappForNodeB))).transpose().getOrThrow() + val (nodeA, nodeB) = listOf(startNode(providedName = ALICE_NAME), startNode(providedName = BOB_NAME, additionalCordapps = setOf(cordappForNodeB))).transpose().getOrThrow() nodeA.rpc.startFlow(::Ping, nodeB.nodeInfo.singleIdentity(), 1).returnValue.getOrThrow() } } diff --git a/node/src/integration-test/kotlin/net/corda/node/flows/FlowRetryTest.kt b/node/src/integration-test/kotlin/net/corda/node/flows/FlowRetryTest.kt index 548f904d7e..cc5361ee4c 100644 --- a/node/src/integration-test/kotlin/net/corda/node/flows/FlowRetryTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/flows/FlowRetryTest.kt @@ -10,6 +10,8 @@ import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap import net.corda.node.services.Permissions +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 @@ -38,8 +40,8 @@ class FlowRetryTest { startNodesInProcess = isQuasarAgentSpecified(), notarySpecs = emptyList() )) { - val nodeAHandle = startNode(rpcUsers = listOf(user)).getOrThrow() - val nodeBHandle = startNode(rpcUsers = listOf(user)).getOrThrow() + val nodeAHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() + val nodeBHandle = startNode(providedName = BOB_NAME, rpcUsers = listOf(user)).getOrThrow() val result = CordaRPCClient(nodeAHandle.rpcAddress).start(user.username, user.password).use { it.proxy.startFlow(::InitiatorFlow, numSessions, numIterations, nodeBHandle.nodeInfo.singleIdentity()).returnValue.getOrThrow() @@ -66,7 +68,7 @@ class InitiatorFlow(private val sessionsCount: Int, private val iterationsCount: fun tracker() = ProgressTracker(FIRST_STEP) - val seen = Collections.synchronizedSet(HashSet<Visited>()) + val seen: MutableSet<Visited> = Collections.synchronizedSet(HashSet<Visited>()) fun visit(sessionNum: Int, iterationNum: Int, step: Step) { val visited = Visited(sessionNum, iterationNum, step) @@ -117,7 +119,7 @@ class InitiatedFlow(val session: FlowSession) : FlowLogic<Any>() { fun tracker() = ProgressTracker(FIRST_STEP) - val seen = Collections.synchronizedSet(HashSet<Visited>()) + val seen: MutableSet<Visited> = Collections.synchronizedSet(HashSet<Visited>()) fun visit(sessionNum: Int, iterationNum: Int, step: Step) { val visited = Visited(sessionNum, iterationNum, step) @@ -154,4 +156,4 @@ data class SessionInfo(val sessionNum: Int, val iterationsCount: Int) enum class Step { First, BeforeInitiate, AfterInitiate, AfterInitiateSendReceive, BeforeSend, AfterSend, BeforeReceive, AfterReceive } -data class Visited(val sessionNum: Int, val iterationNum: Int, val step: Step) \ No newline at end of file +data class Visited(val sessionNum: Int, val iterationNum: Int, val step: Step) diff --git a/node/src/integration-test/kotlin/net/corda/node/modes/draining/P2PFlowsDrainingModeTest.kt b/node/src/integration-test/kotlin/net/corda/node/modes/draining/P2PFlowsDrainingModeTest.kt index 22961b1c40..40b172e162 100644 --- a/node/src/integration-test/kotlin/net/corda/node/modes/draining/P2PFlowsDrainingModeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/modes/draining/P2PFlowsDrainingModeTest.kt @@ -1,15 +1,19 @@ package net.corda.node.modes.draining import co.paralleluniverse.fibers.Suspendable -import net.corda.client.rpc.internal.drainAndShutdown -import net.corda.core.flows.* +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.internal.concurrent.map import net.corda.core.messaging.startFlow +import net.corda.core.utilities.contextLogger import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.loggerFor import net.corda.core.utilities.unwrap import net.corda.node.services.Permissions +import net.corda.nodeapi.internal.hasCancelledDrainingShutdown import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.singleIdentity @@ -18,6 +22,7 @@ import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.driver import net.corda.testing.internal.chooseIdentity import net.corda.testing.node.User +import net.corda.testing.node.internal.waitForShutdown import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat import org.junit.After import org.junit.Before @@ -29,6 +34,9 @@ import java.util.concurrent.TimeUnit import kotlin.test.fail class P2PFlowsDrainingModeTest { + companion object { + private val logger = contextLogger() + } private val portAllocation = PortAllocation.Incremental(10000) private val user = User("mark", "dadada", setOf(Permissions.all())) @@ -36,10 +44,6 @@ class P2PFlowsDrainingModeTest { private var executor: ScheduledExecutorService? = null - companion object { - private val logger = loggerFor<P2PFlowsDrainingModeTest>() - } - @Before fun setup() { executor = Executors.newSingleThreadScheduledExecutor() @@ -81,24 +85,74 @@ class P2PFlowsDrainingModeTest { } @Test - fun `clean shutdown by draining`() { - driver(DriverParameters(startNodesInProcess = true, portAllocation = portAllocation, notarySpecs = emptyList())) { + fun `terminate node waiting for pending flows`() { + + driver(DriverParameters(portAllocation = portAllocation, notarySpecs = emptyList())) { + val nodeA = startNode(providedName = ALICE_NAME, rpcUsers = users).getOrThrow() val nodeB = startNode(providedName = BOB_NAME, rpcUsers = users).getOrThrow() var successful = false val latch = CountDownLatch(1) + nodeB.rpc.setFlowsDrainingModeEnabled(true) IntRange(1, 10).forEach { nodeA.rpc.startFlow(::InitiateSessionFlow, nodeB.nodeInfo.chooseIdentity()) } - nodeA.rpc.drainAndShutdown() - .doOnError { error -> - error.printStackTrace() - successful = false - } - .doOnCompleted { successful = true } - .doAfterTerminate { latch.countDown() } - .subscribe() + nodeA.waitForShutdown().doOnError(Throwable::printStackTrace).doOnError { successful = false }.doOnCompleted { successful = true }.doAfterTerminate(latch::countDown).subscribe() + + nodeA.rpc.terminate(true) nodeB.rpc.setFlowsDrainingModeEnabled(false) + + latch.await() + + assertThat(successful).isTrue() + } + } + + @Test + fun `terminate resets persistent draining mode property when waiting for pending flows`() { + + driver(DriverParameters(portAllocation = portAllocation, notarySpecs = emptyList())) { + + val nodeA = startNode(providedName = ALICE_NAME, rpcUsers = users).getOrThrow() + var successful = false + val latch = CountDownLatch(1) + + // This would not be needed, as `terminate(true)` sets draining mode anyway, but it's here to ensure that it removes the persistent value anyway. + nodeA.rpc.setFlowsDrainingModeEnabled(true) + nodeA.rpc.waitForShutdown().doOnError(Throwable::printStackTrace).doOnError { successful = false }.doOnCompleted(nodeA::stop).doOnCompleted { + val nodeARestarted = startNode(providedName = ALICE_NAME, rpcUsers = users).getOrThrow() + successful = !nodeARestarted.rpc.isFlowsDrainingModeEnabled() + }.doAfterTerminate(latch::countDown).subscribe() + + nodeA.rpc.terminate(true) + + latch.await() + + assertThat(successful).isTrue() + } + } + + @Test + fun `disabling draining mode cancels draining shutdown`() { + + driver(DriverParameters(portAllocation = portAllocation, notarySpecs = emptyList())) { + + val nodeA = startNode(providedName = ALICE_NAME, rpcUsers = users).getOrThrow() + val nodeB = startNode(providedName = BOB_NAME, rpcUsers = users).getOrThrow() + var successful = false + val latch = CountDownLatch(1) + + nodeB.rpc.setFlowsDrainingModeEnabled(true) + IntRange(1, 10).forEach { nodeA.rpc.startFlow(::InitiateSessionFlow, nodeB.nodeInfo.chooseIdentity()) } + + nodeA.waitForShutdown().doOnError(Throwable::printStackTrace).doAfterTerminate { successful = false }.doAfterTerminate(latch::countDown).subscribe() + + nodeA.rpc.terminate(true) + nodeA.rpc.hasCancelledDrainingShutdown().doOnError(Throwable::printStackTrace).doOnError { successful = false }.doOnCompleted { successful = true }.doAfterTerminate(latch::countDown).subscribe() + + nodeA.rpc.setFlowsDrainingModeEnabled(false) + nodeB.rpc.setFlowsDrainingModeEnabled(false) + latch.await() assertThat(successful).isTrue() 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 fb17259383..d21de023cb 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 @@ -23,8 +23,10 @@ import net.corda.core.serialization.SerializationFactory import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.contextLogger import net.corda.core.utilities.getOrThrow +import net.corda.node.VersionInfo import net.corda.node.internal.cordapp.JarScanningCordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl +import net.corda.nodeapi.internal.PLATFORM_VERSION import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME @@ -50,7 +52,7 @@ class AttachmentLoadingTests { @JvmField val testSerialization = SerializationEnvironmentRule() private val attachments = MockAttachmentStorage() - private val provider = CordappProviderImpl(JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR)), MockCordappConfigProvider(), attachments).apply { + private val provider = CordappProviderImpl(JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR), VersionInfo.UNKNOWN), MockCordappConfigProvider(), attachments).apply { start(testNetworkParameters().whitelistedContractImplementations) } private val cordapp get() = provider.cordapps.first() diff --git a/node/src/integration-test/kotlin/net/corda/node/services/events/ScheduledFlowIntegrationTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/events/ScheduledFlowIntegrationTests.kt index 4abb273b48..f858e6189c 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/events/ScheduledFlowIntegrationTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/events/ScheduledFlowIntegrationTests.kt @@ -116,4 +116,4 @@ class ScheduledFlowIntegrationTests { assertEquals(aliceSpentStates.count(), bobSpentStates.count()) } } -} \ No newline at end of file +} diff --git a/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt index 0121d8be47..fef890d2ec 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt @@ -1,18 +1,20 @@ package net.corda.node.services.messaging +import com.codahale.metrics.MetricRegistry import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever import net.corda.core.crypto.generateKeyPair +import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.seconds import net.corda.node.internal.configureDatabase import net.corda.node.services.config.FlowTimeoutConfiguration import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.configureWithDevSSLCertificate -import net.corda.node.services.network.NetworkMapCacheImpl import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor +import net.corda.node.utilities.TestingNamedCacheFactory import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.ALICE_NAME @@ -21,6 +23,7 @@ import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.driver.PortAllocation import net.corda.testing.internal.LogHelper import net.corda.testing.internal.rigorousMock +import net.corda.testing.internal.stubs.CertificateStoreStubs import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.internal.MOCK_VERSION_INFO import org.apache.activemq.artemis.api.core.ActiveMQConnectionTimedOutException @@ -64,24 +67,30 @@ class ArtemisMessagingTest { private var messagingClient: P2PMessagingClient? = null private var messagingServer: ArtemisMessagingServer? = null - private lateinit var networkMapCache: NetworkMapCacheImpl + private lateinit var networkMapCache: PersistentNetworkMapCache @Before fun setUp() { abstract class AbstractNodeConfiguration : NodeConfiguration + + val baseDirectory = temporaryFolder.root.toPath() + val certificatesDirectory = baseDirectory / "certificates" + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) + val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) + config = rigorousMock<AbstractNodeConfiguration>().also { doReturn(temporaryFolder.root.toPath()).whenever(it).baseDirectory doReturn(ALICE_NAME).whenever(it).myLegalName - doReturn("trustpass").whenever(it).trustStorePassword - doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn(certificatesDirectory).whenever(it).certificatesDirectory + doReturn(signingCertificateStore).whenever(it).signingCertificateStore + doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions doReturn(NetworkHostAndPort("0.0.0.0", serverPort)).whenever(it).p2pAddress doReturn(null).whenever(it).jmxMonitoringHttpPort doReturn(FlowTimeoutConfiguration(5.seconds, 3, backoffBase = 1.0)).whenever(it).flowTimeout } LogHelper.setLevel(PersistentUniquenessProvider::class) database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), { null }, { null }) - val persistentNetworkMapCache = PersistentNetworkMapCache(database, ALICE_NAME).apply { start(emptyList()) } - networkMapCache = NetworkMapCacheImpl(persistentNetworkMapCache, rigorousMock(), database).apply { start() } + networkMapCache = PersistentNetworkMapCache(TestingNamedCacheFactory(), database, rigorousMock()).apply { start(emptyList()) } } @After @@ -216,6 +225,8 @@ class ArtemisMessagingTest { ServiceAffinityExecutor("ArtemisMessagingTests", 1), database, networkMapCache, + MetricRegistry(), + TestingNamedCacheFactory(), isDrainingModeOn = { false }, drainingModeWasChangedEvents = PublishSubject.create<Pair<Boolean, Boolean>>()).apply { config.configureWithDevSSLCertificate() diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt index 91d4299702..487135a000 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt @@ -1,8 +1,6 @@ package net.corda.node.services.network -import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.random63BitValue -import net.corda.core.identity.CordaX500Name import net.corda.core.internal.* import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.ParametersUpdateInfo @@ -10,9 +8,7 @@ import net.corda.core.node.NodeInfo import net.corda.core.serialization.serialize import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds -import net.corda.node.services.config.configureDevKeyAndTrustStores import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY -import net.corda.nodeapi.internal.config.NodeSSLConfiguration import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME import net.corda.nodeapi.internal.network.SignedNetworkParameters @@ -242,19 +238,3 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP assertThat(rpc.networkMapSnapshot()).containsOnly(*nodes) } } - -private fun DriverDSLImpl.startNode(providedName: CordaX500Name, devMode: Boolean): CordaFuture<NodeHandle> { - var customOverrides = emptyMap<String, String>() - if (!devMode) { - val nodeDir = baseDirectory(providedName) - val nodeSslConfig = object : NodeSSLConfiguration { - override val baseDirectory = nodeDir - override val keyStorePassword = "cordacadevpass" - override val trustStorePassword = "trustpass" - override val crlCheckSoftFail = true - } - nodeSslConfig.configureDevKeyAndTrustStores(providedName) - customOverrides = mapOf("devMode" to "false") - } - return startNode(providedName = providedName, customOverrides = customOverrides) -} diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt index 1cc3ff5539..1eea4f49db 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt @@ -1,10 +1,12 @@ package net.corda.node.services.network import net.corda.core.node.NodeInfo -import net.corda.core.serialization.serialize import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.internal.configureDatabase import net.corda.node.internal.schemas.NodeInfoSchemaV1 +import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.node.utilities.TestingNamedCacheFactory +import net.corda.nodeapi.internal.DEV_ROOT_CA import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.* import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties @@ -18,7 +20,6 @@ class PersistentNetworkMapCacheTest { private companion object { val ALICE = TestIdentity(ALICE_NAME, 70) val BOB = TestIdentity(BOB_NAME, 80) - val CHARLIE = TestIdentity(CHARLIE_NAME, 90) } @Rule @@ -27,7 +28,7 @@ class PersistentNetworkMapCacheTest { private var portCounter = 1000 private val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), { null }, { null }) - private val charlieNetMapCache = PersistentNetworkMapCache(database, CHARLIE.name) + private val charlieNetMapCache = PersistentNetworkMapCache(TestingNamedCacheFactory(), database, InMemoryIdentityService(trustRoot = DEV_ROOT_CA.certificate)) @After fun cleanUp() { @@ -38,7 +39,6 @@ class PersistentNetworkMapCacheTest { fun addNode() { val alice = createNodeInfo(listOf(ALICE)) charlieNetMapCache.addNode(alice) - assertThat(charlieNetMapCache.nodeReady).isDone() val fromDb = database.transaction { session.createQuery( "from ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name}", @@ -48,32 +48,6 @@ class PersistentNetworkMapCacheTest { assertThat(fromDb).containsOnly(alice) } - @Test - fun `adding the node's own node-info doesn't complete the nodeReady future`() { - val charlie = createNodeInfo(listOf(CHARLIE)) - charlieNetMapCache.addNode(charlie) - assertThat(charlieNetMapCache.nodeReady).isNotDone() - assertThat(charlieNetMapCache.getNodeByLegalName(CHARLIE.name)).isEqualTo(charlie) - } - - @Test - fun `starting with just the node's own node-info in the db`() { - val charlie = createNodeInfo(listOf(CHARLIE)) - saveNodeInfoIntoDb(charlie) - assertThat(charlieNetMapCache.allNodes).containsOnly(charlie) - charlieNetMapCache.start(emptyList()) - assertThat(charlieNetMapCache.nodeReady).isNotDone() - } - - @Test - fun `starting with another node-info in the db`() { - val alice = createNodeInfo(listOf(ALICE)) - saveNodeInfoIntoDb(alice) - assertThat(charlieNetMapCache.allNodes).containsOnly(alice) - charlieNetMapCache.start(emptyList()) - assertThat(charlieNetMapCache.nodeReady).isDone() - } - @Test fun `unknown legal name`() { charlieNetMapCache.addNode(createNodeInfo(listOf(ALICE))) @@ -135,19 +109,4 @@ class PersistentNetworkMapCacheTest { serial = 1 ) } - - private fun saveNodeInfoIntoDb(nodeInfo: NodeInfo) { - database.transaction { - session.save(NodeInfoSchemaV1.PersistentNodeInfo( - id = 0, - hash = nodeInfo.serialize().hash.toString(), - addresses = nodeInfo.addresses.map { NodeInfoSchemaV1.DBHostAndPort.fromHostAndPort(it) }, - legalIdentitiesAndCerts = nodeInfo.legalIdentitiesAndCerts.mapIndexed { idx, elem -> - NodeInfoSchemaV1.DBPartyAndCertificate(elem, isMain = idx == 0) - }, - platformVersion = nodeInfo.platformVersion, - serial = nodeInfo.serial - )) - } - } } diff --git a/node/src/integration-test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt index 36d7d9bba2..c1e3cc935b 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt @@ -17,13 +17,13 @@ import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate import net.corda.node.utilities.saveToKeyStore import net.corda.node.utilities.saveToTrustStore -import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport import net.corda.nodeapi.BrokerRpcSslOptions -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport +import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.config.User import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.driver.PortAllocation -import net.corda.testing.internal.createNodeSslConfig +import net.corda.testing.internal.p2pSslOptions import org.apache.activemq.artemis.api.core.ActiveMQConnectionTimedOutException import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl import org.assertj.core.api.Assertions.assertThat @@ -58,12 +58,12 @@ class ArtemisRpcTests { val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password") val trustStorePath = saveToTrustStore(tempFile("rpcTruststore.jks"), selfSignCert) val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password") - testSslCommunication(createNodeSslConfig(tempFolder.root.toPath()), brokerSslOptions, true, clientSslOptions) + testSslCommunication(p2pSslOptions(tempFolder.root.toPath()), brokerSslOptions, true, clientSslOptions) } @Test fun rpc_with_ssl_disabled() { - testSslCommunication(createNodeSslConfig(tempFolder.root.toPath()), null, false, null) + testSslCommunication(p2pSslOptions(tempFolder.root.toPath()), null, false, null) } @Test @@ -73,7 +73,7 @@ class ArtemisRpcTests { val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password") // here client sslOptions are passed null (as in, do not use SSL) assertThatThrownBy { - testSslCommunication(createNodeSslConfig(tempFolder.root.toPath()), brokerSslOptions, true, null) + testSslCommunication(p2pSslOptions(tempFolder.root.toPath()), brokerSslOptions, true, null) }.isInstanceOf(ActiveMQConnectionTimedOutException::class.java) } @@ -91,11 +91,11 @@ class ArtemisRpcTests { val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password") assertThatThrownBy { - testSslCommunication(createNodeSslConfig(tempFolder.root.toPath()), brokerSslOptions, true, clientSslOptions) + testSslCommunication(p2pSslOptions(tempFolder.root.toPath()), brokerSslOptions, true, clientSslOptions) }.isInstanceOf(RPCException::class.java) } - private fun testSslCommunication(nodeSSlconfig: SSLConfiguration, + private fun testSslCommunication(nodeSSlconfig: MutualSslConfiguration, brokerSslOptions: BrokerRpcSslOptions?, useSslForBroker: Boolean, clientSslOptions: ClientRpcSslOptions?, @@ -140,7 +140,7 @@ class ArtemisRpcTests { class TestRpcOpsImpl : TestRpcOps { override fun greet(name: String): String = "Oh, hello $name!" - override val protocolVersion: Int = 1 + override val protocolVersion: Int = 1000 } private fun tempFile(name: String): Path = tempFolder.root.toPath() / name 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 index c46c6be298..04b293adc0 100644 --- 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 @@ -9,13 +9,13 @@ 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.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.NodeParameters -import net.corda.testing.driver.driver +import net.corda.testing.driver.* import net.corda.testing.node.User +import net.corda.testing.node.internal.startNode import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat import org.hibernate.exception.GenericJDBCException @@ -23,75 +23,95 @@ 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, notarySpecs = emptyList())) { + fun `rpc client receive client-relevant exceptions regardless of devMode`() { + val params = NodeParameters(rpcUsers = users) + val clientRelevantMessage = "This is for the players!" - val node = startNode(NodeParameters(rpcUsers = users)).getOrThrow() - - assertThatThrownBy { node.rpc.startFlow(::Flow).returnValue.getOrThrow() }.isInstanceOfSatisfying(CordaRuntimeException::class.java) { exception -> - - assertThat(exception).hasNoCause() - assertThat(exception.stackTrace).isEmpty() - } + fun NodeHandle.throwExceptionFromFlow() { + rpc.startFlow(::ClientRelevantErrorFlow, clientRelevantMessage).returnValue.getOrThrow() } - } - @Test - fun `rpc client handles client-relevant exceptions thrown on node side`() { - driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { - - val node = startNode(NodeParameters(rpcUsers = users)).getOrThrow() - val clientRelevantMessage = "This is for the players!" - - assertThatThrownBy { node.rpc.startFlow(::ClientRelevantErrorFlow, clientRelevantMessage).returnValue.getOrThrow() }.isInstanceOfSatisfying(CordaRuntimeException::class.java) { exception -> + fun assertThatThrownExceptionIsReceivedUnwrapped(node: NodeHandle) { + assertThatThrownBy { node.throwExceptionFromFlow() }.isInstanceOfSatisfying(ClientRelevantException::class.java) { exception -> assertThat(exception).hasNoCause() assertThat(exception.stackTrace).isEmpty() assertThat(exception.message).isEqualTo(clientRelevantMessage) } } + + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { + val devModeNode = startNode(params, BOB_NAME).getOrThrow() + val node = startNode(ALICE_NAME, devMode = false, parameters = params).getOrThrow() + + assertThatThrownExceptionIsReceivedUnwrapped(devModeNode) + assertThatThrownExceptionIsReceivedUnwrapped(node) + } } @Test - fun `FlowException is received by the RPC client`() { + fun `FlowException is received by the RPC client only if in devMode`() { + val params = NodeParameters(rpcUsers = users) + val expectedMessage = "Flow error!" + val expectedErrorId = 123L + + fun NodeHandle.throwExceptionFromFlow() { + rpc.startFlow(::FlowExceptionFlow, expectedMessage, expectedErrorId).returnValue.getOrThrow() + } + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { - val node = startNode(NodeParameters(rpcUsers = users)).getOrThrow() - val exceptionMessage = "Flow error!" - assertThatThrownBy { node.rpc.startFlow(::FlowExceptionFlow, exceptionMessage).returnValue.getOrThrow() } - .isInstanceOfSatisfying(FlowException::class.java) { exception -> - assertThat(exception).hasNoCause() - assertThat(exception.stackTrace).isEmpty() - assertThat(exception.message).isEqualTo(exceptionMessage) - } + val devModeNode = startNode(params, BOB_NAME).getOrThrow() + val node = startNode(ALICE_NAME, devMode = false, parameters = params).getOrThrow() + + assertThatThrownBy { devModeNode.throwExceptionFromFlow() }.isInstanceOfSatisfying(FlowException::class.java) { exception -> + + assertThat(exception).hasNoCause() + assertThat(exception.stackTrace).isEmpty() + assertThat(exception.message).isEqualTo(expectedMessage) + assertThat(exception.errorId).isEqualTo(expectedErrorId) + } + assertThatThrownBy { node.throwExceptionFromFlow() }.isInstanceOfSatisfying(InternalNodeException::class.java) { exception -> + + assertThat(exception).hasNoCause() + assertThat(exception.stackTrace).isEmpty() + assertThat(exception.message).isEqualTo(InternalNodeException.message) + assertThat(exception.errorId).isEqualTo(expectedErrorId) + } } } @Test fun `rpc client handles exceptions thrown on counter-party side`() { + val params = NodeParameters(rpcUsers = users) + + fun DriverDSL.scenario(devMode: Boolean) { + + val nodeA = startNode(ALICE_NAME, devMode, params).getOrThrow() + val nodeB = startNode(BOB_NAME, devMode, params).getOrThrow() + + nodeA.rpc.startFlow(::InitFlow, nodeB.nodeInfo.singleIdentity()).returnValue.getOrThrow() + } + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { - val nodeA = startNode(NodeParameters(providedName = ALICE_NAME, rpcUsers = users)).getOrThrow() - val nodeB = startNode(NodeParameters(providedName = BOB_NAME, rpcUsers = users)).getOrThrow() - - assertThatThrownBy { nodeA.rpc.startFlow(::InitFlow, nodeB.nodeInfo.singleIdentity()).returnValue.getOrThrow() }.isInstanceOfSatisfying(CordaRuntimeException::class.java) { exception -> + assertThatThrownBy { scenario(true) }.isInstanceOfSatisfying(CordaRuntimeException::class.java) { exception -> assertThat(exception).hasNoCause() assertThat(exception.stackTrace).isEmpty() } } - } -} + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { + assertThatThrownBy { scenario(false) }.isInstanceOfSatisfying(InternalNodeException::class.java) { exception -> -@StartableByRPC -class Flow : FlowLogic<String>() { - @Suspendable - override fun call(): String { - throw GenericJDBCException("Something went wrong!", SQLException("Oops!")) + assertThat(exception).hasNoCause() + assertThat(exception.stackTrace).isEmpty() + assertThat(exception.message).isEqualTo(InternalNodeException.message) + } + } } } @@ -121,7 +141,11 @@ class ClientRelevantErrorFlow(private val message: String) : FlowLogic<String>() } @StartableByRPC -class FlowExceptionFlow(private val message: String) : FlowLogic<String>() { +class FlowExceptionFlow(private val message: String, private val errorId: Long? = null) : FlowLogic<String>() { @Suspendable - override fun call(): String = throw FlowException(message) + override fun call(): String { + val exception = FlowException(message) + errorId?.let { exception.originalErrorId = it } + throw exception + } } diff --git a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/HardRestartTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/HardRestartTest.kt index b0ddaacbf9..1ef0c156a2 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/HardRestartTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/HardRestartTest.kt @@ -28,6 +28,10 @@ import kotlin.concurrent.thread import kotlin.test.assertEquals class HardRestartTest { + companion object { + val logConfigFile = ProjectStructure.projectRootDir / "config" / "dev" / "log4j2.xml" + } + @StartableByRPC @InitiatingFlow class Ping(private val pongParty: Party, val times: Int) : FlowLogic<Unit>() { @@ -54,10 +58,6 @@ class HardRestartTest { } } - companion object { - val logConfigFile = ProjectStructure.projectRootDir / "config" / "dev" / "log4j2.xml" - } - @Test fun restartShortPingPongFlowRandomly() { val demoUser = User("demo", "demo", setOf(Permissions.startFlow<Ping>(), Permissions.all())) @@ -257,4 +257,4 @@ class HardRestartTest { } } } -} \ No newline at end of file +} diff --git a/node/src/integration-test/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLogTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLogTests.kt index 3edb3d3f3e..38389ef595 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLogTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLogTests.kt @@ -16,6 +16,7 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow import net.corda.node.internal.configureDatabase import net.corda.node.services.schema.NodeSchemaService +import net.corda.node.utilities.TestingNamedCacheFactory import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.ALICE_NAME @@ -155,7 +156,7 @@ class RaftTransactionCommitLogTests { val address = Address(myAddress.host, myAddress.port) val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), { null }, { null }, NodeSchemaService(includeNotarySchemas = true)) databases.add(database) - val stateMachineFactory = { RaftTransactionCommitLog(database, Clock.systemUTC(), RaftUniquenessProvider.Companion::createMap) } + val stateMachineFactory = { RaftTransactionCommitLog(database, Clock.systemUTC(), { RaftUniquenessProvider.createMap(TestingNamedCacheFactory()) }) } val server = CopycatServer.builder(address) .withStateMachine(stateMachineFactory) diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/AdditionP2PAddressModeTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/AdditionP2PAddressModeTest.kt new file mode 100644 index 0000000000..e4087fe3f7 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/AdditionP2PAddressModeTest.kt @@ -0,0 +1,70 @@ +package net.corda.services.messaging + +import com.typesafe.config.ConfigValueFactory +import junit.framework.TestCase.assertEquals +import net.corda.client.rpc.CordaRPCClient +import net.corda.core.contracts.Amount +import net.corda.core.contracts.Issued +import net.corda.core.contracts.withoutIssuer +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow +import net.corda.finance.DOLLARS +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.flows.CashIssueAndPaymentFlow +import net.corda.node.services.Permissions.Companion.all +import net.corda.testing.core.DUMMY_BANK_A_NAME +import net.corda.testing.core.DUMMY_BANK_B_NAME +import net.corda.testing.core.expect +import net.corda.testing.core.expectEvents +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.PortAllocation +import net.corda.testing.driver.driver +import net.corda.testing.node.User +import org.junit.Test +import java.util.* + +class AdditionP2PAddressModeTest { + private val portAllocation = PortAllocation.Incremental(27182) + @Test + fun `runs nodes with one configured to use additionalP2PAddresses`() { + val testUser = User("test", "test", setOf(all())) + driver(DriverParameters(startNodesInProcess = true, inMemoryDB = true, extraCordappPackagesToScan = listOf("net.corda.finance"))) { + val mainAddress = portAllocation.nextHostAndPort().toString() + val altAddress = portAllocation.nextHostAndPort().toString() + val haConfig = mutableMapOf<String, Any?>() + haConfig["detectPublicIp"] = false + haConfig["p2pAddress"] = mainAddress //advertise this as primary + haConfig["messagingServerAddress"] = altAddress // but actually host on the alternate address + haConfig["messagingServerExternal"] = false + haConfig["additionalP2PAddresses"] = ConfigValueFactory.fromIterable(listOf(altAddress)) // advertise this secondary address + + val (nodeA, nodeB) = listOf( + startNode(providedName = DUMMY_BANK_A_NAME, rpcUsers = listOf(testUser), customOverrides = haConfig), + startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = listOf(testUser), customOverrides = mapOf("p2pAddress" to portAllocation.nextHostAndPort().toString())) + ).map { it.getOrThrow() } + val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map { + val client = CordaRPCClient(it.rpcAddress) + client.start(testUser.username, testUser.password).proxy + } + + val nodeBVaultUpdates = nodeBRpc.vaultTrack(Cash.State::class.java).updates + + val issueRef = OpaqueBytes.of(0.toByte()) + nodeARpc.startFlowDynamic( + CashIssueAndPaymentFlow::class.java, + DOLLARS(1234), + issueRef, + nodeB.nodeInfo.legalIdentities.get(0), + true, + defaultNotaryIdentity + ).returnValue.getOrThrow() + nodeBVaultUpdates.expectEvents { + expect { update -> + println("Bob got vault update of $update") + val amount: Amount<Issued<Currency>> = update.produced.first().state.data.amount + assertEquals(1234.DOLLARS, amount.withoutIssuer()) + } + } + } + } +} diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt index feda5b405f..7fb2986bc4 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt @@ -2,7 +2,6 @@ package net.corda.services.messaging import net.corda.core.crypto.Crypto import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.copyTo import net.corda.core.internal.createDirectories import net.corda.core.internal.exists import net.corda.core.internal.toX500Name @@ -11,9 +10,10 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_P2P_U import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER import net.corda.nodeapi.internal.DEV_INTERMEDIATE_CA import net.corda.nodeapi.internal.DEV_ROOT_CA -import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.loadDevCaTrustStore +import net.corda.testing.internal.stubs.CertificateStoreStubs import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration import org.apache.activemq.artemis.api.core.ActiveMQClusterSecurityException import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException @@ -84,57 +84,35 @@ class MQSecurityAsNodeTest : P2PMQSecurityTest() { @Test fun `login with invalid certificate chain`() { - val sslConfig = object : SSLConfiguration { - override val certificatesDirectory = Files.createTempDirectory("certs") - override val keyStorePassword: String get() = "cordacadevpass" - override val trustStorePassword: String get() = "trustpass" - override val crlCheckSoftFail: Boolean = true + val certsDir = Files.createTempDirectory("certs") + certsDir.createDirectories() + val signingCertStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certsDir) + val p2pSslConfig = CertificateStoreStubs.P2P.withCertificatesDirectory(certsDir) - init { - val legalName = CordaX500Name("MegaCorp", "London", "GB") - certificatesDirectory.createDirectories() - if (!trustStoreFile.exists()) { - javaClass.classLoader.getResourceAsStream("certificates/cordatruststore.jks").use { it.copyTo(trustStoreFile) } - } - - val clientKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - // Set name constrain to the legal name. - val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.toX500Name()))), arrayOf()) - val clientCACert = X509Utilities.createCertificate( - CertificateType.INTERMEDIATE_CA, - DEV_INTERMEDIATE_CA.certificate, - DEV_INTERMEDIATE_CA.keyPair, - legalName.x500Principal, - clientKeyPair.public, - nameConstraints = nameConstraints) - - val tlsKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - // Using different x500 name in the TLS cert which is not allowed in the name constraints. - val clientTLSCert = X509Utilities.createCertificate( - CertificateType.TLS, - clientCACert, - clientKeyPair, - CordaX500Name("MiniCorp", "London", "GB").x500Principal, - tlsKeyPair.public) - - loadNodeKeyStore(createNew = true).update { - setPrivateKey( - X509Utilities.CORDA_CLIENT_CA, - clientKeyPair.private, - listOf(clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate)) - } - - loadSslKeyStore(createNew = true).update { - setPrivateKey( - X509Utilities.CORDA_CLIENT_TLS, - tlsKeyPair.private, - listOf(clientTLSCert, clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate)) - } - } + val legalName = CordaX500Name("MegaCorp", "London", "GB") + if (!p2pSslConfig.trustStore.path.exists()) { + val trustStore = p2pSslConfig.trustStore.get(true) + loadDevCaTrustStore().copyTo(trustStore) } - val attacker = clientTo(alice.node.configuration.p2pAddress, sslConfig) + val clientKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + // Set name constrain to the legal name. + val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.toX500Name()))), arrayOf()) + val clientCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, DEV_INTERMEDIATE_CA.certificate, DEV_INTERMEDIATE_CA.keyPair, legalName.x500Principal, clientKeyPair.public, nameConstraints = nameConstraints) + val tlsKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + // Using different x500 name in the TLS cert which is not allowed in the name constraints. + val clientTLSCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientKeyPair, CordaX500Name("MiniCorp", "London", "GB").x500Principal, tlsKeyPair.public) + + signingCertStore.get(createNew = true).update { + setPrivateKey(X509Utilities.CORDA_CLIENT_CA, clientKeyPair.private, listOf(clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate)) + } + + p2pSslConfig.keyStore.get(createNew = true).update { + setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, tlsKeyPair.private, listOf(clientTLSCert, clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate)) + } + + val attacker = clientTo(alice.node.configuration.p2pAddress, p2pSslConfig) assertThatExceptionOfType(ActiveMQNotConnectedException::class.java).isThrownBy { attacker.start(PEER_USER, PEER_USER) } diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt index 0214cd0f48..2f52ad269d 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt @@ -18,12 +18,12 @@ import net.corda.node.internal.NodeWithInfo import net.corda.nodeapi.RPCApi import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME -import net.corda.testing.node.User import net.corda.testing.core.singleIdentity import net.corda.testing.internal.configureTestSSL +import net.corda.testing.node.User import net.corda.testing.node.internal.NodeBasedTest import net.corda.testing.node.internal.startFlow import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException @@ -94,7 +94,7 @@ abstract class MQSecurityTest : NodeBasedTest() { assertAllQueueCreationAttacksFail(randomQueue) } - fun clientTo(target: NetworkHostAndPort, sslConfiguration: SSLConfiguration? = configureTestSSL(CordaX500Name("MegaCorp", "London", "GB"))): SimpleMQClient { + fun clientTo(target: NetworkHostAndPort, sslConfiguration: MutualSslConfiguration? = configureTestSSL(CordaX500Name("MegaCorp", "London", "GB"))): SimpleMQClient { val client = SimpleMQClient(target, sslConfiguration) clients += client return client diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt index f04c856f73..004f27a026 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt @@ -38,7 +38,10 @@ class P2PMessagingTest { } private fun startDriverWithDistributedService(dsl: DriverDSL.(List<InProcess>) -> Unit) { - driver(DriverParameters(startNodesInProcess = true, notarySpecs = listOf(NotarySpec(DISTRIBUTED_SERVICE_NAME, cluster = ClusterSpec.Raft(clusterSize = 2))))) { + driver(DriverParameters( + startNodesInProcess = true, + notarySpecs = listOf(NotarySpec(DISTRIBUTED_SERVICE_NAME, cluster = ClusterSpec.Raft(clusterSize = 2))) + )) { dsl(defaultNotaryHandle.nodeHandles.getOrThrow().map { (it as InProcess) }) } } diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/SimpleMQClient.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/SimpleMQClient.kt index 58e04948c4..44e59852a9 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/SimpleMQClient.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/SimpleMQClient.kt @@ -3,8 +3,8 @@ package net.corda.services.messaging import net.corda.core.identity.CordaX500Name import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort -import net.corda.nodeapi.ArtemisTcpTransport -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.p2pConnectorTcpTransport +import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.testing.internal.configureTestSSL import org.apache.activemq.artemis.api.core.client.* @@ -12,7 +12,7 @@ import org.apache.activemq.artemis.api.core.client.* * As the name suggests this is a simple client for connecting to MQ brokers. */ class SimpleMQClient(val target: NetworkHostAndPort, - private val config: SSLConfiguration? = configureTestSSL(DEFAULT_MQ_LEGAL_NAME)) { + private val config: MutualSslConfiguration? = configureTestSSL(DEFAULT_MQ_LEGAL_NAME)) { companion object { val DEFAULT_MQ_LEGAL_NAME = CordaX500Name(organisation = "SimpleMQClient", locality = "London", country = "GB") } @@ -22,7 +22,7 @@ class SimpleMQClient(val target: NetworkHostAndPort, lateinit var producer: ClientProducer fun start(username: String? = null, password: String? = null, enableSSL: Boolean = true) { - val tcpTransport = ArtemisTcpTransport.p2pConnectorTcpTransport(target, config, enableSSL = enableSSL) + val tcpTransport = p2pConnectorTcpTransport(target, config, enableSSL = enableSSL) val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply { isBlockOnNonDurableSend = true threadPoolMaxSize = 1 diff --git a/node/src/main/java/CordaCaplet.java b/node/src/main/java/CordaCaplet.java index 4e4e073e95..fb4163741a 100644 --- a/node/src/main/java/CordaCaplet.java +++ b/node/src/main/java/CordaCaplet.java @@ -21,16 +21,8 @@ public class CordaCaplet extends Capsule { private Config parseConfigFile(List<String> args) { String baseDirOption = getOption(args, "--base-directory"); - // Ensure consistent behaviour with NodeArgsParser.kt, see CORDA-1598. - if (null == baseDirOption || baseDirOption.isEmpty()) { - baseDirOption = getOption(args, "-base-directory"); - } this.baseDir = Paths.get((baseDirOption == null) ? "." : baseDirOption).toAbsolutePath().normalize().toString(); String config = getOption(args, "--config-file"); - // Same as for baseDirOption. - if (null == config || config.isEmpty()) { - config = getOption(args, "-config-file"); - } File configFile = (config == null) ? new File(baseDir, "node.conf") : new File(config); try { ConfigParseOptions parseOptions = ConfigParseOptions.defaults().setAllowMissing(false); diff --git a/node/src/main/kotlin/net/corda/node/Corda.kt b/node/src/main/kotlin/net/corda/node/Corda.kt index 5889aacdc7..62252de5a5 100644 --- a/node/src/main/kotlin/net/corda/node/Corda.kt +++ b/node/src/main/kotlin/net/corda/node/Corda.kt @@ -3,12 +3,12 @@ package net.corda.node +import net.corda.cliutils.start import net.corda.node.internal.NodeStartup -import kotlin.system.exitProcess fun main(args: Array<String>) { // Pass the arguments to the Node factory. In the Enterprise edition, this line is modified to point to a subclass. // It will exit the process in case of startup failure and is not intended to be used by embedders. If you want // to embed Node in your own container, instantiate it directly and set up the configuration objects yourself. - exitProcess(if (NodeStartup(args).run()) 0 else 1) -} \ No newline at end of file + NodeStartup().start(args) +} diff --git a/node/src/main/kotlin/net/corda/node/NodeArgsParser.kt b/node/src/main/kotlin/net/corda/node/NodeArgsParser.kt deleted file mode 100644 index eb6e35ad12..0000000000 --- a/node/src/main/kotlin/net/corda/node/NodeArgsParser.kt +++ /dev/null @@ -1,142 +0,0 @@ -package net.corda.node - -import com.typesafe.config.Config -import com.typesafe.config.ConfigFactory -import joptsimple.OptionSet -import joptsimple.util.EnumConverter -import joptsimple.util.PathConverter -import net.corda.core.internal.div -import net.corda.core.internal.exists -import net.corda.core.utilities.Try -import net.corda.node.services.config.ConfigHelper -import net.corda.node.services.config.NodeConfiguration -import net.corda.node.services.config.parseAsNodeConfiguration -import net.corda.node.utilities.AbstractArgsParser -import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy -import org.slf4j.event.Level -import java.nio.file.Path -import java.nio.file.Paths - -// NOTE: Do not use any logger in this class as args parsing is done before the logger is setup. -class NodeArgsParser : AbstractArgsParser<CmdLineOptions>() { - // The intent of allowing a command line configurable directory and config path is to allow deployment flexibility. - // Other general configuration should live inside the config file unless we regularly need temporary overrides on the command line - private val baseDirectoryArg = optionParser - .accepts("base-directory", "The node working directory where all the files are kept") - .withRequiredArg() - .withValuesConvertedBy(PathConverter()) - .defaultsTo(Paths.get(".")) - private val configFileArg = optionParser - .accepts("config-file", "The path to the config file") - .withRequiredArg() - .defaultsTo("node.conf") - private val loggerLevel = optionParser - .accepts("logging-level", "Enable logging at this level and higher") - .withRequiredArg() - .withValuesConvertedBy(object : EnumConverter<Level>(Level::class.java) {}) - .defaultsTo(Level.INFO) - private val logToConsoleArg = optionParser.accepts("log-to-console", "If set, prints logging to the console as well as to a file.") - private val sshdServerArg = optionParser.accepts("sshd", "Enables SSHD server for node administration.") - private val noLocalShellArg = optionParser.accepts("no-local-shell", "Do not start the embedded shell locally.") - private val isRegistrationArg = optionParser.accepts("initial-registration", "Start initial node registration with Corda network to obtain certificate from the permissioning server.") - private val networkRootTrustStorePathArg = optionParser.accepts("network-root-truststore", "Network root trust store obtained from network operator.") - .withRequiredArg() - .withValuesConvertedBy(PathConverter()) - .defaultsTo((Paths.get("certificates") / "network-root-truststore.jks")) - private val networkRootTrustStorePasswordArg = optionParser.accepts("network-root-truststore-password", "Network root trust store password obtained from network operator.") - .withRequiredArg() - private val unknownConfigKeysPolicy = optionParser.accepts("on-unknown-config-keys", "How to behave on unknown node configuration.") - .withRequiredArg() - .withValuesConvertedBy(object : EnumConverter<UnknownConfigKeysPolicy>(UnknownConfigKeysPolicy::class.java) {}) - .defaultsTo(UnknownConfigKeysPolicy.FAIL) - private val devModeArg = optionParser.accepts("dev-mode", "Run the node in developer mode. Unsafe for production.") - - private val isVersionArg = optionParser.accepts("version", "Print the version and exit") - private val justGenerateNodeInfoArg = optionParser.accepts("just-generate-node-info", - "Perform the node start-up task necessary to generate its nodeInfo, save it to disk, then quit") - private val justGenerateRpcSslCertsArg = optionParser.accepts("just-generate-rpc-ssl-settings", - "Generate the ssl keystore and truststore for a secure RPC connection.") - private val bootstrapRaftClusterArg = optionParser.accepts("bootstrap-raft-cluster", "Bootstraps Raft cluster. The node forms a single node cluster (ignoring otherwise configured peer addresses), acting as a seed for other nodes to join the cluster.") - private val clearNetworkMapCache = optionParser.accepts("clear-network-map-cache", "Clears local copy of network map, on node startup it will be restored from server or file system.") - - override fun doParse(optionSet: OptionSet): CmdLineOptions { - require(optionSet.nonOptionArguments().isEmpty()) { "Unrecognized argument(s): ${optionSet.nonOptionArguments().joinToString(separator = ", ")}"} - - val baseDirectory = optionSet.valueOf(baseDirectoryArg).normalize().toAbsolutePath() - val configFilePath = Paths.get(optionSet.valueOf(configFileArg)) - val configFile = if (configFilePath.isAbsolute) configFilePath else baseDirectory / configFilePath.toString() - val loggingLevel = optionSet.valueOf(loggerLevel) - val logToConsole = optionSet.has(logToConsoleArg) - val isRegistration = optionSet.has(isRegistrationArg) - val isVersion = optionSet.has(isVersionArg) - val noLocalShell = optionSet.has(noLocalShellArg) - val sshdServer = optionSet.has(sshdServerArg) - val justGenerateNodeInfo = optionSet.has(justGenerateNodeInfoArg) - val justGenerateRpcSslCerts = optionSet.has(justGenerateRpcSslCertsArg) - val bootstrapRaftCluster = optionSet.has(bootstrapRaftClusterArg) - val networkRootTrustStorePath = optionSet.valueOf(networkRootTrustStorePathArg) - val networkRootTrustStorePassword = optionSet.valueOf(networkRootTrustStorePasswordArg) - val unknownConfigKeysPolicy = optionSet.valueOf(unknownConfigKeysPolicy) - val devMode = optionSet.has(devModeArg) - val clearNetworkMapCache = optionSet.has(clearNetworkMapCache) - - val registrationConfig = if (isRegistration) { - requireNotNull(networkRootTrustStorePassword) { "Network root trust store password must be provided in registration mode using --network-root-truststore-password." } - require(networkRootTrustStorePath.exists()) { "Network root trust store path: '$networkRootTrustStorePath' doesn't exist" } - NodeRegistrationOption(networkRootTrustStorePath, networkRootTrustStorePassword) - } else { - null - } - - return CmdLineOptions(baseDirectory, - configFile, - loggingLevel, - logToConsole, - registrationConfig, - isVersion, - noLocalShell, - sshdServer, - justGenerateNodeInfo, - justGenerateRpcSslCerts, - bootstrapRaftCluster, - unknownConfigKeysPolicy, - devMode, - clearNetworkMapCache) - } -} - -data class NodeRegistrationOption(val networkRootTrustStorePath: Path, val networkRootTrustStorePassword: String) - -data class CmdLineOptions(val baseDirectory: Path, - val configFile: Path, - val loggingLevel: Level, - val logToConsole: Boolean, - val nodeRegistrationOption: NodeRegistrationOption?, - val isVersion: Boolean, - val noLocalShell: Boolean, - val sshdServer: Boolean, - val justGenerateNodeInfo: Boolean, - val justGenerateRpcSslCerts: Boolean, - val bootstrapRaftCluster: Boolean, - val unknownConfigKeysPolicy: UnknownConfigKeysPolicy, - val devMode: Boolean, - val clearNetworkMapCache: Boolean) { - fun loadConfig(): Pair<Config, Try<NodeConfiguration>> { - val rawConfig = ConfigHelper.loadConfig( - baseDirectory, - configFile, - configOverrides = ConfigFactory.parseMap(mapOf("noLocalShell" to this.noLocalShell) + - if (devMode) mapOf("devMode" to this.devMode) else emptyMap<String, Any>()) - ) - return rawConfig to Try.on { - rawConfig.parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config -> - if (nodeRegistrationOption != null) { - require(!config.devMode) { "registration cannot occur in devMode" } - require(config.compatibilityZoneURL != null || config.networkServices != null) { - "compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode." - } - } - } - } - } -} diff --git a/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt b/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt new file mode 100644 index 0000000000..6d04567d9d --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt @@ -0,0 +1,134 @@ +package net.corda.node + +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import net.corda.core.internal.div +import net.corda.core.internal.exists +import net.corda.core.utilities.Try +import net.corda.node.services.config.ConfigHelper +import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.parseAsNodeConfiguration +import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy +import picocli.CommandLine.Option +import java.nio.file.Path +import java.nio.file.Paths + +class NodeCmdLineOptions { + @Option( + names = ["-b", "--base-directory"], + description = ["The node working directory where all the files are kept."] + ) + var baseDirectory: Path = Paths.get(".").toAbsolutePath().normalize() + + @Option( + names = ["-f", "--config-file"], + description = ["The path to the config file. By default this is node.conf in the base directory."] + ) + private var _configFile: Path? = null + val configFile: Path get() = _configFile ?: (baseDirectory / "node.conf") + + @Option( + names = ["--sshd"], + description = ["If set, enables SSH server for node administration."] + ) + var sshdServer: Boolean = false + + @Option( + names = ["--sshd-port"], + description = ["The port to start the SSH server on, if enabled."] + ) + var sshdServerPort: Int = 2222 + + @Option( + names = ["-n", "--no-local-shell"], + description = ["Do not start the embedded shell locally."] + ) + var noLocalShell: Boolean = false + + @Option( + names = ["--initial-registration"], + description = ["Start initial node registration with Corda network to obtain certificate from the permissioning server."] + ) + var isRegistration: Boolean = false + + @Option( + names = ["-t", "--network-root-truststore"], + description = ["Network root trust store obtained from network operator."] + ) + private var _networkRootTrustStorePath: Path? = null + val networkRootTrustStorePath: Path get() = _networkRootTrustStorePath ?: baseDirectory / "certificates" / "network-root-truststore.jks" + + @Option( + names = ["-p", "--network-root-truststore-password"], + description = ["Network root trust store password obtained from network operator."] + ) + var networkRootTrustStorePassword: String? = null + + @Option( + names = ["--on-unknown-config-keys"], + description = ["How to behave on unknown node configuration. \${COMPLETION-CANDIDATES}"] + ) + var unknownConfigKeysPolicy: UnknownConfigKeysPolicy = UnknownConfigKeysPolicy.FAIL + + @Option( + names = ["-d", "--dev-mode"], + description = ["Run the node in developer mode. Unsafe for production."] + ) + var devMode: Boolean? = null + + @Option( + names = ["--just-generate-node-info"], + description = ["Perform the node start-up task necessary to generate its node info, save it to disk, then quit"] + ) + var justGenerateNodeInfo: Boolean = false + + @Option( + names = ["--just-generate-rpc-ssl-settings"], + description = ["Generate the SSL key and trust stores for a secure RPC connection."] + ) + var justGenerateRpcSslCerts: Boolean = false + + @Option( + names = ["--bootstrap-raft-cluster"], + description = ["Bootstraps Raft cluster. The node forms a single node cluster (ignoring otherwise configured peer addresses), acting as a seed for other nodes to join the cluster."] + ) + var bootstrapRaftCluster: Boolean = false + + @Option( + names = ["-c", "--clear-network-map-cache"], + description = ["Clears local copy of network map, on node startup it will be restored from server or file system."] + ) + var clearNetworkMapCache: Boolean = false + + val nodeRegistrationOption: NodeRegistrationOption? by lazy { + if (isRegistration) { + requireNotNull(networkRootTrustStorePassword) { "Network root trust store password must be provided in registration mode using --network-root-truststore-password." } + require(networkRootTrustStorePath.exists()) { "Network root trust store path: '$networkRootTrustStorePath' doesn't exist" } + NodeRegistrationOption(networkRootTrustStorePath, networkRootTrustStorePassword!!) + } else { + null + } + } + + fun loadConfig(): Pair<Config, Try<NodeConfiguration>> { + val rawConfig = ConfigHelper.loadConfig( + baseDirectory, + configFile, + configOverrides = ConfigFactory.parseMap(mapOf("noLocalShell" to this.noLocalShell) + + if (sshdServer) mapOf("sshd" to mapOf("port" to sshdServerPort.toString())) else emptyMap<String, Any>() + + if (devMode != null) mapOf("devMode" to this.devMode) else emptyMap()) + ) + return rawConfig to Try.on { + rawConfig.parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config -> + if (nodeRegistrationOption != null) { + require(!config.devMode) { "Registration cannot occur in devMode" } + require(config.compatibilityZoneURL != null || config.networkServices != null) { + "compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode." + } + } + } + } + } +} + +data class NodeRegistrationOption(val networkRootTrustStorePath: Path, val networkRootTrustStorePassword: String) diff --git a/node/src/main/kotlin/net/corda/node/VersionInfo.kt b/node/src/main/kotlin/net/corda/node/VersionInfo.kt index c51df32229..b3d0ec0fbb 100644 --- a/node/src/main/kotlin/net/corda/node/VersionInfo.kt +++ b/node/src/main/kotlin/net/corda/node/VersionInfo.kt @@ -1,5 +1,7 @@ package net.corda.node +import net.corda.nodeapi.internal.PLATFORM_VERSION + /** * Encapsulates various pieces of version information of the node. */ @@ -14,4 +16,9 @@ data class VersionInfo( /** The exact version control commit ID of the node build. */ val revision: String, /** The node vendor */ - val vendor: String) \ No newline at end of file + val vendor: String) { + + companion object { + val UNKNOWN = VersionInfo(PLATFORM_VERSION, "Unknown", "Unknown", "Unknown") + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/cordapp/CordappLoader.kt b/node/src/main/kotlin/net/corda/node/cordapp/CordappLoader.kt index 8f472c8922..8a8feffc2d 100644 --- a/node/src/main/kotlin/net/corda/node/cordapp/CordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/cordapp/CordappLoader.kt @@ -2,6 +2,7 @@ package net.corda.node.cordapp import net.corda.core.cordapp.Cordapp import net.corda.core.flows.FlowLogic +import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.schemas.MappedSchema /** @@ -12,7 +13,7 @@ interface CordappLoader { /** * Returns all [Cordapp]s found. */ - val cordapps: List<Cordapp> + val cordapps: List<CordappImpl> /** * Returns a [ClassLoader] containing all types from all [Cordapp]s. diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 64e5fb7cfc..2391cf7fca 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -38,12 +38,14 @@ import net.corda.node.internal.cordapp.CordappConfigFileProvider import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.node.internal.cordapp.CordappProviderInternal import net.corda.node.internal.rpc.proxies.AuthenticatedRpcOpsProxy +import net.corda.node.internal.rpc.proxies.ExceptionMaskingRpcOpsProxy import net.corda.node.internal.rpc.proxies.ExceptionSerialisingRpcOpsProxy import net.corda.node.services.ContractUpgradeHandler import net.corda.node.services.FinalityHandler import net.corda.node.services.NotaryChangeHandler import net.corda.node.services.api.* import net.corda.node.services.config.* +import net.corda.node.services.config.rpc.NodeRpcOptions import net.corda.node.services.config.shell.toShellConfig import net.corda.node.services.events.NodeSchedulerService import net.corda.node.services.events.ScheduledActivityObserver @@ -52,21 +54,26 @@ import net.corda.node.services.keys.KeyManagementServiceInternal import net.corda.node.services.keys.PersistentKeyManagementService import net.corda.node.services.messaging.DeduplicationHandler import net.corda.node.services.messaging.MessagingService -import net.corda.node.services.network.* +import net.corda.node.services.network.NetworkMapClient +import net.corda.node.services.network.NetworkMapUpdater +import net.corda.node.services.network.NodeInfoWatcher +import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.persistence.* import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.statemachine.* import net.corda.node.services.transactions.* import net.corda.node.services.upgrade.ContractUpgradeServiceImpl import net.corda.node.services.vault.NodeVaultService -import net.corda.node.utilities.AffinityExecutor -import net.corda.node.utilities.JVMAgentRegistry -import net.corda.node.utilities.NamedThreadFactory -import net.corda.node.utilities.NodeBuildProperties -import net.corda.nodeapi.internal.DevIdentityGenerator +import net.corda.node.utilities.* import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.SignedNodeInfo +import net.corda.nodeapi.internal.config.CertificateStore import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA +import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS +import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA +import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_ALIAS_PREFIX +import net.corda.nodeapi.internal.crypto.X509Utilities.NODE_IDENTITY_ALIAS_PREFIX import net.corda.nodeapi.internal.persistence.* import net.corda.nodeapi.internal.storeLegalIdentity import net.corda.tools.shell.InteractiveShell @@ -106,6 +113,7 @@ import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair // TODO Log warning if this node is a notary but not one of the ones specified in the network parameters, both for core and custom abstract class AbstractNode<S>(val configuration: NodeConfiguration, val platformClock: CordaClock, + cacheFactoryPrototype: NamedCacheFactory, protected val versionInfo: VersionInfo, protected val cordappLoader: CordappLoader, protected val serverThread: AffinityExecutor.ServiceAffinityExecutor, @@ -115,6 +123,11 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration, @Suppress("LeakingThis") private var tokenizableServices: MutableList<Any>? = mutableListOf(platformClock, this) + + protected val metricRegistry = MetricRegistry() + protected val cacheFactory = cacheFactoryPrototype.bindWithConfig(configuration).bindWithMetrics(metricRegistry).tokenize() + val monitoringService = MonitoringService(metricRegistry).tokenize() + protected val runOnStop = ArrayList<() -> Any?>() init { @@ -129,7 +142,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration, } val schemaService = NodeSchemaService(cordappLoader.cordappSchemas, configuration.notary != null).tokenize() - val identityService = PersistentIdentityService().tokenize() + val identityService = PersistentIdentityService(cacheFactory).tokenize() val database: CordaPersistence = createCordaPersistence( configuration.database, identityService::wellKnownPartyFromX500Name, @@ -141,14 +154,13 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration, // TODO Break cyclic dependency identityService.database = database } - private val persistentNetworkMapCache = PersistentNetworkMapCache(database, configuration.myLegalName) - val networkMapCache = NetworkMapCacheImpl(persistentNetworkMapCache, identityService, database).tokenize() + + val networkMapCache = PersistentNetworkMapCache(cacheFactory, database, identityService).tokenize() val checkpointStorage = DBCheckpointStorage() @Suppress("LeakingThis") val transactionStorage = makeTransactionStorage(configuration.transactionCacheSizeBytes).tokenize() - val networkMapClient: NetworkMapClient? = configuration.networkServices?.let { NetworkMapClient(it.networkMapURL) } - private val metricRegistry = MetricRegistry() - val attachments = NodeAttachmentService(metricRegistry, database, configuration.attachmentContentCacheSizeBytes, configuration.attachmentCacheBound).tokenize() + val networkMapClient: NetworkMapClient? = configuration.networkServices?.let { NetworkMapClient(it.networkMapURL, versionInfo) } + val attachments = NodeAttachmentService(metricRegistry, cacheFactory, database).tokenize() val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments).tokenize() @Suppress("LeakingThis") val keyManagementService = makeKeyManagementService(identityService).tokenize() @@ -157,7 +169,6 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration, val vaultService = makeVaultService(keyManagementService, servicesForResolution, database).tokenize() val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database) val flowLogicRefFactory = FlowLogicRefFactoryImpl(cordappLoader.appClassLoader) - val monitoringService = MonitoringService(metricRegistry).tokenize() val networkMapUpdater = NetworkMapUpdater( networkMapCache, NodeInfoWatcher( @@ -204,7 +215,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration, /** * Completes once the node has successfully registered with the network map service - * or has loaded network map data from local database + * or has loaded network map data from local database. */ val nodeReadyFuture: CordaFuture<Unit> get() = networkMapCache.nodeReady.map { Unit } @@ -213,7 +224,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration, } /** Set to non-null once [start] has been successfully called. */ - open val started get() = _started + open val started: S? get() = _started @Volatile private var _started: S? = null @@ -229,30 +240,34 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration, /** The implementation of the [CordaRPCOps] interface used by this node. */ open fun makeRPCOps(): CordaRPCOps { - val ops: CordaRPCOps = CordaRPCOpsImpl(services, smm, flowStarter) { shutdownExecutor.submit { stop() } } + val ops: CordaRPCOps = CordaRPCOpsImpl(services, smm, flowStarter) { shutdownExecutor.submit { stop() } }.also { it.closeOnStop() } + val proxies = mutableListOf<(CordaRPCOps) -> CordaRPCOps>() // Mind that order is relevant here. - val proxies = listOf<(CordaRPCOps) -> CordaRPCOps>(::AuthenticatedRpcOpsProxy, { ExceptionSerialisingRpcOpsProxy(it, true) }) + proxies += ::AuthenticatedRpcOpsProxy + if (!configuration.devMode) { + proxies += { it -> ExceptionMaskingRpcOpsProxy(it, true) } + } + proxies += { it -> ExceptionSerialisingRpcOpsProxy(it, configuration.devMode) } return proxies.fold(ops) { delegate, decorate -> decorate(delegate) } } - private fun initKeyStore(): X509Certificate { + private fun initKeyStores(): X509Certificate { if (configuration.devMode) { configuration.configureWithDevSSLCertificate() } - return validateKeyStore() + return validateKeyStores() } open fun generateAndSaveNodeInfo(): NodeInfo { check(started == null) { "Node has already been started" } log.info("Generating nodeInfo ...") - val trustRoot = initKeyStore() + val trustRoot = initKeyStores() val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null) startDatabase() - val nodeCa = configuration.loadNodeKeyStore().getCertificate(X509Utilities.CORDA_CLIENT_CA) + val nodeCa = configuration.signingCertificateStore.get()[CORDA_CLIENT_CA] identityService.start(trustRoot, listOf(identity.certificate, nodeCa)) return database.use { it.transaction { - persistentNetworkMapCache.start(notaries = emptyList()) val (_, nodeInfoAndSigned) = updateNodeInfo(identity, identityKeyPair, publish = false) nodeInfoAndSigned.nodeInfo } @@ -264,8 +279,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration, log.info("Starting clearing of network map cache entries...") startDatabase() database.use { - persistentNetworkMapCache.start(notaries = emptyList()) - persistentNetworkMapCache.clearNetworkMapCache() + networkMapCache.clearNetworkMapCache() } } @@ -277,8 +291,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration, } log.info("Node starting up ...") - val trustRoot = initKeyStore() - val nodeCa = configuration.loadNodeKeyStore().getCertificate(X509Utilities.CORDA_CLIENT_CA) + val trustRoot = initKeyStores() + val nodeCa = configuration.signingCertificateStore.get()[CORDA_CLIENT_CA] initialiseJVMAgents() schemaService.mappedSchemasWarnings().forEach { @@ -300,14 +314,13 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration, "Node's platform version is lower than network's required minimumPlatformVersion" } servicesForResolution.start(netParams) + networkMapCache.start(netParams.notaries) startDatabase() val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null) identityService.start(trustRoot, listOf(identity.certificate, nodeCa)) val (keyPairs, nodeInfoAndSigned, myNotaryIdentity) = database.transaction { - persistentNetworkMapCache.start(netParams.notaries) - networkMapCache.start() updateNodeInfo(identity, identityKeyPair, publish = true) } @@ -350,7 +363,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration, } } - /** Subclasses must override this to create a [StartedNode] of the desired type, using the provided machinery. */ + /** Subclasses must override this to create a "started" node of the desired type, using the provided machinery. */ abstract fun createStartedNode(nodeInfo: NodeInfo, rpcOps: CordaRPCOps, notaryService: NotaryService?): S private fun verifyCheckpointsCompatible(tokenizableServices: List<Any>) { @@ -368,10 +381,10 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration, open fun startShell() { if (configuration.shouldInitCrashShell()) { val shellConfiguration = configuration.toShellConfig() - shellConfiguration.sshHostKeyDirectory?.let { + shellConfiguration.sshdPort?.let { log.info("Binding Shell SSHD server on port $it.") } - InteractiveShell.startShellInternal(shellConfiguration, cordappLoader.appClassLoader) + InteractiveShell.startShell(shellConfiguration, cordappLoader.appClassLoader) } } @@ -455,7 +468,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration, } else { 1.days } - val executor = Executors.newSingleThreadScheduledExecutor(NamedThreadFactory("Network Map Updater", Executors.defaultThreadFactory())) + val executor = Executors.newSingleThreadScheduledExecutor(NamedThreadFactory("Network Map Updater")) executor.submit(object : Runnable { override fun run() { val republishInterval = try { @@ -516,13 +529,13 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration, } /** - * If the [serviceClass] is a notary service, it will only be enable if the "custom" flag is set in + * If the [serviceClass] is a notary service, it will only be enabled if the "custom" flag is set in * the notary configuration. */ private fun isNotaryService(serviceClass: Class<*>) = NotaryService::class.java.isAssignableFrom(serviceClass) /** - * This customizes the ServiceHub for each CordaService that is initiating flows + * This customizes the ServiceHub for each CordaService that is initiating flows. */ // TODO Move this into its own file private class AppServiceHubImpl<T : SerializeAsToken>(private val serviceHub: ServiceHub, private val flowStarter: FlowStarter) : AppServiceHub, ServiceHub by serviceHub { @@ -563,28 +576,38 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration, private fun <T : SerializeAsToken> installCordaService(flowStarter: FlowStarter, serviceClass: Class<T>, myNotaryIdentity: PartyAndCertificate?) { serviceClass.requireAnnotation<CordaService>() + val service = try { - val serviceContext = AppServiceHubImpl<T>(services, flowStarter) - if (isNotaryService(serviceClass)) { - myNotaryIdentity ?: throw IllegalStateException("Trying to install a notary service but no notary identity specified") - val constructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java, PublicKey::class.java).apply { isAccessible = true } - serviceContext.serviceInstance = constructor.newInstance(serviceContext, myNotaryIdentity.owningKey) - serviceContext.serviceInstance - } else { - try { - val extendedServiceConstructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java).apply { isAccessible = true } - serviceContext.serviceInstance = extendedServiceConstructor.newInstance(serviceContext) - serviceContext.serviceInstance - } catch (ex: NoSuchMethodException) { - val constructor = serviceClass.getDeclaredConstructor(ServiceHub::class.java).apply { isAccessible = true } - log.warn("${serviceClass.name} is using legacy CordaService constructor with ServiceHub parameter. " + - "Upgrade to an AppServiceHub parameter to enable updated API features.") - constructor.newInstance(services) - } - } + if (isNotaryService(serviceClass)) { + myNotaryIdentity ?: throw IllegalStateException("Trying to install a notary service but no notary identity specified") + try { + val constructor = serviceClass.getDeclaredConstructor(ServiceHubInternal::class.java, PublicKey::class.java).apply { isAccessible = true } + constructor.newInstance(services, myNotaryIdentity.owningKey ) + } catch (ex: NoSuchMethodException) { + val constructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java, PublicKey::class.java).apply { isAccessible = true } + val serviceContext = AppServiceHubImpl<T>(services, flowStarter) + val service = constructor.newInstance(serviceContext, myNotaryIdentity.owningKey) + serviceContext.serviceInstance = service + service + } + } else { + try { + val serviceContext = AppServiceHubImpl<T>(services, flowStarter) + val extendedServiceConstructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java).apply { isAccessible = true } + val service = extendedServiceConstructor.newInstance(serviceContext) + serviceContext.serviceInstance = service + service + } catch (ex: NoSuchMethodException) { + val constructor = serviceClass.getDeclaredConstructor(ServiceHub::class.java).apply { isAccessible = true } + log.warn("${serviceClass.name} is using legacy CordaService constructor with ServiceHub parameter. " + + "Upgrade to an AppServiceHub parameter to enable updated API features.") + constructor.newInstance(services) + } + } } catch (e: InvocationTargetException) { throw ServiceInstantiationException(e.cause) } + cordappServices.putInstance(serviceClass, service) if (service is NotaryService) handleCustomNotaryService(service) @@ -687,36 +710,56 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration, } protected open fun makeTransactionStorage(transactionCacheSizeBytes: Long): WritableTransactionStorage { - return DBTransactionStorage(transactionCacheSizeBytes, database) + return DBTransactionStorage(database, cacheFactory) } @VisibleForTesting protected open fun acceptableLiveFiberCountOnStop(): Int = 0 - private fun validateKeyStore(): X509Certificate { - val containCorrectKeys = try { - // This will throw IOException if key file not found or KeyStoreException if keystore password is incorrect. - val sslKeystore = configuration.loadSslKeyStore() - val identitiesKeystore = configuration.loadNodeKeyStore() - X509Utilities.CORDA_CLIENT_TLS in sslKeystore && X509Utilities.CORDA_CLIENT_CA in identitiesKeystore + private fun getCertificateStores(): AllCertificateStores? { + return try { + // The following will throw IOException if key file not found or KeyStoreException if keystore password is incorrect. + val sslKeyStore = configuration.p2pSslOptions.keyStore.get() + val identitiesKeyStore = configuration.signingCertificateStore.get() + val trustStore = configuration.p2pSslOptions.trustStore.get() + AllCertificateStores(trustStore, sslKeyStore, identitiesKeyStore) } catch (e: KeyStoreException) { - log.warn("Certificate key store found but key store password does not match configuration.") - false + log.warn("At least one of the keystores or truststore passwords does not match configuration.") + null } catch (e: IOException) { - log.error("IO exception while trying to validate keystore", e) - false + log.error("IO exception while trying to validate keystores and truststore", e) + null } - require(containCorrectKeys) { - "Identity certificate not found. " + - "Please either copy your existing identity key and certificate from another node, " + + } + + private data class AllCertificateStores(val trustStore: CertificateStore, val sslKeyStore: CertificateStore, val identitiesKeyStore: CertificateStore) + + private fun validateKeyStores(): X509Certificate { + // Step 1. Check trustStore, sslKeyStore and identitiesKeyStore exist. + val certStores = requireNotNull(getCertificateStores()) { + "One or more keyStores (identity or TLS) or trustStore not found. " + + "Please either copy your existing keys and certificates from another node, " + "or if you don't have one yet, fill out the config file and run corda.jar --initial-registration. " + "Read more at: https://docs.corda.net/permissioning.html" } + // Step 2. Check that trustStore contains the correct key-alias entry. + require(CORDA_ROOT_CA in certStores.trustStore) { + "Alias for trustRoot key not found. Please ensure you have an updated trustStore file." + } + // Step 3. Check that tls keyStore contains the correct key-alias entry. + require(CORDA_CLIENT_TLS in certStores.sslKeyStore) { + "Alias for TLS key not found. Please ensure you have an updated TLS keyStore file." + } - // Check all cert path chain to the trusted root - val sslCertChainRoot = configuration.loadSslKeyStore().getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).last() - val nodeCaCertChainRoot = configuration.loadNodeKeyStore().getCertificateChain(X509Utilities.CORDA_CLIENT_CA).last() - val trustRoot = configuration.loadTrustStore().getCertificate(X509Utilities.CORDA_ROOT_CA) + // Step 4. Check that identity keyStores contain the correct key-alias entry for Node CA. + require(CORDA_CLIENT_CA in certStores.identitiesKeyStore) { + "Alias for Node CA key not found. Please ensure you have an updated identity keyStore file." + } + + // Step 5. Check all cert paths chain to the trusted root. + val trustRoot = certStores.trustStore[CORDA_ROOT_CA] + val sslCertChainRoot = certStores.sslKeyStore.query { getCertificateChain(CORDA_CLIENT_TLS) }.last() + val nodeCaCertChainRoot = certStores.identitiesKeyStore.query { getCertificateChain(CORDA_CLIENT_CA) }.last() require(sslCertChainRoot == trustRoot) { "TLS certificate must chain to the trusted root." } require(nodeCaCertChainRoot == trustRoot) { "Client CA certificate must chain to the trusted root." } @@ -725,12 +768,12 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration, } // Specific class so that MockNode can catch it. - class DatabaseConfigurationException(msg: String) : CordaException(msg) + class DatabaseConfigurationException(message: String) : CordaException(message) protected open fun startDatabase() { val props = configuration.dataSourceProperties if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.") - database.startHikariPool(props, configuration.database, schemaService.internalSchemas()) + database.startHikariPool(props, configuration.database, schemaService.internalSchemas(), metricRegistry) // Now log the vendor string as this will also cause a connection to be tested eagerly. logVendorString(database, log) } @@ -751,7 +794,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration, // Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because // the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with // the identity key. But the infrastructure to make that easy isn't here yet. - return PersistentKeyManagementService(identityService, database) + return PersistentKeyManagementService(cacheFactory, identityService, database) } private fun makeCoreNotaryService(notaryConfig: NotaryConfig, myNotaryIdentity: PartyAndCertificate?): NotaryService { @@ -760,7 +803,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration, return notaryConfig.run { when { raft != null -> { - val uniquenessProvider = RaftUniquenessProvider(configuration, database, platformClock, monitoringService.metrics, raft) + val uniquenessProvider = RaftUniquenessProvider(configuration.baseDirectory, configuration.p2pSslOptions, database, platformClock, monitoringService.metrics, cacheFactory, raft) (if (validating) ::RaftValidatingNotaryService else ::RaftNonValidatingNotaryService)(services, notaryKey, uniquenessProvider) } bftSMaRt != null -> { @@ -787,7 +830,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration, // Meanwhile, we let the remote service send us updates until the acknowledgment buffer overflows and it // unsubscribes us forcibly, rather than blocking the shutdown process. - // Run shutdown hooks in opposite order to starting + // Run shutdown hooks in opposite order to starting. for (toRun in runOnStop.reversed()) { toRun() } @@ -804,40 +847,40 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration, networkParameters: NetworkParameters) private fun obtainIdentity(notaryConfig: NotaryConfig?): Pair<PartyAndCertificate, KeyPair> { - val keyStore = configuration.loadNodeKeyStore() + val keyStore = configuration.signingCertificateStore.get() val (id, singleName) = if (notaryConfig == null || !notaryConfig.isClusterConfig) { - // Node's main identity or if it's a single node notary - Pair(DevIdentityGenerator.NODE_IDENTITY_ALIAS_PREFIX, configuration.myLegalName) + // Node's main identity or if it's a single node notary. + Pair(NODE_IDENTITY_ALIAS_PREFIX, configuration.myLegalName) } else { // The node is part of a distributed notary whose identity must already be generated beforehand. - Pair(DevIdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX, null) + Pair(DISTRIBUTED_NOTARY_ALIAS_PREFIX, null) } // TODO: Integrate with Key management service? val privateKeyAlias = "$id-private-key" if (privateKeyAlias !in keyStore) { - singleName ?: throw IllegalArgumentException( - "Unable to find in the key store the identity of the distributed notary the node is part of") - log.info("$privateKeyAlias not found in key store ${configuration.nodeKeystore}, generating fresh key!") - // TODO This check shouldn't be needed - check(singleName == configuration.myLegalName) + // We shouldn't have a distributed notary at this stage, so singleName should NOT be null. + requireNotNull(singleName) { + "Unable to find in the key store the identity of the distributed notary the node is part of" + } + log.info("$privateKeyAlias not found in key store, generating fresh key!") keyStore.storeLegalIdentity(privateKeyAlias, generateKeyPair()) } - val (x509Cert, keyPair) = keyStore.getCertificateAndKeyPair(privateKeyAlias) + val (x509Cert, keyPair) = keyStore.query { getCertificateAndKeyPair(privateKeyAlias) } // TODO: Use configuration to indicate composite key should be used instead of public key for the identity. val compositeKeyAlias = "$id-composite-key" val certificates = if (compositeKeyAlias in keyStore) { - // Use composite key instead if it exists - val certificate = keyStore.getCertificate(compositeKeyAlias) + // Use composite key instead if it exists. + val certificate = keyStore[compositeKeyAlias] // We have to create the certificate chain for the composite key manually, this is because we don't have a keystore // provider that understand compositeKey-privateKey combo. The cert chain is created using the composite key certificate + // the tail of the private key certificates, as they are both signed by the same certificate chain. - listOf(certificate) + keyStore.getCertificateChain(privateKeyAlias).drop(1) + listOf(certificate) + keyStore.query { getCertificateChain(privateKeyAlias) }.drop(1) } else { - keyStore.getCertificateChain(privateKeyAlias).let { + keyStore.query { getCertificateChain(privateKeyAlias) }.let { check(it[0] == x509Cert) { "Certificates from key store do not line up!" } it } @@ -901,6 +944,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration, override val clock: Clock get() = platformClock override val configuration: NodeConfiguration get() = this@AbstractNode.configuration override val networkMapUpdater: NetworkMapUpdater get() = this@AbstractNode.networkMapUpdater + override val cacheFactory: NamedCacheFactory get() = this@AbstractNode.cacheFactory private lateinit var _myInfo: NodeInfo override val myInfo: NodeInfo get() = _myInfo @@ -1013,9 +1057,9 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig, return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, jdbcUrl, attributeConverters) } -fun CordaPersistence.startHikariPool(hikariProperties: Properties, databaseConfig: DatabaseConfig, schemas: Set<MappedSchema>) { +fun CordaPersistence.startHikariPool(hikariProperties: Properties, databaseConfig: DatabaseConfig, schemas: Set<MappedSchema>, metricRegistry: MetricRegistry? = null) { try { - val dataSource = DataSourceFactory.createDataSource(hikariProperties) + val dataSource = DataSourceFactory.createDataSource(hikariProperties, metricRegistry = metricRegistry) val schemaMigration = SchemaMigration(schemas, dataSource, databaseConfig) schemaMigration.nodeStartup(dataSource.connection.use { DBCheckpointStorage().getCheckpointCount(it) != 0L }) start(dataSource) @@ -1027,4 +1071,13 @@ fun CordaPersistence.startHikariPool(hikariProperties: Properties, databaseConfi else -> throw CouldNotCreateDataSourceException("Could not create the DataSource: ${ex.message}", ex) } } -} \ No newline at end of file +} + +fun clientSslOptionsCompatibleWith(nodeRpcOptions: NodeRpcOptions): ClientRpcSslOptions? { + + if (!nodeRpcOptions.useSsl || nodeRpcOptions.sslConfig == null) { + return null + } + // Here we're using the node's RPC key store as the RPC client's trust store. + return ClientRpcSslOptions(trustStorePath = nodeRpcOptions.sslConfig!!.keyStorePath, trustStorePassword = nodeRpcOptions.sslConfig!!.keyStorePassword) +} diff --git a/node/src/main/kotlin/net/corda/node/internal/CheckpointVerifier.kt b/node/src/main/kotlin/net/corda/node/internal/CheckpointVerifier.kt index ad2a7c903d..53f22b3147 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CheckpointVerifier.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CheckpointVerifier.kt @@ -5,11 +5,12 @@ import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic import net.corda.core.node.ServiceHub import net.corda.core.serialization.SerializationDefaults -import net.corda.core.serialization.deserialize +import net.corda.core.serialization.internal.CheckpointSerializationDefaults +import net.corda.core.serialization.internal.checkpointDeserialize import net.corda.node.services.api.CheckpointStorage import net.corda.node.services.statemachine.SubFlow import net.corda.node.services.statemachine.SubFlowVersion -import net.corda.serialization.internal.SerializeAsTokenContextImpl +import net.corda.serialization.internal.CheckpointSerializeAsTokenContextImpl import net.corda.serialization.internal.withTokenContext object CheckpointVerifier { @@ -19,13 +20,13 @@ object CheckpointVerifier { * @throws CheckpointIncompatibleException if any offending checkpoint is found. */ fun verifyCheckpointsCompatible(checkpointStorage: CheckpointStorage, currentCordapps: List<Cordapp>, platformVersion: Int, serviceHub: ServiceHub, tokenizableServices: List<Any>) { - val checkpointSerializationContext = SerializationDefaults.CHECKPOINT_CONTEXT.withTokenContext( - SerializeAsTokenContextImpl(tokenizableServices, SerializationDefaults.SERIALIZATION_FACTORY, SerializationDefaults.CHECKPOINT_CONTEXT, serviceHub) + val checkpointSerializationContext = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT.withTokenContext( + CheckpointSerializeAsTokenContextImpl(tokenizableServices, CheckpointSerializationDefaults.CHECKPOINT_SERIALIZATION_FACTORY, CheckpointSerializationDefaults.CHECKPOINT_CONTEXT, serviceHub) ) checkpointStorage.getAllCheckpoints().forEach { (_, serializedCheckpoint) -> val checkpoint = try { - serializedCheckpoint.deserialize(context = checkpointSerializationContext) + serializedCheckpoint.checkpointDeserialize(context = checkpointSerializationContext) } catch (e: Exception) { throw CheckpointIncompatibleException.CannotBeDeserialisedException(e) } 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 f584a72818..3000a66127 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -19,6 +19,7 @@ import net.corda.core.internal.RPC_UPLOADER import net.corda.core.internal.STRUCTURAL_STEP_PREFIX import net.corda.core.internal.sign import net.corda.core.messaging.* +import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.NetworkMapCache @@ -27,17 +28,21 @@ import net.corda.core.node.services.vault.* import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.loggerFor import net.corda.node.services.api.FlowStarter import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.messaging.context import net.corda.node.services.statemachine.StateMachineManager import net.corda.nodeapi.exceptions.NonRpcFlowException import net.corda.nodeapi.exceptions.RejectedCommandException +import net.corda.nodeapi.internal.pendingFlowsCount import rx.Observable +import rx.Subscription import java.io.InputStream import java.net.ConnectException import java.security.PublicKey import java.time.Instant +import java.util.concurrent.atomic.AtomicReference /** * Server side implementations of RPCs available to MQ based client tools. Execution takes place on the server @@ -48,13 +53,38 @@ internal class CordaRPCOpsImpl( private val smm: StateMachineManager, private val flowStarter: FlowStarter, private val shutdownNode: () -> Unit -) : CordaRPCOps { +) : CordaRPCOps, AutoCloseable { + + private companion object { + private val logger = loggerFor<CordaRPCOpsImpl>() + } + + private val drainingShutdownHook = AtomicReference<Subscription?>() + + init { + services.nodeProperties.flowsDrainingMode.values.filter { it.isDisabled() }.subscribe({ + cancelDrainingShutdownHook() + }, { + // Nothing to do in case of errors here. + }) + } + + private fun Pair<Boolean, Boolean>.isDisabled(): Boolean = first && !second + + /** + * Returns the RPC protocol version, which is the same the node's platform Version. Exists since version 1 so guaranteed + * to be present. + */ + override val protocolVersion: Int get() = nodeInfo().platformVersion + override fun networkMapSnapshot(): List<NodeInfo> { val (snapshot, updates) = networkMapFeed() updates.notUsed() return snapshot } + override val networkParameters: NetworkParameters get() = services.networkParameters + override fun networkParametersFeed(): DataFeed<ParametersUpdateInfo?, ParametersUpdateInfo> { return services.networkMapUpdater.trackParametersUpdate() } @@ -213,7 +243,7 @@ internal class CordaRPCOpsImpl( return services.networkMapCache.getNodeByLegalIdentity(party) } - override fun registeredFlows(): List<String> = services.rpcFlows.map { it.name }.sorted() + override fun registeredFlows(): List<String> = services.rpcFlows.asSequence().map(Class<*>::getName).sorted().toList() override fun clearNetworkMapCache() { services.networkMapCache.clearNetworkMapCache() @@ -262,18 +292,46 @@ internal class CordaRPCOpsImpl( return vaultTrackBy(criteria, PageSpecification(), sorting, contractStateType) } - override fun setFlowsDrainingModeEnabled(enabled: Boolean) { - services.nodeProperties.flowsDrainingMode.setEnabled(enabled) + override fun setFlowsDrainingModeEnabled(enabled: Boolean) = setPersistentDrainingModeProperty(enabled, propagateChange = true) + + override fun isFlowsDrainingModeEnabled() = services.nodeProperties.flowsDrainingMode.isEnabled() + + override fun shutdown() = terminate(false) + + override fun terminate(drainPendingFlows: Boolean) { + + if (drainPendingFlows) { + logger.info("Waiting for pending flows to complete before shutting down.") + setFlowsDrainingModeEnabled(true) + drainingShutdownHook.set(pendingFlowsCount().updates.doOnNext {(completed, total) -> + logger.info("Pending flows progress before shutdown: $completed / $total.") + }.doOnCompleted { setPersistentDrainingModeProperty(false, false) }.doOnCompleted(::cancelDrainingShutdownHook).doOnCompleted { logger.info("No more pending flows to drain. Shutting down.") }.doOnCompleted(shutdownNode::invoke).subscribe({ + // Nothing to do on each update here, only completion matters. + }, { error -> + logger.error("Error while waiting for pending flows to drain in preparation for shutdown. Cause was: ${error.message}", error) + })) + } else { + shutdownNode.invoke() + } } - override fun isFlowsDrainingModeEnabled(): Boolean { - return services.nodeProperties.flowsDrainingMode.isEnabled() + override fun isWaitingForShutdown() = drainingShutdownHook.get() != null + + override fun close() { + + cancelDrainingShutdownHook() } - override fun shutdown() { - shutdownNode.invoke() + private fun cancelDrainingShutdownHook() { + + drainingShutdownHook.getAndSet(null)?.let { + it.unsubscribe() + logger.info("Cancelled draining shutdown hook.") + } } + private fun setPersistentDrainingModeProperty(enabled: Boolean, propagateChange: Boolean) = services.nodeProperties.flowsDrainingMode.setEnabled(enabled, propagateChange) + private fun stateMachineInfoFromFlowLogic(flowLogic: FlowLogic<*>): StateMachineInfo { return StateMachineInfo(flowLogic.runId, flowLogic.javaClass.name, flowLogic.stateMachine.context.toFlowInitiator(), flowLogic.track(), flowLogic.stateMachine.context) } diff --git a/node/src/main/kotlin/net/corda/node/internal/DataSourceFactory.kt b/node/src/main/kotlin/net/corda/node/internal/DataSourceFactory.kt index d4cad1ff10..e418813989 100644 --- a/node/src/main/kotlin/net/corda/node/internal/DataSourceFactory.kt +++ b/node/src/main/kotlin/net/corda/node/internal/DataSourceFactory.kt @@ -1,5 +1,6 @@ package net.corda.node.internal +import com.codahale.metrics.MetricRegistry import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import com.zaxxer.hikari.util.PropertyElf @@ -32,10 +33,14 @@ object DataSourceFactory { }.set(null, SynchronizedGetPutRemove<String, Database>()) } - fun createDataSource(hikariProperties: Properties, pool: Boolean = true): DataSource { + fun createDataSource(hikariProperties: Properties, pool: Boolean = true, metricRegistry: MetricRegistry? = null): DataSource { val config = HikariConfig(hikariProperties) return if (pool) { - HikariDataSource(config) + val dataSource = HikariDataSource(config) + if (metricRegistry != null) { + dataSource.metricRegistry = metricRegistry + } + dataSource } else { // Basic init for the one test that wants to go via this API but without starting a HikariPool: (Class.forName(hikariProperties.getProperty("dataSourceClassName")).newInstance() as DataSource).also { diff --git a/node/src/main/kotlin/net/corda/node/internal/InitiatedFlowFactory.kt b/node/src/main/kotlin/net/corda/node/internal/InitiatedFlowFactory.kt index f259512109..3b86147c4e 100644 --- a/node/src/main/kotlin/net/corda/node/internal/InitiatedFlowFactory.kt +++ b/node/src/main/kotlin/net/corda/node/internal/InitiatedFlowFactory.kt @@ -12,4 +12,3 @@ sealed class InitiatedFlowFactory<out F : FlowLogic<*>> { val appName: String, override val factory: (FlowSession) -> F) : InitiatedFlowFactory<F>() } - 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 383bc11ba2..f1c9140938 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -21,6 +21,7 @@ import net.corda.core.messaging.RPCOps import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub +import net.corda.core.serialization.internal.CheckpointSerializationFactory import net.corda.core.serialization.internal.SerializationEnvironmentImpl import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort @@ -37,29 +38,24 @@ import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.internal.security.RPCSecurityManagerWithAdditionalUser import net.corda.node.serialization.amqp.AMQPServerSerializationScheme import net.corda.node.serialization.kryo.KRYO_CHECKPOINT_CONTEXT -import net.corda.node.serialization.kryo.KryoServerSerializationScheme +import net.corda.node.serialization.kryo.KryoSerializationScheme import net.corda.node.services.Permissions import net.corda.node.services.api.FlowStarter import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.api.StartedNodeServices -import net.corda.node.services.config.NodeConfiguration -import net.corda.node.services.config.SecurityConfiguration -import net.corda.node.services.config.shouldInitCrashShell -import net.corda.node.services.config.shouldStartLocalShell -import net.corda.node.services.config.JmxReporterType +import net.corda.node.services.config.* import net.corda.node.services.messaging.* import net.corda.node.services.rpc.ArtemisRpcBroker -import net.corda.node.utilities.AddressUtils -import net.corda.node.utilities.AffinityExecutor -import net.corda.node.utilities.DemoClock +import net.corda.node.utilities.* import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_SHELL_USER import net.corda.nodeapi.internal.ShutdownHook import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.bridging.BridgeControlListener import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.crypto.X509Utilities -import net.corda.serialization.internal.* import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException +import net.corda.serialization.internal.* +import org.apache.commons.lang.SystemUtils import org.h2.jdbc.JdbcSQLException import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -71,10 +67,10 @@ import java.net.InetAddress import java.nio.file.Path import java.nio.file.Paths import java.time.Clock +import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger import javax.management.ObjectName import kotlin.system.exitProcess -import java.util.concurrent.TimeUnit class NodeWithInfo(val node: Node, val info: NodeInfo) { val services: StartedNodeServices = object : StartedNodeServices, ServiceHubInternal by node.services, FlowStarter by node.flowStarter {} @@ -92,10 +88,12 @@ class NodeWithInfo(val node: Node, val info: NodeInfo) { open class Node(configuration: NodeConfiguration, versionInfo: VersionInfo, private val initialiseSerialization: Boolean = true, - cordappLoader: CordappLoader = makeCordappLoader(configuration) + cordappLoader: CordappLoader = makeCordappLoader(configuration, versionInfo), + cacheFactoryPrototype: NamedCacheFactory = DefaultNamedCacheFactory() ) : AbstractNode<NodeInfo>( configuration, createClock(configuration), + cacheFactoryPrototype, versionInfo, cordappLoader, // Under normal (non-test execution) it will always be "1" @@ -134,11 +132,33 @@ open class Node(configuration: NodeConfiguration, } private val sameVmNodeCounter = AtomicInteger() - private fun makeCordappLoader(configuration: NodeConfiguration): CordappLoader { - return JarScanningCordappLoader.fromDirectories(configuration.cordappDirectories) + + private fun makeCordappLoader(configuration: NodeConfiguration, versionInfo: VersionInfo): CordappLoader { + return JarScanningCordappLoader.fromDirectories(configuration.cordappDirectories, versionInfo) } + // TODO: make this configurable. const val MAX_RPC_MESSAGE_SIZE = 10485760 + + fun isValidJavaVersion(): Boolean { + if (!hasMinimumJavaVersion()) { + println("You are using a version of Java that is not supported (${SystemUtils.JAVA_VERSION}). Please upgrade to the latest version of Java 8.") + println("Corda will now exit...") + return false + } + return true + } + + private fun hasMinimumJavaVersion(): Boolean { + // when the ext.java8_minUpdateVersion gradle constant changes, so must this check + val major = SystemUtils.JAVA_VERSION_FLOAT + return try { + val update = SystemUtils.JAVA_VERSION.substringAfter("_").toLong() + major == 1.8F && update >= 171 + } catch (e: NumberFormatException) { // custom JDKs may not have the update version (e.g. 1.8.0-adoptopenjdk) + false + } + } } override val log: Logger get() = staticLog @@ -196,7 +216,9 @@ open class Node(configuration: NodeConfiguration, database = database, networkMap = networkMapCache, isDrainingModeOn = nodeProperties.flowsDrainingMode::isEnabled, - drainingModeWasChangedEvents = nodeProperties.flowsDrainingMode.values + drainingModeWasChangedEvents = nodeProperties.flowsDrainingMode.values, + metricRegistry = metricRegistry, + cacheFactory = cacheFactory ) } @@ -227,12 +249,12 @@ open class Node(configuration: NodeConfiguration, startLocalRpcBroker(securityManager) } - val bridgeControlListener = BridgeControlListener(configuration, network.serverAddress, networkParameters.maxMessageSize) + val bridgeControlListener = BridgeControlListener(configuration.p2pSslOptions, network.serverAddress, networkParameters.maxMessageSize) printBasicNodeInfo("Advertised P2P messaging addresses", nodeInfo.addresses.joinToString()) val rpcServerConfiguration = RPCServerConfiguration.DEFAULT rpcServerAddresses?.let { - internalRpcMessagingClient = InternalRPCMessagingClient(configuration, it.admin, MAX_RPC_MESSAGE_SIZE, CordaX500Name.build(configuration.loadSslKeyStore().getCertificate(X509Utilities.CORDA_CLIENT_TLS).subjectX500Principal), rpcServerConfiguration) + internalRpcMessagingClient = InternalRPCMessagingClient(configuration.p2pSslOptions, it.admin, MAX_RPC_MESSAGE_SIZE, CordaX500Name.build(configuration.p2pSslOptions.keyStore.get()[X509Utilities.CORDA_CLIENT_TLS].subjectX500Principal), rpcServerConfiguration) printBasicNodeInfo("RPC connection address", it.primary.toString()) printBasicNodeInfo("RPC admin connection address", it.admin.toString()) } @@ -271,18 +293,17 @@ open class Node(configuration: NodeConfiguration, val rpcBrokerDirectory: Path = baseDirectory / "brokers" / "rpc" with(rpcOptions) { rpcBroker = if (useSsl) { - ArtemisRpcBroker.withSsl(configuration, this.address, adminAddress, sslConfig!!, securityManager, MAX_RPC_MESSAGE_SIZE, jmxMonitoringHttpPort != null, rpcBrokerDirectory, shouldStartLocalShell()) + ArtemisRpcBroker.withSsl(configuration.p2pSslOptions, this.address, adminAddress, sslConfig!!, securityManager, MAX_RPC_MESSAGE_SIZE, jmxMonitoringHttpPort != null, rpcBrokerDirectory, shouldStartLocalShell()) } else { - ArtemisRpcBroker.withoutSsl(configuration, this.address, adminAddress, securityManager, MAX_RPC_MESSAGE_SIZE, jmxMonitoringHttpPort != null, rpcBrokerDirectory, shouldStartLocalShell()) + ArtemisRpcBroker.withoutSsl(configuration.p2pSslOptions, this.address, adminAddress, securityManager, MAX_RPC_MESSAGE_SIZE, jmxMonitoringHttpPort != null, rpcBrokerDirectory, shouldStartLocalShell()) } } - rpcBroker!!.closeOnStop() rpcBroker!!.addresses } } } - override fun myAddresses(): List<NetworkHostAndPort> = listOf(getAdvertisedAddress()) + override fun myAddresses(): List<NetworkHostAndPort> = listOf(getAdvertisedAddress()) + configuration.additionalP2PAddresses private fun getAdvertisedAddress(): NetworkHostAndPort { return with(configuration) { @@ -420,12 +441,13 @@ open class Node(configuration: NodeConfiguration, // https://jolokia.org/agent/jvm.html JmxReporter.forRegistry(registry).inDomain("net.corda").createsObjectNamesWith { _, domain, name -> // Make the JMX hierarchy a bit better organised. - val category = name.substringBefore('.') + val category = name.substringBefore('.').substringBeforeLast('/') + val component = name.substringBefore('.').substringAfterLast('/', "") val subName = name.substringAfter('.', "") - if (subName == "") - ObjectName("$domain:name=$category") + (if (subName == "") + ObjectName("$domain:name=$category${if (component.isNotEmpty()) ",component=$component," else ""}") else - ObjectName("$domain:type=$category,name=$subName") + ObjectName("$domain:type=$category,${if (component.isNotEmpty()) "component=$component," else ""}name=$subName")) }.build().start() } @@ -452,8 +474,8 @@ open class Node(configuration: NodeConfiguration, SerializationFactoryImpl().apply { registerScheme(AMQPServerSerializationScheme(cordappLoader.cordapps)) registerScheme(AMQPClientSerializationScheme(cordappLoader.cordapps)) - registerScheme(KryoServerSerializationScheme()) }, + checkpointSerializationFactory = CheckpointSerializationFactory(KryoSerializationScheme), p2pContext = AMQP_P2P_CONTEXT.withClassLoader(classloader), rpcServerContext = AMQP_RPC_SERVER_CONTEXT.withClassLoader(classloader), storageContext = AMQP_STORAGE_CONTEXT.withClassLoader(classloader), @@ -489,4 +511,4 @@ open class Node(configuration: NodeConfiguration, log.info("Shutdown complete") } -} \ No newline at end of file +} diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index c73dfea87e..681d7ad264 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -1,19 +1,21 @@ package net.corda.node.internal -import com.jcabi.manifests.Manifests import com.typesafe.config.Config import com.typesafe.config.ConfigException import com.typesafe.config.ConfigRenderOptions import io.netty.channel.unix.Errors -import joptsimple.OptionParser -import joptsimple.util.PathConverter +import net.corda.cliutils.CordaCliWrapper +import net.corda.cliutils.CordaVersionProvider +import net.corda.cliutils.ExitCodes import net.corda.core.crypto.Crypto import net.corda.core.internal.* import net.corda.core.internal.concurrent.thenMatch +import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.internal.errors.AddressBindingException import net.corda.core.utilities.Try import net.corda.core.utilities.loggerFor import net.corda.node.* +import net.corda.node.internal.Node.Companion.isValidJavaVersion import net.corda.node.internal.cordapp.MultipleCordappsForFlowException import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfigurationImpl @@ -22,18 +24,19 @@ import net.corda.node.services.config.shouldStartSSHDaemon import net.corda.node.services.transactions.bftSMaRtSerialFilter import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate import net.corda.node.utilities.registration.HTTPNetworkRegistrationService -import net.corda.node.utilities.registration.NodeRegistrationHelper import net.corda.node.utilities.registration.NodeRegistrationException +import net.corda.node.utilities.registration.NodeRegistrationHelper import net.corda.node.utilities.saveToKeyStore import net.corda.node.utilities.saveToTrustStore +import net.corda.nodeapi.internal.PLATFORM_VERSION import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException import net.corda.tools.shell.InteractiveShell import org.fusesource.jansi.Ansi -import org.fusesource.jansi.AnsiConsole import org.slf4j.bridge.SLF4JBridgeHandler +import picocli.CommandLine.Mixin import sun.misc.VMSupport import java.io.Console import java.io.File @@ -42,14 +45,13 @@ import java.io.RandomAccessFile import java.lang.management.ManagementFactory import java.net.InetAddress import java.nio.file.Path -import java.nio.file.Paths import java.time.DayOfWeek import java.time.ZonedDateTime import java.util.* import kotlin.system.exitProcess /** This class is responsible for starting a Node from command line arguments. */ -open class NodeStartup(val args: Array<String>) { +open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { companion object { private val logger by lazy { loggerFor<Node>() } // I guess this is lazy to allow for logging init, but why Node? const val LOGS_DIRECTORY_NAME = "logs" @@ -57,72 +59,84 @@ open class NodeStartup(val args: Array<String>) { private const val INITIAL_REGISTRATION_MARKER = ".initialregistration" } + @Mixin + val cmdLineOptions = NodeCmdLineOptions() /** - * @return true if the node startup was successful. This value is intended to be the exit code of the process. + * @return exit code based on the success of the node startup. This value is intended to be the exit code of the process. */ - open fun run(): Boolean { + override fun runProgram(): Int { val startTime = System.currentTimeMillis() - if (!canNormalizeEmptyPath()) { - println("You are using a version of Java that is not supported (${System.getProperty("java.version")}). Please upgrade to the latest version.") - println("Corda will now exit...") - return false - } + // Step 1. Check for supported Java version. + if (!isValidJavaVersion()) return ExitCodes.FAILURE - val registrationMode = checkRegistrationMode() - val cmdlineOptions: CmdLineOptions = if (registrationMode && !args.contains("--initial-registration")) { - println("Node was started before with `--initial-registration`, but the registration was not completed.\nResuming registration.") - // Pretend that the node was started with `--initial-registration` to help prevent user error. - NodeArgsParser().parseOrExit(*args.plus("--initial-registration")) - } else { - NodeArgsParser().parseOrExit(*args) - } - // We do the single node check before we initialise logging so that in case of a double-node start it + // Step 2. We do the single node check before we initialise logging so that in case of a double-node start it // doesn't mess with the running node's logs. - enforceSingleNodeIsRunning(cmdlineOptions.baseDirectory) + enforceSingleNodeIsRunning(cmdLineOptions.baseDirectory) - initLogging(cmdlineOptions) - // Register all cryptography [Provider]s. + // Step 3. Initialise logging. + initLogging() + + // Step 4. Register all cryptography [Provider]s. // Required to install our [SecureRandom] before e.g., UUID asks for one. // This needs to go after initLogging(netty clashes with our logging). Crypto.registerProviders() + // Step 5. Print banner and basic node info. val versionInfo = getVersionInfo() - - if (cmdlineOptions.isVersion) { - println("${versionInfo.vendor} ${versionInfo.releaseVersion}") - println("Revision ${versionInfo.revision}") - println("Platform Version ${versionInfo.platformVersion}") - return true - } - drawBanner(versionInfo) Node.printBasicNodeInfo(LOGS_CAN_BE_FOUND_IN_STRING, System.getProperty("log-path")) - val configuration = (attempt { loadConfiguration(cmdlineOptions) }.doOnException(handleConfigurationLoadingError(cmdlineOptions.configFile)) as? Try.Success)?.let(Try.Success<NodeConfiguration>::value) ?: return false - + // Step 6. Load and validate node configuration. + val configuration = (attempt { loadConfiguration() }.doOnException(handleConfigurationLoadingError(cmdLineOptions.configFile)) as? Try.Success)?.let(Try.Success<NodeConfiguration>::value) ?: return ExitCodes.FAILURE val errors = configuration.validate() if (errors.isNotEmpty()) { logger.error("Invalid node configuration. Errors were:${System.lineSeparator()}${errors.joinToString(System.lineSeparator())}") - return false + return ExitCodes.FAILURE } - attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success ?: return false + // Step 7. Configuring special serialisation requirements, i.e., bft-smart relies on Java serialization. + attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success ?: return ExitCodes.FAILURE - attempt { preNetworkRegistration(configuration) }.doOnException(handleRegistrationError) as? Try.Success ?: return false + // Step 8. Any actions required before starting up the Corda network layer. + attempt { preNetworkRegistration(configuration) }.doOnException(handleRegistrationError) as? Try.Success ?: return ExitCodes.FAILURE - cmdlineOptions.nodeRegistrationOption?.let { + // Step 9. Check if in registration mode. + checkAndRunRegistrationMode(configuration, versionInfo)?.let { + return if (it) ExitCodes.SUCCESS + else ExitCodes.FAILURE + } + + // Step 10. Log startup info. + logStartupInfo(versionInfo, configuration) + + // Step 11. Start node: create the node, check for other command-line options, add extra logging etc. + attempt { startNode(configuration, versionInfo, startTime) }.doOnSuccess { logger.info("Node exiting successfully") }.doOnException(handleStartError) as? Try.Success ?: return ExitCodes.FAILURE + + return ExitCodes.SUCCESS + } + + private fun checkAndRunRegistrationMode(configuration: NodeConfiguration, versionInfo: VersionInfo): Boolean? { + checkUnfinishedRegistration() + cmdLineOptions.nodeRegistrationOption?.let { // Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig] - attempt { registerWithNetwork(configuration, versionInfo, cmdlineOptions.nodeRegistrationOption) }.doOnException(handleRegistrationError) as? Try.Success ?: return false - + attempt { registerWithNetwork(configuration, versionInfo, it) }.doOnException(handleRegistrationError) as? Try.Success + ?: return false // At this point the node registration was successful. We can delete the marker file. - deleteNodeRegistrationMarker(cmdlineOptions.baseDirectory) + deleteNodeRegistrationMarker(cmdLineOptions.baseDirectory) return true } + return null + } - logStartupInfo(versionInfo, cmdlineOptions, configuration) - - return attempt { startNode(configuration, versionInfo, startTime, cmdlineOptions) }.doOnSuccess { logger.info("Node exiting successfully") }.doOnException(handleStartError).isSuccess + // TODO: Reconsider if automatic re-registration should be applied when something failed during initial registration. + // There might be cases where the node user should investigate what went wrong before registering again. + private fun checkUnfinishedRegistration() { + if (checkRegistrationMode() && !cmdLineOptions.isRegistration) { + println("Node was started before with `--initial-registration`, but the registration was not completed.\nResuming registration.") + // Pretend that the node was started with `--initial-registration` to help prevent user error. + cmdLineOptions.isRegistration = true + } } private fun <RESULT> attempt(action: () -> RESULT): Try<RESULT> = Try.on(action) @@ -131,27 +145,12 @@ open class NodeStartup(val args: Array<String>) { private val startNodeExpectedErrors = setOf(MultipleCordappsForFlowException::class, CheckpointIncompatibleException::class, AddressBindingException::class, NetworkParametersReader::class, DatabaseIncompatibleException::class) - private fun Exception.logAsExpected(message: String? = this.message, print: (String?) -> Unit = logger::error) = print("$message [errorCode=${errorCode()}]") + private fun Exception.logAsExpected(message: String? = this.message, print: (String?) -> Unit = logger::error) = print(message) - private fun Exception.logAsUnexpected(message: String? = this.message, error: Exception = this, print: (String?, Throwable) -> Unit = logger::error) = print("$message${this.message?.let { ": $it" } ?: ""} [errorCode=${errorCode()}]", error) + private fun Exception.logAsUnexpected(message: String? = this.message, error: Exception = this, print: (String?, Throwable) -> Unit = logger::error) = print("$message${this.message?.let { ": $it" } ?: ""}", error) private fun Exception.isOpenJdkKnownIssue() = message?.startsWith("Unknown named curve:") == true - private fun Exception.errorCode(): String { - - val hash = staticLocationBasedHash() - return Integer.toOctalString(hash) - } - - private fun Throwable.staticLocationBasedHash(visited: Set<Throwable> = setOf(this)): Int { - - val cause = this.cause - return when { - cause != null && !visited.contains(cause) -> Objects.hash(this::class.java.name, stackTrace, cause.staticLocationBasedHash(visited + cause)) - else -> Objects.hash(this::class.java.name, stackTrace) - } - } - private val handleRegistrationError = { error: Exception -> when (error) { is NodeRegistrationException -> error.logAsExpected("Node registration service is unavailable. Perhaps try to perform the initial registration again after a while.") @@ -186,14 +185,13 @@ open class NodeStartup(val args: Array<String>) { """.trimIndent() } - private fun loadConfiguration(cmdlineOptions: CmdLineOptions): NodeConfiguration { - - val (rawConfig, configurationResult) = loadConfigFile(cmdlineOptions) - if (cmdlineOptions.devMode) { + private fun loadConfiguration(): NodeConfiguration { + val (rawConfig, configurationResult) = loadConfigFile() + if (cmdLineOptions.devMode == true) { println("Config:\n${rawConfig.root().render(ConfigRenderOptions.defaults())}") } val configuration = configurationResult.getOrThrow() - return if (cmdlineOptions.bootstrapRaftCluster) { + return if (cmdLineOptions.bootstrapRaftCluster) { println("Bootstrapping raft cluster (starting up as seed node).") // Ignore the configured clusterAddresses to make the node bootstrap a cluster instead of joining. (configuration as NodeConfigurationImpl).copy(notary = configuration.notary?.copy(raft = configuration.notary?.raft?.copy(clusterAddresses = emptyList()))) @@ -203,31 +201,14 @@ open class NodeStartup(val args: Array<String>) { } private fun checkRegistrationMode(): Boolean { - // Parse the command line args just to get the base directory. The base directory is needed to determine - // if the node registration marker file exists, _before_ we call NodeArgsParser.parse(). - // If it does exist, we call NodeArgsParser with `--initial-registration` added to the argument list. This way - // we make sure that the initial registration is completed, even if the node was restarted before the first - // attempt to register succeeded and the node administrator forgets to specify `--initial-registration` upon - // restart. - val optionParser = OptionParser() - optionParser.allowsUnrecognizedOptions() - val baseDirectoryArg = optionParser - .accepts("base-directory", "The node working directory where all the files are kept") - .withRequiredArg() - .withValuesConvertedBy(PathConverter()) - .defaultsTo(Paths.get(".")) - val isRegistrationArg = - optionParser.accepts("initial-registration", "Start initial node registration with Corda network to obtain certificate from the permissioning server.") - val optionSet = optionParser.parse(*args) - val baseDirectory = optionSet.valueOf(baseDirectoryArg).normalize().toAbsolutePath() // If the node was started with `--initial-registration`, create marker file. // We do this here to ensure the marker is created even if parsing the args with NodeArgsParser fails. - val marker = File((baseDirectory / INITIAL_REGISTRATION_MARKER).toUri()) - if (!optionSet.has(isRegistrationArg) && !marker.exists()) { + val marker = cmdLineOptions.baseDirectory / INITIAL_REGISTRATION_MARKER + if (!cmdLineOptions.isRegistration && !marker.exists()) { return false } try { - marker.createNewFile() + marker.createFile() } catch (e: Exception) { logger.warn("Could not create marker file for `--initial-registration`.", e) } @@ -249,80 +230,20 @@ open class NodeStartup(val args: Array<String>) { protected open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo) - protected open fun startNode(conf: NodeConfiguration, versionInfo: VersionInfo, startTime: Long, cmdlineOptions: CmdLineOptions) { - - cmdlineOptions.baseDirectory.createDirectories() + protected open fun startNode(conf: NodeConfiguration, versionInfo: VersionInfo, startTime: Long) { + cmdLineOptions.baseDirectory.createDirectories() val node = createNode(conf, versionInfo) - if (cmdlineOptions.clearNetworkMapCache) { + if (cmdLineOptions.clearNetworkMapCache) { node.clearNetworkMapCache() return } - if (cmdlineOptions.justGenerateNodeInfo) { + if (cmdLineOptions.justGenerateNodeInfo) { // Perform the minimum required start-up logic to be able to write a nodeInfo to disk node.generateAndSaveNodeInfo() return } - if (cmdlineOptions.justGenerateRpcSslCerts) { - val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(conf.myLegalName.x500Principal) - - val keyStorePath = conf.baseDirectory / "certificates" / "rpcsslkeystore.jks" - val trustStorePath = conf.baseDirectory / "certificates" / "export" / "rpcssltruststore.jks" - - if (keyStorePath.exists() || trustStorePath.exists()) { - println("Found existing RPC SSL keystores. Command was already run. Exiting..") - exitProcess(0) - } - - val console: Console? = System.console() - - when (console) { - // In this case, the JVM is not connected to the console so we need to exit - null -> { - println("Not connected to console. Exiting") - exitProcess(1) - } - // Otherwise we can proceed normally - else -> { - while (true) { - val keystorePassword1 = console.readPassword("Enter the keystore password => ") - val keystorePassword2 = console.readPassword("Re-enter the keystore password => ") - if (!keystorePassword1.contentEquals(keystorePassword2)) { - println("The keystore passwords don't match.") - continue - } - saveToKeyStore(keyStorePath, keyPair, cert, String(keystorePassword1), "rpcssl") - println("The keystore was saved to: $keyStorePath .") - break - } - - while (true) { - val trustStorePassword1 = console.readPassword("Enter the truststore password => ") - val trustStorePassword2 = console.readPassword("Re-enter the truststore password => ") - if (!trustStorePassword1.contentEquals(trustStorePassword2)) { - println("The truststore passwords don't match.") - continue - } - - saveToTrustStore(trustStorePath, cert, String(trustStorePassword1), "rpcssl") - println("The truststore was saved to: $trustStorePath .") - println("You need to distribute this file along with the password in a secure way to all RPC clients.") - break - } - - val dollar = '$' - println(""" - | - |The SSL certificates were generated successfully. - | - |Add this snippet to the "rpcSettings" section of your node.conf: - | useSsl=true - | ssl { - | keyStorePath=$dollar{baseDirectory}/certificates/rpcsslkeystore.jks - | keyStorePassword=the_above_password - | } - |""".trimMargin()) - } - } + if (cmdLineOptions.justGenerateRpcSslCerts) { + generateRpcSslCertificates(conf) return } @@ -335,8 +256,10 @@ open class NodeStartup(val args: Array<String>) { } val nodeInfo = node.start() - Node.printBasicNodeInfo("Loaded CorDapps", node.services.cordappProvider.cordapps.joinToString { it.name }) + logLoadedCorDapps(node.services.cordappProvider.cordapps) + node.nodeReadyFuture.thenMatch({ + // Elapsed time in seconds. We used 10 / 100.0 and not directly / 1000.0 to only keep two decimal digits. val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0 val name = nodeInfo.legalIdentitiesAndCerts.first().name.organisation Node.printBasicNodeInfo("Node for \"$name\" started up and registered in $elapsed sec") @@ -361,7 +284,83 @@ open class NodeStartup(val args: Array<String>) { node.run() } - protected open fun logStartupInfo(versionInfo: VersionInfo, cmdlineOptions: CmdLineOptions, conf: NodeConfiguration) { + private fun generateRpcSslCertificates(conf: NodeConfiguration) { + val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(conf.myLegalName.x500Principal) + + val keyStorePath = conf.baseDirectory / "certificates" / "rpcsslkeystore.jks" + val trustStorePath = conf.baseDirectory / "certificates" / "export" / "rpcssltruststore.jks" + + if (keyStorePath.exists() || trustStorePath.exists()) { + println("Found existing RPC SSL keystores. Command was already run. Exiting..") + exitProcess(0) + } + + val console: Console? = System.console() + + when (console) { + // In this case, the JVM is not connected to the console so we need to exit. + null -> { + println("Not connected to console. Exiting") + exitProcess(1) + } + // Otherwise we can proceed normally. + else -> { + while (true) { + val keystorePassword1 = console.readPassword("Enter the RPC keystore password => ") + // TODO: consider adding a password strength policy. + if (keystorePassword1.isEmpty()) { + println("The RPC keystore password cannot be an empty String.") + continue + } + + val keystorePassword2 = console.readPassword("Re-enter the RPC keystore password => ") + if (!keystorePassword1.contentEquals(keystorePassword2)) { + println("The RPC keystore passwords don't match.") + continue + } + + saveToKeyStore(keyStorePath, keyPair, cert, String(keystorePassword1), "rpcssl") + println("The RPC keystore was saved to: $keyStorePath .") + break + } + + while (true) { + val trustStorePassword1 = console.readPassword("Enter the RPC truststore password => ") + // TODO: consider adding a password strength policy. + if (trustStorePassword1.isEmpty()) { + println("The RPC truststore password cannot be an empty String.") + continue + } + + val trustStorePassword2 = console.readPassword("Re-enter the RPC truststore password => ") + if (!trustStorePassword1.contentEquals(trustStorePassword2)) { + println("The RPC truststore passwords don't match.") + continue + } + + saveToTrustStore(trustStorePath, cert, String(trustStorePassword1), "rpcssl") + println("The RPC truststore was saved to: $trustStorePath .") + println("You need to distribute this file along with the password in a secure way to all RPC clients.") + break + } + + val dollar = '$' + println(""" + | + |The SSL certificates for RPC were generated successfully. + | + |Add this snippet to the "rpcSettings" section of your node.conf: + | useSsl=true + | ssl { + | keyStorePath=$dollar{baseDirectory}/certificates/rpcsslkeystore.jks + | keyStorePassword=the_above_password + | } + |""".trimMargin()) + } + } + } + + protected open fun logStartupInfo(versionInfo: VersionInfo, conf: NodeConfiguration) { logger.info("Vendor: ${versionInfo.vendor}") logger.info("Release: ${versionInfo.releaseVersion}") logger.info("Platform Version: ${versionInfo.platformVersion}") @@ -370,12 +369,11 @@ open class NodeStartup(val args: Array<String>) { logger.info("PID: ${info.name.split("@").firstOrNull()}") // TODO Java 9 has better support for this logger.info("Main class: ${NodeConfiguration::class.java.location.toURI().path}") logger.info("CommandLine Args: ${info.inputArguments.joinToString(" ")}") - logger.info("Application Args: ${args.joinToString(" ")}") logger.info("bootclasspath: ${info.bootClassPath}") logger.info("classpath: ${info.classPath}") logger.info("VM ${info.vmName} ${info.vmVendor} ${info.vmVersion}") logger.info("Machine: ${lookupMachineNameAndMaybeWarn()}") - logger.info("Working Directory: ${cmdlineOptions.baseDirectory}") + logger.info("Working Directory: ${cmdLineOptions.baseDirectory}") val agentProperties = VMSupport.getAgentProperties() if (agentProperties.containsKey("sun.jdwp.listenerAddress")) { logger.info("Debug port: ${agentProperties.getProperty("sun.jdwp.listenerAddress")}") @@ -403,24 +401,32 @@ open class NodeStartup(val args: Array<String>) { println("Corda node will now terminate.") } - protected open fun loadConfigFile(cmdlineOptions: CmdLineOptions): Pair<Config, Try<NodeConfiguration>> = cmdlineOptions.loadConfig() + protected open fun loadConfigFile(): Pair<Config, Try<NodeConfiguration>> = cmdLineOptions.loadConfig() protected open fun banJavaSerialisation(conf: NodeConfiguration) { SerialFilter.install(if (conf.notary?.bftSMaRt != null) ::bftSMaRtSerialFilter else ::defaultSerialFilter) } protected open fun getVersionInfo(): VersionInfo { - // Manifest properties are only available if running from the corda jar - fun manifestValue(name: String): String? = if (Manifests.exists(name)) Manifests.read(name) else null - return VersionInfo( - manifestValue("Corda-Platform-Version")?.toInt() ?: 1, - manifestValue("Corda-Release-Version") ?: "Unknown", - manifestValue("Corda-Revision") ?: "Unknown", - manifestValue("Corda-Vendor") ?: "Unknown" + PLATFORM_VERSION, + CordaVersionProvider.releaseVersion, + CordaVersionProvider.revision, + CordaVersionProvider.vendor ) } + protected open fun logLoadedCorDapps(corDapps: List<CordappImpl>) { + fun CordappImpl.Info.description() = "$shortName version $version by $vendor" + + Node.printBasicNodeInfo("Loaded ${corDapps.size} CorDapp(s)", corDapps.map { it.info }.joinToString(", ", transform = CordappImpl.Info::description)) + corDapps.map { it.info }.filter { it.hasUnknownFields() }.let { malformed -> + if (malformed.isNotEmpty()) { + logger.warn("Found ${malformed.size} CorDapp(s) with unknown information. They will be unable to run on Corda in the future.") + } + } + } + private fun enforceSingleNodeIsRunning(baseDirectory: Path) { // Write out our process ID (which may or may not resemble a UNIX process id - to us it's just a string) to a // file that we'll do our best to delete on exit. But if we don't, it'll be overwritten next time. If it already @@ -454,14 +460,14 @@ open class NodeStartup(val args: Array<String>) { } } - protected open fun initLogging(cmdlineOptions: CmdLineOptions) { - val loggingLevel = cmdlineOptions.loggingLevel.name.toLowerCase(Locale.ENGLISH) + override fun initLogging() { + val loggingLevel = loggingLevel.name.toLowerCase(Locale.ENGLISH) System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file. - if (cmdlineOptions.logToConsole) { + if (verbose) { System.setProperty("consoleLogLevel", loggingLevel) Node.renderBasicInfoToConsole = false } - System.setProperty("log-path", (cmdlineOptions.baseDirectory / LOGS_DIRECTORY_NAME).toString()) + System.setProperty("log-path", (cmdLineOptions.baseDirectory / LOGS_DIRECTORY_NAME).toString()) SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler. SLF4JBridgeHandler.install() } @@ -491,20 +497,7 @@ open class NodeStartup(val args: Array<String>) { return hostName } - private fun canNormalizeEmptyPath(): Boolean { - // Check we're not running a version of Java with a known bug: https://github.com/corda/corda/issues/83 - return try { - Paths.get("").normalize() - true - } catch (e: ArrayIndexOutOfBoundsException) { - false - } - } - open fun drawBanner(versionInfo: VersionInfo) { - // This line makes sure ANSI escapes work on Windows, where they aren't supported out of the box. - AnsiConsole.systemInstall() - Emoji.renderIfSupported { val messages = arrayListOf( "The only distributed ledger that pays\nhomage to Pac Man in its logo.", @@ -581,3 +574,4 @@ open class NodeStartup(val args: Array<String>) { } } } + diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt index 4241b6aea2..5ab49c53a0 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt @@ -8,6 +8,7 @@ import net.corda.core.cordapp.CordappContext import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER +import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.internal.createCordappContext import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentStorage @@ -34,7 +35,7 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader, /** * Current known CorDapps loaded on this node */ - override val cordapps get() = cordappLoader.cordapps + override val cordapps: List<CordappImpl> get() = cordappLoader.cordapps fun start(whitelistedContractImplementations: Map<String, List<AttachmentId>>) { cordappAttachments.putAll(loadContractsIntoAttachmentStore()) diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderInternal.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderInternal.kt index 01b88312e6..fcd6e4d602 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderInternal.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderInternal.kt @@ -3,8 +3,9 @@ package net.corda.node.internal.cordapp import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.CordappProvider import net.corda.core.flows.FlowLogic +import net.corda.core.internal.cordapp.CordappImpl interface CordappProviderInternal : CordappProvider { - val cordapps: List<Cordapp> + val cordapps: List<CordappImpl> fun getCordappForFlow(flowLogic: FlowLogic<*>): Cordapp? } diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt index 94dc45a0fb..c691af76c7 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt @@ -8,12 +8,14 @@ import net.corda.core.crypto.sha256 import net.corda.core.flows.* import net.corda.core.internal.* import net.corda.core.internal.cordapp.CordappImpl +import net.corda.core.internal.cordapp.CordappInfoResolver import net.corda.core.node.services.CordaService import net.corda.core.schemas.MappedSchema import net.corda.core.serialization.SerializationCustomSerializer import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import net.corda.core.utilities.contextLogger +import net.corda.node.VersionInfo import net.corda.node.cordapp.CordappLoader import net.corda.node.internal.classloading.requireAnnotation import net.corda.nodeapi.internal.coreContractClasses @@ -24,6 +26,7 @@ import java.net.URL import java.net.URLClassLoader import java.nio.file.Path import java.util.* +import java.util.jar.JarInputStream import kotlin.reflect.KClass import kotlin.streams.toList @@ -32,9 +35,10 @@ import kotlin.streams.toList * * @property cordappJarPaths The classpath of cordapp JARs */ -class JarScanningCordappLoader private constructor(private val cordappJarPaths: List<RestrictedURL>) : CordappLoaderTemplate() { +class JarScanningCordappLoader private constructor(private val cordappJarPaths: List<RestrictedURL>, + private val versionInfo: VersionInfo = VersionInfo.UNKNOWN) : CordappLoaderTemplate() { - override val cordapps: List<Cordapp> by lazy { loadCordapps() + coreCordapp } + override val cordapps: List<CordappImpl> by lazy { loadCordapps() + coreCordapp } override val appClassLoader: ClassLoader = URLClassLoader(cordappJarPaths.stream().map { it.url }.toTypedArray(), javaClass.classLoader) @@ -54,10 +58,9 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: * * @param corDappDirectories Directories used to scan for CorDapp JARs. */ - fun fromDirectories(corDappDirectories: Iterable<Path>): CordappLoader { - + fun fromDirectories(corDappDirectories: Iterable<Path>, versionInfo: VersionInfo = VersionInfo.UNKNOWN): JarScanningCordappLoader { logger.info("Looking for CorDapps in ${corDappDirectories.distinct().joinToString(", ", "[", "]")}") - return JarScanningCordappLoader(corDappDirectories.distinct().flatMap(this::jarUrlsInDirectory).map { it.restricted() }) + return JarScanningCordappLoader(corDappDirectories.distinct().flatMap(this::jarUrlsInDirectory).map { it.restricted() }, versionInfo) } /** @@ -65,7 +68,9 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: * * @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection. */ - fun fromJarUrls(scanJars: List<URL>) = JarScanningCordappLoader(scanJars.map { it.restricted() }) + fun fromJarUrls(scanJars: List<URL>, versionInfo: VersionInfo = VersionInfo.UNKNOWN): JarScanningCordappLoader { + return JarScanningCordappLoader(scanJars.map { it.restricted() }, versionInfo) + } private fun URL.restricted(rootPackageName: String? = null) = RestrictedURL(this, rootPackageName) @@ -86,31 +91,42 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: ContractUpgradeFlow.Initiate::class.java, ContractUpgradeFlow.Authorise::class.java, ContractUpgradeFlow.Deauthorise::class.java) - - /** A Cordapp representing the core package which is not scanned automatically. */ - @VisibleForTesting - internal val coreCordapp = CordappImpl( - contractClassNames = listOf(), - initiatedFlows = listOf(), - rpcFlows = coreRPCFlows, - serviceFlows = listOf(), - schedulableFlows = listOf(), - services = listOf(), - serializationWhitelists = listOf(), - serializationCustomSerializers = listOf(), - customSchemas = setOf(), - allFlows = listOf(), - jarPath = ContractUpgradeFlow.javaClass.location, // Core JAR location - jarHash = SecureHash.allOnesHash - ) } - private fun loadCordapps(): List<Cordapp> { - return cordappJarPaths.map { scanCordapp(it).toCordapp(it) } + /** A Cordapp representing the core package which is not scanned automatically. */ + @VisibleForTesting + internal val coreCordapp = CordappImpl( + contractClassNames = listOf(), + initiatedFlows = listOf(), + rpcFlows = coreRPCFlows, + serviceFlows = listOf(), + schedulableFlows = listOf(), + services = listOf(), + serializationWhitelists = listOf(), + serializationCustomSerializers = listOf(), + customSchemas = setOf(), + info = CordappImpl.Info("corda-core", versionInfo.vendor, versionInfo.releaseVersion, 1, versionInfo.platformVersion), + allFlows = listOf(), + jarPath = ContractUpgradeFlow.javaClass.location, // Core JAR location + jarHash = SecureHash.allOnesHash + ) + + private fun loadCordapps(): List<CordappImpl> { + val cordapps = cordappJarPaths.map { scanCordapp(it).toCordapp(it) } + .filter { + if (it.info.minimumPlatformVersion > versionInfo.platformVersion) { + logger.warn("Not loading CorDapp ${it.info.shortName} (${it.info.vendor}) as it requires minimum platform version ${it.info.minimumPlatformVersion} (This node is running version ${versionInfo.platformVersion}).") + false + } else { + true + } + } + cordapps.forEach { CordappInfoResolver.register(it.cordappClasses, it.info) } + return cordapps } - private fun RestrictedScanResult.toCordapp(url: RestrictedURL): Cordapp { - + private fun RestrictedScanResult.toCordapp(url: RestrictedURL): CordappImpl { + val info = url.url.openStream().let(::JarInputStream).use { it.manifest }.toCordappInfo(CordappImpl.jarName(url.url)) return CordappImpl( findContractClassNames(this), findInitiatedFlows(this), @@ -123,6 +139,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: findCustomSchemas(this), findAllFlows(this), url.url, + info, getJarHash(url.url) ) } @@ -268,24 +285,23 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: class MultipleCordappsForFlowException(message: String) : Exception(message) abstract class CordappLoaderTemplate : CordappLoader { - override val flowCordappMap: Map<Class<out FlowLogic<*>>, Cordapp> by lazy { cordapps.flatMap { corDapp -> corDapp.allFlows.map { flow -> flow to corDapp } } .groupBy { it.first } - .mapValues { - if(it.value.size > 1) { throw MultipleCordappsForFlowException("There are multiple CorDapp JARs on the classpath for flow ${it.value.first().first.name}: [ ${it.value.joinToString { it.second.name }} ].") } - it.value.single().second + .mapValues { entry -> + if (entry.value.size > 1) { + throw MultipleCordappsForFlowException("There are multiple CorDapp JARs on the classpath for flow " + + "${entry.value.first().first.name}: [ ${entry.value.joinToString { it.second.name }} ].") + } + entry.value.single().second } } override val cordappSchemas: Set<MappedSchema> by lazy { - cordapps.flatMap { it.customSchemas }.toSet() } override val appClassLoader: ClassLoader by lazy { - URLClassLoader(cordapps.stream().map { it.jarPath }.toTypedArray(), javaClass.classLoader) } } - diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/ManifestUtils.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/ManifestUtils.kt index 86d6a3168b..c71a5bf7cb 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/ManifestUtils.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/ManifestUtils.kt @@ -1,13 +1,10 @@ package net.corda.node.internal.cordapp -import net.corda.core.cordapp.Cordapp import net.corda.core.internal.cordapp.CordappImpl -import java.util.* import java.util.jar.Attributes import java.util.jar.Manifest fun createTestManifest(name: String, title: String, version: String, vendor: String): Manifest { - val manifest = Manifest() // Mandatory manifest attribute. If not present, all other entries are silently skipped. @@ -27,21 +24,22 @@ fun createTestManifest(name: String, title: String, version: String, vendor: Str } operator fun Manifest.set(key: String, value: String) { - mainAttributes.putValue(key, value) } -internal fun Manifest?.toCordappInfo(defaultShortName: String): Cordapp.Info { - - var unknown = CordappImpl.Info.UNKNOWN +fun Manifest?.toCordappInfo(defaultShortName: String): CordappImpl.Info { + var info = CordappImpl.Info.UNKNOWN (this?.mainAttributes?.getValue("Name") ?: defaultShortName).let { shortName -> - unknown = unknown.copy(shortName = shortName) + info = info.copy(shortName = shortName) } this?.mainAttributes?.getValue("Implementation-Vendor")?.let { vendor -> - unknown = unknown.copy(vendor = vendor) + info = info.copy(vendor = vendor) } this?.mainAttributes?.getValue("Implementation-Version")?.let { version -> - unknown = unknown.copy(version = version) + info = info.copy(version = version) } - return unknown + val minPlatformVersion = this?.mainAttributes?.getValue("Min-Platform-Version")?.toInt() ?: 1 + val targetPlatformVersion = this?.mainAttributes?.getValue("Target-Platform-Version")?.toInt() ?: minPlatformVersion + info = info.copy(minimumPlatformVersion = minPlatformVersion, targetPlatformVersion = targetPlatformVersion) + return info } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/rpc/proxies/AuthenticatedRpcOpsProxy.kt b/node/src/main/kotlin/net/corda/node/internal/rpc/proxies/AuthenticatedRpcOpsProxy.kt index e27ce2800f..f8e0366045 100644 --- a/node/src/main/kotlin/net/corda/node/internal/rpc/proxies/AuthenticatedRpcOpsProxy.kt +++ b/node/src/main/kotlin/net/corda/node/internal/rpc/proxies/AuthenticatedRpcOpsProxy.kt @@ -16,6 +16,8 @@ internal class AuthenticatedRpcOpsProxy(private val delegate: CordaRPCOps) : Cor /** * Returns the RPC protocol version, which is the same the node's Platform Version. Exists since version 1 so guaranteed * to be present. + * + * TODO: Why is this logic duplicated vs the actual implementation? */ override val protocolVersion: Int get() = delegate.nodeInfo().platformVersion @@ -31,7 +33,6 @@ internal class AuthenticatedRpcOpsProxy(private val delegate: CordaRPCOps) : Cor private companion object { private fun proxy(delegate: CordaRPCOps, context: () -> RpcAuthContext): CordaRPCOps { - val handler = PermissionsEnforcingInvocationHandler(delegate, context) return Proxy.newProxyInstance(delegate::class.java.classLoader, arrayOf(CordaRPCOps::class.java), handler) as CordaRPCOps } diff --git a/node/src/main/kotlin/net/corda/node/internal/rpc/proxies/ExceptionMaskingRpcOpsProxy.kt b/node/src/main/kotlin/net/corda/node/internal/rpc/proxies/ExceptionMaskingRpcOpsProxy.kt new file mode 100644 index 0000000000..244fade5cb --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/rpc/proxies/ExceptionMaskingRpcOpsProxy.kt @@ -0,0 +1,125 @@ +package net.corda.node.internal.rpc.proxies + +import net.corda.core.ClientRelevantError +import net.corda.core.CordaException +import net.corda.core.CordaRuntimeException +import net.corda.core.concurrent.CordaFuture +import net.corda.core.contracts.TransactionVerificationException +import net.corda.core.doOnError +import net.corda.core.flows.IdentifiableException +import net.corda.core.internal.concurrent.doOnError +import net.corda.core.internal.concurrent.mapError +import net.corda.core.internal.declaredField +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.FlowHandleImpl +import net.corda.core.messaging.FlowProgressHandle +import net.corda.core.messaging.FlowProgressHandleImpl +import net.corda.core.utilities.loggerFor +import net.corda.node.internal.InvocationHandlerTemplate +import net.corda.nodeapi.exceptions.InternalNodeException +import rx.Observable +import java.lang.reflect.Method +import java.lang.reflect.Proxy.newProxyInstance +import kotlin.reflect.KClass + +internal class ExceptionMaskingRpcOpsProxy(private val delegate: CordaRPCOps, doLog: Boolean) : CordaRPCOps by proxy(delegate, doLog) { + private companion object { + private val logger = loggerFor<ExceptionMaskingRpcOpsProxy>() + + private val whitelist = setOf( + ClientRelevantError::class, + TransactionVerificationException::class + ) + + private fun proxy(delegate: CordaRPCOps, doLog: Boolean): CordaRPCOps { + val handler = ErrorObfuscatingInvocationHandler(delegate, whitelist, doLog) + return newProxyInstance(delegate::class.java.classLoader, arrayOf(CordaRPCOps::class.java), handler) as CordaRPCOps + } + } + + private class ErrorObfuscatingInvocationHandler(override val delegate: CordaRPCOps, private val whitelist: Set<KClass<*>>, private val doLog: Boolean) : InvocationHandlerTemplate { + override fun invoke(proxy: Any, method: Method, arguments: Array<out Any?>?): Any? { + try { + val result = super.invoke(proxy, method, arguments) + return result?.let { obfuscateResult(it) } + } catch (exception: Exception) { + // In this special case logging and re-throwing is the right approach. + log(exception) + throw obfuscate(exception) + } + } + + private fun <RESULT : Any> obfuscateResult(result: RESULT): Any { + return when (result) { + is CordaFuture<*> -> wrapFuture(result) + is DataFeed<*, *> -> wrapFeed(result) + is FlowProgressHandle<*> -> wrapFlowProgressHandle(result) + is FlowHandle<*> -> wrapFlowHandle(result) + is Observable<*> -> wrapObservable(result) + else -> result + } + } + + private fun wrapFlowProgressHandle(handle: FlowProgressHandle<*>): FlowProgressHandle<*> { + val returnValue = wrapFuture(handle.returnValue) + val progress = wrapObservable(handle.progress) + val stepsTreeIndexFeed = handle.stepsTreeIndexFeed?.let { wrapFeed(it) } + val stepsTreeFeed = handle.stepsTreeFeed?.let { wrapFeed(it) } + + return FlowProgressHandleImpl(handle.id, returnValue, progress, stepsTreeIndexFeed, stepsTreeFeed) + } + + private fun wrapFlowHandle(handle: FlowHandle<*>): FlowHandle<*> { + return FlowHandleImpl(handle.id, wrapFuture(handle.returnValue)) + } + + private fun <ELEMENT> wrapObservable(observable: Observable<ELEMENT>): Observable<ELEMENT> { + return observable.doOnError(::log).mapErrors(::obfuscate) + } + + private fun <SNAPSHOT, ELEMENT> wrapFeed(feed: DataFeed<SNAPSHOT, ELEMENT>): DataFeed<SNAPSHOT, ELEMENT> { + return feed.doOnError(::log).mapErrors(::obfuscate) + } + + private fun <RESULT> wrapFuture(future: CordaFuture<RESULT>): CordaFuture<RESULT> { + return future.doOnError(::log).mapError(::obfuscate) + } + + private fun log(error: Throwable) { + if (doLog) { + logger.error("Error during RPC invocation", error) + } + } + + private fun obfuscate(error: Throwable): Throwable { + val exposed = if (error.isWhitelisted()) error else InternalNodeException((error as? IdentifiableException)?.errorId) + removeDetails(exposed) + return exposed + } + + private fun removeDetails(error: Throwable) { + error.stackTrace = arrayOf<StackTraceElement>() + error.declaredField<Any?>("cause").value = null + error.declaredField<Any?>("suppressedExceptions").value = null + when (error) { + is CordaException -> error.setCause(null) + is CordaRuntimeException -> error.setCause(null) + } + } + + private fun Throwable.isWhitelisted(): Boolean { + return whitelist.any { it.isInstance(this) } + } + + override fun toString(): String { + return "ErrorObfuscatingInvocationHandler(whitelist=$whitelist)" + } + } + + override fun toString(): String { + return "ExceptionMaskingRpcOpsProxy" + } +} diff --git a/node/src/main/kotlin/net/corda/node/serialization/amqp/RpcServerObservableSerializer.kt b/node/src/main/kotlin/net/corda/node/serialization/amqp/RpcServerObservableSerializer.kt index 7cdd638152..e010741d18 100644 --- a/node/src/main/kotlin/net/corda/node/serialization/amqp/RpcServerObservableSerializer.kt +++ b/node/src/main/kotlin/net/corda/node/serialization/amqp/RpcServerObservableSerializer.kt @@ -2,6 +2,7 @@ package net.corda.node.serialization.amqp import net.corda.core.context.Trace import net.corda.core.serialization.SerializationContext +import net.corda.core.utilities.contextLogger import net.corda.core.utilities.loggerFor import net.corda.node.services.messaging.ObservableContextInterface import net.corda.node.services.messaging.ObservableSubscription @@ -30,8 +31,9 @@ class RpcServerObservableSerializer : CustomSerializer.Implements<Observable<*>> fun createContext( serializationContext: SerializationContext, observableContext: ObservableContextInterface - ) = serializationContext.withProperty( - RpcServerObservableSerializer.RpcObservableContextKey, observableContext) + ) = serializationContext.withProperty(RpcServerObservableSerializer.RpcObservableContextKey, observableContext) + + val log = contextLogger() } override val schemaForDocumentation = Schema( @@ -136,5 +138,6 @@ class RpcServerObservableSerializer : CustomSerializer.Implements<Observable<*>> } } observableContext.observableMap.put(observableId, observableWithSubscription) + log.trace("Serialized observable $observableId of type $obj") } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/serialization/kryo/CordaClassResolver.kt b/node/src/main/kotlin/net/corda/node/serialization/kryo/CordaClassResolver.kt index e9ce3e4d56..e3ff2584f7 100644 --- a/node/src/main/kotlin/net/corda/node/serialization/kryo/CordaClassResolver.kt +++ b/node/src/main/kotlin/net/corda/node/serialization/kryo/CordaClassResolver.kt @@ -8,8 +8,8 @@ import com.esotericsoftware.kryo.util.DefaultClassResolver import com.esotericsoftware.kryo.util.Util import net.corda.core.internal.kotlinObjectInstance import net.corda.core.internal.writer +import net.corda.core.serialization.internal.CheckpointSerializationContext import net.corda.core.serialization.ClassWhitelist -import net.corda.core.serialization.SerializationContext import net.corda.core.utilities.contextLogger import net.corda.serialization.internal.AttachmentsClassLoader import net.corda.serialization.internal.MutableClassWhitelist @@ -25,7 +25,7 @@ import java.util.* /** * Corda specific class resolver which enables extra customisation for the purposes of serialization using Kryo */ -class CordaClassResolver(serializationContext: SerializationContext) : DefaultClassResolver() { +class CordaClassResolver(serializationContext: CheckpointSerializationContext) : DefaultClassResolver() { val whitelist: ClassWhitelist = TransientClassWhiteList(serializationContext.whitelist) // These classes are assignment-compatible Java equivalents of Kotlin classes. diff --git a/node/src/main/kotlin/net/corda/node/serialization/kryo/Kryo.kt b/node/src/main/kotlin/net/corda/node/serialization/kryo/Kryo.kt index e7b94b9635..674f4702f2 100644 --- a/node/src/main/kotlin/net/corda/node/serialization/kryo/Kryo.kt +++ b/node/src/main/kotlin/net/corda/node/serialization/kryo/Kryo.kt @@ -14,12 +14,11 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationContext.UseCase.Checkpoint -import net.corda.core.serialization.SerializationContext.UseCase.Storage import net.corda.core.serialization.SerializeAsTokenContext import net.corda.core.serialization.SerializedBytes import net.corda.core.transactions.* import net.corda.core.utilities.OpaqueBytes +import net.corda.serialization.internal.checkUseCase import net.corda.serialization.internal.serializationContextKey import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -275,16 +274,9 @@ object SignedTransactionSerializer : Serializer<SignedTransaction>() { } } -sealed class UseCaseSerializer<T>(private val allowedUseCases: EnumSet<SerializationContext.UseCase>) : Serializer<T>() { - protected fun checkUseCase() { - net.corda.serialization.internal.checkUseCase(allowedUseCases) - } -} - @ThreadSafe -object PrivateKeySerializer : UseCaseSerializer<PrivateKey>(EnumSet.of(Storage, Checkpoint)) { +object PrivateKeySerializer : Serializer<PrivateKey>() { override fun write(kryo: Kryo, output: Output, obj: PrivateKey) { - checkUseCase() output.writeBytesWithLength(obj.encoded) } diff --git a/node/src/main/kotlin/net/corda/node/serialization/kryo/KryoSerializationScheme.kt b/node/src/main/kotlin/net/corda/node/serialization/kryo/KryoSerializationScheme.kt index 417fceabec..22edf8258e 100644 --- a/node/src/main/kotlin/net/corda/node/serialization/kryo/KryoSerializationScheme.kt +++ b/node/src/main/kotlin/net/corda/node/serialization/kryo/KryoSerializationScheme.kt @@ -10,10 +10,9 @@ import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.pool.KryoPool import com.esotericsoftware.kryo.serializers.ClosureSerializer import net.corda.core.internal.uncheckedCast -import net.corda.core.serialization.ClassWhitelist -import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationDefaults -import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.* +import net.corda.core.serialization.internal.CheckpointSerializationContext +import net.corda.core.serialization.internal.CheckpointSerializationScheme import net.corda.core.utilities.ByteSequence import net.corda.serialization.internal.* import java.security.PublicKey @@ -32,46 +31,30 @@ private object AutoCloseableSerialisationDetector : Serializer<AutoCloseable>() override fun read(kryo: Kryo, input: Input, type: Class<AutoCloseable>) = throw IllegalStateException("Should not reach here!") } -abstract class AbstractKryoSerializationScheme : SerializationScheme { +object KryoSerializationScheme : CheckpointSerializationScheme { private val kryoPoolsForContexts = ConcurrentHashMap<Pair<ClassWhitelist, ClassLoader>, KryoPool>() - protected abstract fun rpcClientKryoPool(context: SerializationContext): KryoPool - protected abstract fun rpcServerKryoPool(context: SerializationContext): KryoPool - - // this can be overridden in derived serialization schemes - protected open val publicKeySerializer: Serializer<PublicKey> = PublicKeySerializer - - private fun getPool(context: SerializationContext): KryoPool { + private fun getPool(context: CheckpointSerializationContext): KryoPool { return kryoPoolsForContexts.computeIfAbsent(Pair(context.whitelist, context.deserializationClassLoader)) { - when (context.useCase) { - SerializationContext.UseCase.Checkpoint -> - KryoPool.Builder { - val serializer = Fiber.getFiberSerializer(false) as KryoSerializer - val classResolver = CordaClassResolver(context).apply { setKryo(serializer.kryo) } - // TODO The ClassResolver can only be set in the Kryo constructor and Quasar doesn't provide us with a way of doing that - val field = Kryo::class.java.getDeclaredField("classResolver").apply { isAccessible = true } - serializer.kryo.apply { - field.set(this, classResolver) - // don't allow overriding the public key serializer for checkpointing - DefaultKryoCustomizer.customize(this) - addDefaultSerializer(AutoCloseable::class.java, AutoCloseableSerialisationDetector) - register(ClosureSerializer.Closure::class.java, CordaClosureSerializer) - classLoader = it.second - } - }.build() - SerializationContext.UseCase.RPCClient -> - rpcClientKryoPool(context) - SerializationContext.UseCase.RPCServer -> - rpcServerKryoPool(context) - else -> - KryoPool.Builder { - DefaultKryoCustomizer.customize(CordaKryo(CordaClassResolver(context)), publicKeySerializer).apply { classLoader = it.second } - }.build() - } + KryoPool.Builder { + val serializer = Fiber.getFiberSerializer(false) as KryoSerializer + val classResolver = CordaClassResolver(context).apply { setKryo(serializer.kryo) } + // TODO The ClassResolver can only be set in the Kryo constructor and Quasar doesn't provide us with a way of doing that + val field = Kryo::class.java.getDeclaredField("classResolver").apply { isAccessible = true } + serializer.kryo.apply { + field.set(this, classResolver) + // don't allow overriding the public key serializer for checkpointing + DefaultKryoCustomizer.customize(this) + addDefaultSerializer(AutoCloseable::class.java, AutoCloseableSerialisationDetector) + register(ClosureSerializer.Closure::class.java, CordaClosureSerializer) + classLoader = it.second + } + }.build() + } } - private fun <T : Any> SerializationContext.kryo(task: Kryo.() -> T): T { + private fun <T : Any> CheckpointSerializationContext.kryo(task: Kryo.() -> T): T { return getPool(this).run { kryo -> kryo.context.ensureCapacity(properties.size) properties.forEach { kryo.context.put(it.key, it.value) } @@ -83,7 +66,7 @@ abstract class AbstractKryoSerializationScheme : SerializationScheme { } } - override fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): T { + override fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: CheckpointSerializationContext): T { val dataBytes = kryoMagic.consume(byteSequence) ?: throw KryoException("Serialized bytes header does not match expected format.") return context.kryo { @@ -111,7 +94,7 @@ abstract class AbstractKryoSerializationScheme : SerializationScheme { } } - override fun <T : Any> serialize(obj: T, context: SerializationContext): SerializedBytes<T> { + override fun <T : Any> serialize(obj: T, context: CheckpointSerializationContext): SerializedBytes<T> { return context.kryo { SerializedBytes(kryoOutput { kryoMagic.writeTo(this) @@ -131,13 +114,11 @@ abstract class AbstractKryoSerializationScheme : SerializationScheme { } } -val KRYO_CHECKPOINT_CONTEXT = SerializationContextImpl( - kryoMagic, +val KRYO_CHECKPOINT_CONTEXT = CheckpointSerializationContextImpl( SerializationDefaults.javaClass.classLoader, QuasarWhitelist, emptyMap(), true, - SerializationContext.UseCase.Checkpoint, null, AlwaysAcceptEncodingWhitelist ) diff --git a/node/src/main/kotlin/net/corda/node/serialization/kryo/KryoServerSerializationScheme.kt b/node/src/main/kotlin/net/corda/node/serialization/kryo/KryoServerSerializationScheme.kt deleted file mode 100644 index 86a4226812..0000000000 --- a/node/src/main/kotlin/net/corda/node/serialization/kryo/KryoServerSerializationScheme.kt +++ /dev/null @@ -1,14 +0,0 @@ -package net.corda.node.serialization.kryo - -import com.esotericsoftware.kryo.pool.KryoPool -import net.corda.core.serialization.SerializationContext -import net.corda.serialization.internal.CordaSerializationMagic - -class KryoServerSerializationScheme : AbstractKryoSerializationScheme() { - override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean { - return magic == kryoMagic && target == SerializationContext.UseCase.Checkpoint - } - - override fun rpcClientKryoPool(context: SerializationContext): KryoPool = throw UnsupportedOperationException() - override fun rpcServerKryoPool(context: SerializationContext): KryoPool = throw UnsupportedOperationException() -} diff --git a/node/src/main/kotlin/net/corda/node/services/api/NodePropertiesStore.kt b/node/src/main/kotlin/net/corda/node/services/api/NodePropertiesStore.kt index a3c4b6ab4c..c19803fd3d 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/NodePropertiesStore.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/NodePropertiesStore.kt @@ -8,7 +8,7 @@ interface NodePropertiesStore { interface FlowsDrainingModeOperations { - fun setEnabled(enabled: Boolean) + fun setEnabled(enabled: Boolean, propagateChange: Boolean = true) fun isEnabled(): Boolean diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index e8b7aa8d15..8d5583544f 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -6,6 +6,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic import net.corda.core.flows.StateMachineRunId import net.corda.core.internal.FlowStateMachine +import net.corda.core.internal.concurrent.OpenFuture import net.corda.core.messaging.DataFeed import net.corda.core.messaging.StateMachineTransactionMapping import net.corda.core.node.NodeInfo @@ -24,10 +25,13 @@ import net.corda.node.services.network.NetworkMapUpdater import net.corda.node.services.persistence.AttachmentStorageInternal import net.corda.node.services.statemachine.ExternalEvent import net.corda.node.services.statemachine.FlowStateMachineImpl +import net.corda.node.utilities.NamedCacheFactory import net.corda.nodeapi.internal.persistence.CordaPersistence +import java.security.PublicKey + +interface NetworkMapCacheInternal : NetworkMapCache, NetworkMapCacheBase { + override val nodeReady: OpenFuture<Void?> -interface NetworkMapCacheInternal : NetworkMapCache, NetworkMapCacheBaseInternal -interface NetworkMapCacheBaseInternal : NetworkMapCacheBase { val allNodeHashes: List<SecureHash> fun getNodeByHash(nodeHash: SecureHash): NodeInfo? @@ -129,6 +133,7 @@ interface ServiceHubInternal : ServiceHub { } fun getFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>): InitiatedFlowFactory<*>? + val cacheFactory: NamedCacheFactory } interface FlowStarter { diff --git a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt index 051097ff3f..1b8d0507b8 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt @@ -3,16 +3,16 @@ package net.corda.node.services.config import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigParseOptions +import net.corda.cliutils.CordaSystemUtils import net.corda.core.identity.CordaX500Name import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.internal.exists import net.corda.nodeapi.internal.* -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier +import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.config.toProperties import net.corda.nodeapi.internal.crypto.X509KeyStore -import net.corda.nodeapi.internal.crypto.loadKeyStore -import net.corda.nodeapi.internal.crypto.save import org.slf4j.LoggerFactory import java.nio.file.Path @@ -68,22 +68,33 @@ object ConfigHelper { * the CA certs in Node resources. Then provision KeyStores into certificates folder under node path. */ // TODO Move this to KeyStoreConfigHelpers -fun NodeConfiguration.configureWithDevSSLCertificate() = configureDevKeyAndTrustStores(myLegalName) +fun NodeConfiguration.configureWithDevSSLCertificate() = p2pSslOptions.configureDevKeyAndTrustStores(myLegalName, signingCertificateStore, certificatesDirectory) // TODO Move this to KeyStoreConfigHelpers -fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) { +fun MutualSslConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name, signingCertificateStore: FileBasedCertificateStoreSupplier, certificatesDirectory: Path) { + + val specifiedTrustStore = trustStore.getOptional() + + val specifiedKeyStore = keyStore.getOptional() + val specifiedSigningStore = signingCertificateStore.getOptional() + + if (specifiedTrustStore != null && specifiedKeyStore != null && specifiedSigningStore != null) return certificatesDirectory.createDirectories() - if (!trustStoreFile.exists()) { - loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/$DEV_CA_TRUST_STORE_FILE"), DEV_CA_TRUST_STORE_PASS).save(trustStoreFile, trustStorePassword) + + if (specifiedTrustStore == null) { + loadDevCaTrustStore().copyTo(trustStore.get(true)) } - if (!sslKeystore.exists() || !nodeKeystore.exists()) { - val (nodeKeyStore) = createDevKeyStores(myLegalName) + + if (keyStore.getOptional() == null || signingCertificateStore.getOptional() == null) { + val signingKeyStore = FileBasedCertificateStoreSupplier(signingCertificateStore.path, signingCertificateStore.password).get(true).also { it.registerDevSigningCertificates(myLegalName) } + + FileBasedCertificateStoreSupplier(keyStore.path, keyStore.password).get(true).also { it.registerDevP2pCertificates(myLegalName) } // Move distributed service composite key (generated by IdentityGenerator.generateToDisk) to keystore if exists. val distributedServiceKeystore = certificatesDirectory / "distributedService.jks" if (distributedServiceKeystore.exists()) { val serviceKeystore = X509KeyStore.fromFile(distributedServiceKeystore, DEV_CA_KEY_STORE_PASS) - nodeKeyStore.update { + signingKeyStore.update { serviceKeystore.aliases().forEach { if (serviceKeystore.internal.isKeyEntry(it)) { setPrivateKey(it, serviceKeystore.getPrivateKey(it, DEV_CA_PRIVATE_KEY_PASS), serviceKeystore.getCertificateChain(it)) @@ -95,15 +106,3 @@ fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) { } } } - -/** This is generally covered by commons-lang. */ -object CordaSystemUtils { - const val OS_NAME = "os.name" - - const val MAC_PREFIX = "Mac" - const val WIN_PREFIX = "Windows" - - fun isOsMac() = getOsName().startsWith(MAC_PREFIX) - fun isOsWindows() = getOsName().startsWith(WIN_PREFIX) - fun getOsName() = System.getProperty(OS_NAME) -} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index c095fbadf3..1d7f89f7bc 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -11,7 +11,9 @@ import net.corda.core.utilities.loggerFor import net.corda.core.utilities.seconds import net.corda.node.services.config.rpc.NodeRpcOptions import net.corda.nodeapi.BrokerRpcSslOptions -import net.corda.nodeapi.internal.config.NodeSSLConfiguration +import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier +import net.corda.nodeapi.internal.config.SslConfiguration +import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.parseAs @@ -30,7 +32,7 @@ private val DEFAULT_FLOW_MONITOR_PERIOD_MILLIS: Duration = Duration.ofMinutes(1) private val DEFAULT_FLOW_MONITOR_SUSPENSION_LOGGING_THRESHOLD_MILLIS: Duration = Duration.ofMinutes(1) private const val CORDAPPS_DIR_NAME_DEFAULT = "cordapps" -interface NodeConfiguration : NodeSSLConfiguration { +interface NodeConfiguration { val myLegalName: CordaX500Name val emailAddress: String val jmxMonitoringHttpPort: Int? @@ -48,6 +50,7 @@ interface NodeConfiguration : NodeSSLConfiguration { val notary: NotaryConfig? val additionalNodeInfoPollingFrequencyMsec: Long val p2pAddress: NetworkHostAndPort + val additionalP2PAddresses: List<NetworkHostAndPort> val rpcOptions: NodeRpcOptions val messagingServerAddress: NetworkHostAndPort? val messagingServerExternal: Boolean @@ -69,9 +72,16 @@ interface NodeConfiguration : NodeSSLConfiguration { val effectiveH2Settings: NodeH2Settings? val flowMonitorPeriodMillis: Duration get() = DEFAULT_FLOW_MONITOR_PERIOD_MILLIS val flowMonitorSuspensionLoggingThresholdMillis: Duration get() = DEFAULT_FLOW_MONITOR_SUSPENSION_LOGGING_THRESHOLD_MILLIS - val cordappDirectories: List<Path> get() = listOf(baseDirectory / CORDAPPS_DIR_NAME_DEFAULT) + val crlCheckSoftFail: Boolean val jmxReporterType : JmxReporterType? get() = defaultJmxReporterType + val baseDirectory: Path + val certificatesDirectory: Path + val signingCertificateStore: FileBasedCertificateStoreSupplier + val p2pSslOptions: MutualSslConfiguration + + val cordappDirectories: List<Path> + fun validate(): List<String> companion object { @@ -176,8 +186,8 @@ data class NodeConfigurationImpl( override val myLegalName: CordaX500Name, override val jmxMonitoringHttpPort: Int? = null, override val emailAddress: String, - override val keyStorePassword: String, - override val trustStorePassword: String, + private val keyStorePassword: String, + private val trustStorePassword: String, override val crlCheckSoftFail: Boolean, override val dataSourceProperties: Properties, override val compatibilityZoneURL: URL? = null, @@ -189,6 +199,7 @@ data class NodeConfigurationImpl( override val verifierType: VerifierType, override val flowTimeout: FlowTimeoutConfiguration, override val p2pAddress: NetworkHostAndPort, + override val additionalP2PAddresses: List<NetworkHostAndPort> = emptyList(), private val rpcAddress: NetworkHostAndPort? = null, private val rpcSettings: NodeRpcSettings, override val messagingServerAddress: NetworkHostAndPort?, @@ -243,6 +254,17 @@ data class NodeConfigurationImpl( } } + override val certificatesDirectory = baseDirectory / "certificates" + + private val signingCertificateStorePath = certificatesDirectory / "nodekeystore.jks" + override val signingCertificateStore = FileBasedCertificateStoreSupplier(signingCertificateStorePath, keyStorePassword) + + private val p2pKeystorePath: Path get() = certificatesDirectory / "sslkeystore.jks" + private val p2pKeyStore = FileBasedCertificateStoreSupplier(p2pKeystorePath, keyStorePassword) + private val p2pTrustStoreFilePath: Path get() = certificatesDirectory / "truststore.jks" + private val p2pTrustStore = FileBasedCertificateStoreSupplier(p2pTrustStoreFilePath, trustStorePassword) + override val p2pSslOptions: MutualSslConfiguration = SslConfiguration.mutual(p2pKeyStore, p2pTrustStore) + override val rpcOptions: NodeRpcOptions get() { return actualRpcSettings.asOptions() @@ -356,8 +378,6 @@ data class NodeConfigurationImpl( } } - - data class NodeRpcSettings( val address: NetworkHostAndPort?, val adminAddress: NetworkHostAndPort?, diff --git a/node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt b/node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt index 5a0eaa4ac9..3138a29934 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/shell/ShellConfig.kt @@ -1,6 +1,7 @@ package net.corda.node.services.config.shell import net.corda.core.internal.div +import net.corda.node.internal.clientSslOptionsCompatibleWith import net.corda.node.services.config.NodeConfiguration import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_SHELL_USER import net.corda.tools.shell.ShellConfiguration @@ -14,8 +15,8 @@ fun NodeConfiguration.toShellConfig() = ShellConfiguration( cordappsDirectory = this.baseDirectory.toString() / CORDAPPS_DIR, user = INTERNAL_SHELL_USER, password = INTERNAL_SHELL_USER, - hostAndPort = this.rpcOptions.adminAddress, - nodeSslConfig = this, + hostAndPort = this.rpcOptions.address, + ssl = clientSslOptionsCompatibleWith(this.rpcOptions), sshdPort = this.sshd?.port, sshHostKeyDirectory = this.baseDirectory / SSHD_HOSTKEY_DIR, noLocalShell = this.noLocalShell) diff --git a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt index 8ed683b8bf..46f29b1ffd 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt @@ -10,6 +10,7 @@ import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.node.services.api.IdentityServiceInternal import net.corda.node.utilities.AppendOnlyPersistentMap +import net.corda.node.utilities.NamedCacheFactory import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.crypto.x509Certificates import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -29,13 +30,14 @@ import javax.persistence.Lob * cached for efficient lookup. */ @ThreadSafe -class PersistentIdentityService : SingletonSerializeAsToken(), IdentityServiceInternal { +class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSerializeAsToken(), IdentityServiceInternal { companion object { private val log = contextLogger() - fun createPKMap(): AppendOnlyPersistentMap<SecureHash, PartyAndCertificate, PersistentIdentity, String> { + fun createPKMap(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap<SecureHash, PartyAndCertificate, PersistentIdentity, String> { return AppendOnlyPersistentMap( - "PersistentIdentityService_partyByKey", + cacheFactory = cacheFactory, + name = "PersistentIdentityService_partyByKey", toPersistentEntityKey = { it.toString() }, fromPersistentEntity = { Pair( @@ -50,9 +52,10 @@ class PersistentIdentityService : SingletonSerializeAsToken(), IdentityServiceIn ) } - fun createX500Map(): AppendOnlyPersistentMap<CordaX500Name, SecureHash, PersistentIdentityNames, String> { + fun createX500Map(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap<CordaX500Name, SecureHash, PersistentIdentityNames, String> { return AppendOnlyPersistentMap( - "PersistentIdentityService_partyByName", + cacheFactory = cacheFactory, + name = "PersistentIdentityService_partyByName", toPersistentEntityKey = { it.toString() }, fromPersistentEntity = { Pair(CordaX500Name.parse(it.name), SecureHash.parse(it.publicKeyHash)) }, toPersistentEntity = { key: CordaX500Name, value: SecureHash -> @@ -101,8 +104,8 @@ class PersistentIdentityService : SingletonSerializeAsToken(), IdentityServiceIn // CordaPersistence is not a c'tor parameter to work around the cyclic dependency lateinit var database: CordaPersistence - private val keyToParties = createPKMap() - private val principalToParties = createX500Map() + private val keyToParties = createPKMap(cacheFactory) + private val principalToParties = createX500Map(cacheFactory) fun start(trustRoot: X509Certificate, caCertificates: List<X509Certificate> = emptyList()) { _trustRoot = trustRoot diff --git a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt index fe9107a5b1..835a751c73 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt @@ -6,6 +6,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.MAX_HASH_HEX_SIZE import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.utilities.AppendOnlyPersistentMap +import net.corda.node.utilities.NamedCacheFactory import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import org.apache.commons.lang.ArrayUtils.EMPTY_BYTE_ARRAY @@ -25,12 +26,11 @@ import javax.persistence.Lob * * This class needs database transactions to be in-flight during method calls and init. */ -class PersistentKeyManagementService(val identityService: PersistentIdentityService, +class PersistentKeyManagementService(cacheFactory: NamedCacheFactory, val identityService: PersistentIdentityService, private val database: CordaPersistence) : SingletonSerializeAsToken(), KeyManagementServiceInternal { @Entity @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}our_key_pairs") class PersistentKey( - @Id @Column(name = "public_key_hash", length = MAX_HASH_HEX_SIZE, nullable = false) var publicKeyHash: String, @@ -47,12 +47,15 @@ class PersistentKeyManagementService(val identityService: PersistentIdentityServ } private companion object { - fun createKeyMap(): AppendOnlyPersistentMap<PublicKey, PrivateKey, PersistentKey, String> { + fun createKeyMap(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap<PublicKey, PrivateKey, PersistentKey, String> { return AppendOnlyPersistentMap( - "PersistentKeyManagementService_keys", + cacheFactory = cacheFactory, + name = "PersistentKeyManagementService_keys", toPersistentEntityKey = { it.toStringShort() }, - fromPersistentEntity = { Pair(Crypto.decodePublicKey(it.publicKey), Crypto.decodePrivateKey( - it.privateKey)) }, + fromPersistentEntity = { + Pair(Crypto.decodePublicKey(it.publicKey), + Crypto.decodePrivateKey(it.privateKey)) + }, toPersistentEntity = { key: PublicKey, value: PrivateKey -> PersistentKey(key, value) }, @@ -61,7 +64,7 @@ class PersistentKeyManagementService(val identityService: PersistentIdentityServ } } - private val keysMap = createKeyMap() + private val keysMap = createKeyMap(cacheFactory) override fun start(initialKeyPairs: Set<KeyPair>) { initialKeyPairs.forEach { keysMap.addWithDuplicatesAllowed(it.public, it.private) } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index 170dfd1b37..2014ca46c2 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -1,6 +1,5 @@ package net.corda.node.services.messaging -import io.netty.channel.unix.Errors import net.corda.core.internal.ThreadBox import net.corda.core.internal.div import net.corda.core.serialization.SingletonSerializeAsToken @@ -12,13 +11,13 @@ import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.NODE_P2P_ import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.PEER_ROLE import net.corda.core.internal.errors.AddressBindingException import net.corda.node.services.config.NodeConfiguration -import net.corda.nodeapi.ArtemisTcpTransport.Companion.p2pAcceptorTcpTransport import net.corda.nodeapi.internal.AmqpMessageSizeChecksInterceptor import net.corda.nodeapi.internal.ArtemisMessageSizeChecksInterceptor import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.JOURNAL_HEADER_SIZE import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.p2pAcceptorTcpTransport import net.corda.nodeapi.internal.requireOnDefaultFileSystem import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl @@ -120,7 +119,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, bindingsDirectory = (artemisDir / "bindings").toString() journalDirectory = (artemisDir / "journal").toString() largeMessagesDirectory = (artemisDir / "large-messages").toString() - acceptorConfigurations = mutableSetOf(p2pAcceptorTcpTransport(NetworkHostAndPort(messagingServerAddress.host, messagingServerAddress.port), config)) + acceptorConfigurations = mutableSetOf(p2pAcceptorTcpTransport(NetworkHostAndPort(messagingServerAddress.host, messagingServerAddress.port), config.p2pSslOptions)) // Enable built in message deduplication. Note we still have to do our own as the delayed commits // and our own definition of commit mean that the built in deduplication cannot remove all duplicates. idCacheSize = 2000 // Artemis Default duplicate cache size i.e. a guess @@ -162,8 +161,8 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, @Throws(IOException::class, KeyStoreException::class) private fun createArtemisSecurityManager(): ActiveMQJAASSecurityManager { - val keyStore = config.loadSslKeyStore().internal - val trustStore = config.loadTrustStore().internal + val keyStore = config.p2pSslOptions.keyStore.get().value.internal + val trustStore = config.p2pSslOptions.trustStore.get().value.internal val securityConfig = object : SecurityConfiguration() { // Override to make it work with our login module diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/InternalRPCMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/InternalRPCMessagingClient.kt index 9b5191b312..a484261ba5 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/InternalRPCMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/InternalRPCMessagingClient.kt @@ -6,9 +6,9 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.internal.security.RPCSecurityManager -import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_RPC_USER -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.ArtemisTcpTransport +import net.corda.nodeapi.internal.config.MutualSslConfiguration import org.apache.activemq.artemis.api.core.client.ActiveMQClient import org.apache.activemq.artemis.api.core.client.ServerLocator import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl @@ -16,7 +16,7 @@ import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl /** * Used by the Node to communicate with the RPC broker. */ -class InternalRPCMessagingClient(val sslConfig: SSLConfiguration, val serverAddress: NetworkHostAndPort, val maxMessageSize: Int, val nodeName: CordaX500Name, val rpcServerConfiguration: RPCServerConfiguration) : SingletonSerializeAsToken(), AutoCloseable { +class InternalRPCMessagingClient(val sslConfig: MutualSslConfiguration, val serverAddress: NetworkHostAndPort, val maxMessageSize: Int, val nodeName: CordaX500Name, val rpcServerConfiguration: RPCServerConfiguration) : SingletonSerializeAsToken(), AutoCloseable { private var locator: ServerLocator? = null private var rpcServer: RPCServer? = null diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessageDeduplicator.kt b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessageDeduplicator.kt index fcf89d2177..b8c29d8955 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessageDeduplicator.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessageDeduplicator.kt @@ -4,6 +4,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name import net.corda.node.services.statemachine.DeduplicationId import net.corda.node.utilities.AppendOnlyPersistentMap +import net.corda.node.utilities.NamedCacheFactory import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import java.time.Instant @@ -15,17 +16,18 @@ import javax.persistence.Id /** * Encapsulate the de-duplication logic. */ -class P2PMessageDeduplicator(private val database: CordaPersistence) { +class P2PMessageDeduplicator(cacheFactory: NamedCacheFactory, private val database: CordaPersistence) { // A temporary in-memory set of deduplication IDs and associated high water mark details. // When we receive a message we don't persist the ID immediately, // so we store the ID here in the meantime (until the persisting db tx has committed). This is because Artemis may // redeliver messages to the same consumer if they weren't ACKed. private val beingProcessedMessages = ConcurrentHashMap<DeduplicationId, MessageMeta>() - private val processedMessages = createProcessedMessages() + private val processedMessages = createProcessedMessages(cacheFactory) - private fun createProcessedMessages(): AppendOnlyPersistentMap<DeduplicationId, MessageMeta, ProcessedMessage, String> { + private fun createProcessedMessages(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap<DeduplicationId, MessageMeta, ProcessedMessage, String> { return AppendOnlyPersistentMap( - "P2PMessageDeduplicator_processedMessages", + cacheFactory = cacheFactory, + name = "P2PMessageDeduplicator_processedMessages", toPersistentEntityKey = { it.toString }, fromPersistentEntity = { Pair(DeduplicationId(it.id), MessageMeta(it.insertionTime, it.hash, it.seqNo)) }, toPersistentEntity = { key: DeduplicationId, value: MessageMeta -> diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt index 44667c43b8..13b96c61b0 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt @@ -1,6 +1,7 @@ package net.corda.node.services.messaging import co.paralleluniverse.fibers.Suspendable +import com.codahale.metrics.MetricRegistry import net.corda.core.crypto.toStringShort import net.corda.core.identity.CordaX500Name import net.corda.core.internal.ThreadBox @@ -26,7 +27,7 @@ import net.corda.node.services.statemachine.DeduplicationId import net.corda.node.services.statemachine.ExternalEvent import net.corda.node.services.statemachine.SenderDeduplicationId import net.corda.node.utilities.AffinityExecutor -import net.corda.nodeapi.ArtemisTcpTransport.Companion.p2pConnectorTcpTransport +import net.corda.node.utilities.NamedCacheFactory import net.corda.nodeapi.internal.ArtemisMessagingComponent import net.corda.nodeapi.internal.ArtemisMessagingComponent.* import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_CONTROL @@ -34,6 +35,7 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_NOT import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.JOURNAL_HEADER_SIZE import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2PMessagingHeaders import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.p2pConnectorTcpTransport import net.corda.nodeapi.internal.bridging.BridgeControl import net.corda.nodeapi.internal.bridging.BridgeEntry import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -81,6 +83,9 @@ class P2PMessagingClient(val config: NodeConfiguration, private val nodeExecutor: AffinityExecutor.ServiceAffinityExecutor, private val database: CordaPersistence, private val networkMap: NetworkMapCacheInternal, + @Suppress("UNUSED") + private val metricRegistry: MetricRegistry, + cacheFactory: NamedCacheFactory, private val isDrainingModeOn: () -> Boolean, private val drainingModeWasChangedEvents: Observable<Pair<Boolean, Boolean>> ) : SingletonSerializeAsToken(), MessagingService, AddressToArtemisQueueResolver { @@ -120,7 +125,7 @@ class P2PMessagingClient(val config: NodeConfiguration, private lateinit var advertisedAddress: NetworkHostAndPort private var maxMessageSize: Int = -1 - override val myAddress: SingleMessageRecipient get() = NodeAddress(myIdentity, advertisedAddress) + override val myAddress: SingleMessageRecipient get() = NodeAddress(myIdentity) override val ourSenderUUID = UUID.randomUUID().toString() private val state = ThreadBox(InnerState()) @@ -129,7 +134,7 @@ class P2PMessagingClient(val config: NodeConfiguration, private val handlers = ConcurrentHashMap<String, MessageHandler>() - private val deduplicator = P2PMessageDeduplicator(database) + private val deduplicator = P2PMessageDeduplicator(cacheFactory, database) internal var messagingExecutor: MessagingExecutor? = null /** @@ -149,7 +154,7 @@ class P2PMessagingClient(val config: NodeConfiguration, started = true log.info("Connecting to message broker: $serverAddress") // TODO Add broker CN to config for host verification in case the embedded broker isn't used - val tcpTransport = p2pConnectorTcpTransport(serverAddress, config) + val tcpTransport = p2pConnectorTcpTransport(serverAddress, config.p2pSslOptions) locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply { // Never time out on our loopback Artemis connections. If we switch back to using the InVM transport this // would be the default and the two lines below can be deleted. @@ -233,7 +238,7 @@ class P2PMessagingClient(val config: NodeConfiguration, fun gatherAddresses(node: NodeInfo): Sequence<BridgeEntry> { return state.locked { node.legalIdentitiesAndCerts.map { - val messagingAddress = NodeAddress(it.party.owningKey, node.addresses.first()) + val messagingAddress = NodeAddress(it.party.owningKey) BridgeEntry(messagingAddress.queueName, node.addresses, node.legalIdentities.map { it.name }) }.filter { producerSession!!.queueQuery(SimpleString(it.queueName)).isExists }.asSequence() } @@ -242,14 +247,14 @@ class P2PMessagingClient(val config: NodeConfiguration, fun deployBridges(node: NodeInfo) { gatherAddresses(node) .forEach { - sendBridgeControl(BridgeControl.Create(myIdentity.toStringShort(), it)) + sendBridgeControl(BridgeControl.Create(config.myLegalName.toString(), it)) } } fun destroyBridges(node: NodeInfo) { gatherAddresses(node) .forEach { - sendBridgeControl(BridgeControl.Delete(myIdentity.toStringShort(), it)) + sendBridgeControl(BridgeControl.Delete(config.myLegalName.toString(), it)) } } @@ -289,7 +294,7 @@ class P2PMessagingClient(val config: NodeConfiguration, delayStartQueues += queue.toString() } } - val startupMessage = BridgeControl.NodeToBridgeSnapshot(myIdentity.toStringShort(), inboxes, requiredBridges) + val startupMessage = BridgeControl.NodeToBridgeSnapshot(config.myLegalName.toString(), inboxes, requiredBridges) sendBridgeControl(startupMessage) } @@ -310,12 +315,9 @@ class P2PMessagingClient(val config: NodeConfiguration, return } eventsSubscription = p2pConsumer!!.messages - .doOnError { error -> throw error } - .doOnNext { message -> deliver(message) } // this `run()` method is semantically meant to block until the message consumption runs, hence the latch here .doOnCompleted(latch::countDown) - .doOnError { error -> throw error } - .subscribe() + .subscribe({ message -> deliver(message) }, { error -> throw error }) p2pConsumer!! } consumer.start() @@ -495,7 +497,7 @@ class P2PMessagingClient(val config: NodeConfiguration, val peers = networkMap.getNodesByOwningKeyIndex(keyHash) for (node in peers) { val bridge = BridgeEntry(queueName, node.addresses, node.legalIdentities.map { it.name }) - val createBridgeMessage = BridgeControl.Create(myIdentity.toStringShort(), bridge) + val createBridgeMessage = BridgeControl.Create(config.myLegalName.toString(), bridge) sendBridgeControl(createBridgeMessage) } } @@ -540,7 +542,7 @@ class P2PMessagingClient(val config: NodeConfiguration, override fun getAddressOfParty(partyInfo: PartyInfo): MessageRecipients { return when (partyInfo) { - is PartyInfo.SingleNode -> NodeAddress(partyInfo.party.owningKey, partyInfo.addresses.single()) + is PartyInfo.SingleNode -> NodeAddress(partyInfo.party.owningKey) is PartyInfo.DistributedNode -> ServiceAddress(partyInfo.party.owningKey) } } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt index f47f97dc7b..7c9ca38e93 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt @@ -336,6 +336,7 @@ class RPCServer( context.invocation.pushToLoggingContext() when (arguments) { is Try.Success -> { + log.debug { "Arguments: ${arguments.value.toTypedArray().contentDeepToString()}" } rpcExecutor!!.submit { val result = invokeRpc(context, clientToServer.methodName, arguments.value) sendReply(clientToServer.replyId, clientToServer.clientAddress, result) diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapCacheImpl.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapCacheImpl.kt deleted file mode 100644 index c31f089a07..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapCacheImpl.kt +++ /dev/null @@ -1,51 +0,0 @@ -package net.corda.node.services.network - -import net.corda.core.identity.AbstractParty -import net.corda.core.node.NodeInfo -import net.corda.core.node.services.IdentityService -import net.corda.core.node.services.NetworkMapCache -import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.utilities.contextLogger -import net.corda.node.services.api.NetworkMapCacheBaseInternal -import net.corda.node.services.api.NetworkMapCacheInternal -import net.corda.nodeapi.internal.persistence.CordaPersistence - -class NetworkMapCacheImpl( - private val networkMapCacheBase: NetworkMapCacheBaseInternal, - private val identityService: IdentityService, - private val database: CordaPersistence -) : NetworkMapCacheBaseInternal by networkMapCacheBase, NetworkMapCacheInternal, SingletonSerializeAsToken() { - companion object { - private val logger = contextLogger() - } - - fun start() { - for (nodeInfo in networkMapCacheBase.allNodes) { - for (identity in nodeInfo.legalIdentitiesAndCerts) { - identityService.verifyAndRegisterIdentity(identity) - } - } - networkMapCacheBase.changed.subscribe { mapChange -> - // TODO how should we handle network map removal - if (mapChange is NetworkMapCache.MapChange.Added) { - mapChange.node.legalIdentitiesAndCerts.forEach { - try { - identityService.verifyAndRegisterIdentity(it) - } catch (ignore: Exception) { - // Log a warning to indicate node info is not added to the network map cache. - logger.warn("Node info for :'${it.name}' is not added to the network map due to verification error.") - } - } - } - } - } - - override fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? { - return database.transaction { - val wellKnownParty = identityService.wellKnownPartyFromAnonymous(party) - wellKnownParty?.let { - getNodesByLegalIdentityKey(it.owningKey).firstOrNull() - } - } - } -} diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt index 69a2af2cd2..91a0e159c6 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt @@ -11,6 +11,7 @@ import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger import net.corda.core.utilities.seconds import net.corda.core.utilities.trace +import net.corda.node.VersionInfo import net.corda.node.utilities.registration.cacheControl import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.network.NetworkMap @@ -23,7 +24,7 @@ import java.security.cert.X509Certificate import java.time.Duration import java.util.* -class NetworkMapClient(compatibilityZoneURL: URL) { +class NetworkMapClient(compatibilityZoneURL: URL, private val versionInfo: VersionInfo) { companion object { private val logger = contextLogger() } @@ -38,14 +39,18 @@ class NetworkMapClient(compatibilityZoneURL: URL) { fun publish(signedNodeInfo: SignedNodeInfo) { val publishURL = URL("$networkMapUrl/publish") logger.trace { "Publishing NodeInfo to $publishURL." } - publishURL.post(signedNodeInfo.serialize()) + publishURL.post(signedNodeInfo.serialize(), + "Platform-Version" to "${versionInfo.platformVersion}", + "Client-Version" to versionInfo.releaseVersion) logger.trace { "Published NodeInfo to $publishURL successfully." } } fun ackNetworkParametersUpdate(signedParametersHash: SignedData<SecureHash>) { val ackURL = URL("$networkMapUrl/ack-parameters") logger.trace { "Sending network parameters with hash ${signedParametersHash.raw.deserialize()} approval to $ackURL." } - ackURL.post(signedParametersHash.serialize()) + ackURL.post(signedParametersHash.serialize(), + "Platform-Version" to "${versionInfo.platformVersion}", + "Client-Version" to versionInfo.releaseVersion) logger.trace { "Sent network parameters approval to $ackURL successfully." } } 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 06ef292b0d..9194b413e2 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 @@ -24,13 +24,12 @@ import java.nio.file.StandardCopyOption import java.security.cert.X509Certificate import java.time.Duration import java.util.* -import java.util.concurrent.Executors import java.util.concurrent.ScheduledThreadPoolExecutor import java.util.concurrent.TimeUnit import kotlin.system.exitProcess class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, - private val fileWatcher: NodeInfoWatcher, + private val nodeInfoWatcher: NodeInfoWatcher, private val networkMapClient: NetworkMapClient?, private val baseDirectory: Path, private val extraNetworkMapKeys: List<UUID> @@ -40,8 +39,10 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, private val defaultRetryInterval = 1.minutes } - private val parametersUpdatesTrack: PublishSubject<ParametersUpdateInfo> = PublishSubject.create<ParametersUpdateInfo>() - private val executor = ScheduledThreadPoolExecutor(1, NamedThreadFactory("Network Map Updater Thread", Executors.defaultThreadFactory())) + private val parametersUpdatesTrack = PublishSubject.create<ParametersUpdateInfo>() + private val networkMapPoller = ScheduledThreadPoolExecutor(1, NamedThreadFactory("Network Map Updater Thread")).apply { + executeExistingDelayedTasksAfterShutdownPolicy = false + } private var newNetworkParameters: Pair<ParametersUpdate, SignedNetworkParameters>? = null private var fileWatcherSubscription: Subscription? = null private lateinit var trustRoot: X509Certificate @@ -50,7 +51,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, override fun close() { fileWatcherSubscription?.unsubscribe() - MoreExecutors.shutdownAndAwaitTermination(executor, 50, TimeUnit.SECONDS) + MoreExecutors.shutdownAndAwaitTermination(networkMapPoller, 50, TimeUnit.SECONDS) } fun start(trustRoot: X509Certificate, currentParametersHash: SecureHash, ourNodeInfoHash: SecureHash) { @@ -58,26 +59,38 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, this.trustRoot = trustRoot this.currentParametersHash = currentParametersHash this.ourNodeInfoHash = ourNodeInfoHash - // Subscribe to file based networkMap - fileWatcherSubscription = fileWatcher.nodeInfoUpdates().subscribe { - when (it) { - is NodeInfoUpdate.Add -> { - networkMapCache.addNode(it.nodeInfo) - } - is NodeInfoUpdate.Remove -> { - if (it.hash != ourNodeInfoHash) { - val nodeInfo = networkMapCache.getNodeByHash(it.hash) - nodeInfo?.let { networkMapCache.removeNode(it) } + watchForNodeInfoFiles() + if (networkMapClient != null) { + watchHttpNetworkMap() + } + } + + private fun watchForNodeInfoFiles() { + nodeInfoWatcher + .nodeInfoUpdates() + .subscribe { + for (update in it) { + when (update) { + is NodeInfoUpdate.Add -> networkMapCache.addNode(update.nodeInfo) + is NodeInfoUpdate.Remove -> { + if (update.hash != ourNodeInfoHash) { + val nodeInfo = networkMapCache.getNodeByHash(update.hash) + nodeInfo?.let(networkMapCache::removeNode) + } + } + } + } + if (networkMapClient == null) { + // Mark the network map cache as ready on a successful poll of the node infos dir if not using + // the HTTP network map even if there aren't any node infos + networkMapCache.nodeReady.set(null) } } - } - } + } - if (networkMapClient == null) return - - // Subscribe to remote network map if configured. - executor.executeExistingDelayedTasksAfterShutdownPolicy = false - executor.submit(object : Runnable { + private fun watchHttpNetworkMap() { + // The check may be expensive, so always run it in the background even the first time. + networkMapPoller.submit(object : Runnable { override fun run() { val nextScheduleDelay = try { updateNetworkMapCache() @@ -86,9 +99,9 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, defaultRetryInterval } // Schedule the next update. - executor.schedule(this, nextScheduleDelay.toMillis(), TimeUnit.MILLISECONDS) + networkMapPoller.schedule(this, nextScheduleDelay.toMillis(), TimeUnit.MILLISECONDS) } - }) // The check may be expensive, so always run it in the background even the first time. + }) } fun trackParametersUpdate(): DataFeed<ParametersUpdateInfo?, ParametersUpdateInfo> { @@ -99,9 +112,13 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, } fun updateNetworkMapCache(): Duration { - if (networkMapClient == null) throw CordaRuntimeException("Network map cache can be updated only if network map/compatibility zone URL is specified") + if (networkMapClient == null) { + throw CordaRuntimeException("Network map cache can be updated only if network map/compatibility zone URL is specified") + } + val (globalNetworkMap, cacheTimeout) = networkMapClient.getNetworkMap() globalNetworkMap.parametersUpdate?.let { handleUpdateNetworkParameters(networkMapClient, it) } + val additionalHashes = extraNetworkMapKeys.flatMap { try { networkMapClient.getNetworkMap(it).payload.nodeInfoHashes @@ -111,6 +128,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, emptyList<SecureHash>() } } + val allHashesFromNetworkMap = (globalNetworkMap.nodeInfoHashes + additionalHashes).toSet() if (currentParametersHash != globalNetworkMap.networkParameterHash) { @@ -120,12 +138,9 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, val currentNodeHashes = networkMapCache.allNodeHashes // Remove node info from network map. - (currentNodeHashes - allHashesFromNetworkMap - fileWatcher.processedNodeInfoHashes) - .mapNotNull { - if (it != ourNodeInfoHash) { - networkMapCache.getNodeByHash(it) - } else null - }.forEach(networkMapCache::removeNode) + (currentNodeHashes - allHashesFromNetworkMap - nodeInfoWatcher.processedNodeInfoHashes) + .mapNotNull { if (it != ourNodeInfoHash) networkMapCache.getNodeByHash(it) else null } + .forEach(networkMapCache::removeNode) (allHashesFromNetworkMap - currentNodeHashes).mapNotNull { // Download new node info from network map @@ -141,6 +156,10 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, networkMapCache.addNode(it) } + // Mark the network map cache as ready on a successful poll of the HTTP network map, even on the odd chance that + // it's empty + networkMapCache.nodeReady.set(null) + return cacheTimeout } diff --git a/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt b/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt index cdd370ff8f..814e193b25 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt @@ -3,23 +3,16 @@ package net.corda.node.services.network import net.corda.core.crypto.SecureHash import net.corda.core.internal.* import net.corda.core.node.NodeInfo -import net.corda.core.serialization.internal.SerializationEnvironmentImpl -import net.corda.core.serialization.internal._contextSerializationEnv import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.core.utilities.seconds -import net.corda.node.serialization.amqp.AMQPServerSerializationScheme import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY import net.corda.nodeapi.internal.NodeInfoAndSigned -import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.network.NodeInfoFilesCopier -import net.corda.serialization.internal.AMQP_P2P_CONTEXT -import net.corda.serialization.internal.SerializationFactoryImpl import rx.Observable import rx.Scheduler import java.nio.file.Path -import java.nio.file.Paths import java.nio.file.StandardCopyOption.REPLACE_EXISTING import java.nio.file.attribute.FileTime import java.time.Duration @@ -63,7 +56,7 @@ class NodeInfoWatcher(private val nodePath: Path, } } - internal data class NodeInfoFromFile(val nodeInfohash: SecureHash, val lastModified: FileTime) + private data class NodeInfoFromFile(val nodeInfohash: SecureHash, val lastModified: FileTime) private val nodeInfosDir = nodePath / NODE_INFO_DIRECTORY private val nodeInfoFilesMap = HashMap<Path, NodeInfoFromFile>() @@ -75,20 +68,16 @@ class NodeInfoWatcher(private val nodePath: Path, } /** - * Read all the files contained in [nodePath] / [NODE_INFO_DIRECTORY] and keep watching - * the folder for further updates. + * Read all the files contained in [nodePath] / [NODE_INFO_DIRECTORY] and keep watching the folder for further updates. * - * We simply list the directory content every 5 seconds, the Java implementation of WatchService has been proven to - * be unreliable on MacOs and given the fairly simple use case we have, this simple implementation should do. - * - * @return an [Observable] returning [NodeInfoUpdate]s, at most one [NodeInfo] is returned for each processed file. + * @return an [Observable] that emits lists of [NodeInfoUpdate]s. Each emitted list is a poll event of the folder and + * may be empty if no changes were detected. */ - fun nodeInfoUpdates(): Observable<NodeInfoUpdate> { - return Observable.interval(0, pollInterval.toMillis(), TimeUnit.MILLISECONDS, scheduler) - .flatMapIterable { loadFromDirectory() } + fun nodeInfoUpdates(): Observable<List<NodeInfoUpdate>> { + return Observable.interval(0, pollInterval.toMillis(), TimeUnit.MILLISECONDS, scheduler).map { pollDirectory() } } - private fun loadFromDirectory(): List<NodeInfoUpdate> { + private fun pollDirectory(): List<NodeInfoUpdate> { val processedPaths = HashSet<Path>() val result = nodeInfosDir.list { paths -> paths @@ -122,14 +111,3 @@ class NodeInfoWatcher(private val nodePath: Path, return result.map { NodeInfoUpdate.Add(it.nodeInfo) } + removedHashes } } - -// TODO Remove this once we have a tool that can read AMQP serialised files -fun main(args: Array<String>) { - _contextSerializationEnv.set(SerializationEnvironmentImpl( - SerializationFactoryImpl().apply { - registerScheme(AMQPServerSerializationScheme()) - }, - AMQP_P2P_CONTEXT) - ) - println(Paths.get(args[0]).readObject<SignedNodeInfo>().verified()) -} diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt index c2fbf40dac..f873120c32 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt @@ -1,25 +1,29 @@ package net.corda.node.services.network -import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.SecureHash import net.corda.core.crypto.toStringShort +import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.bufferUntilSubscribed +import net.corda.core.internal.concurrent.OpenFuture import net.corda.core.internal.concurrent.openFuture import net.corda.core.messaging.DataFeed import net.corda.core.node.NodeInfo import net.corda.core.node.NotaryInfo +import net.corda.core.node.services.IdentityService import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.node.services.PartyInfo import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.serialize import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.Try import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.node.internal.schemas.NodeInfoSchemaV1 -import net.corda.node.services.api.NetworkMapCacheBaseInternal +import net.corda.node.services.api.NetworkMapCacheInternal +import net.corda.node.utilities.NamedCacheFactory import net.corda.node.utilities.NonInvalidatingCache import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.bufferUntilDatabaseCommit @@ -33,8 +37,9 @@ import javax.annotation.concurrent.ThreadSafe /** Database-based network map cache. */ @ThreadSafe -open class PersistentNetworkMapCache(private val database: CordaPersistence, - private val myLegalName: CordaX500Name) : SingletonSerializeAsToken(), NetworkMapCacheBaseInternal { +open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory, + private val database: CordaPersistence, + private val identityService: IdentityService) : NetworkMapCacheInternal, SingletonSerializeAsToken() { companion object { private val logger = contextLogger() } @@ -44,10 +49,8 @@ open class PersistentNetworkMapCache(private val database: CordaPersistence, override val changed: Observable<MapChange> = _changed.wrapWithDatabaseTransaction() private val changePublisher: rx.Observer<MapChange> get() = _changed.bufferUntilDatabaseCommit() - // TODO revisit the logic under which nodeReady and loadDBSuccess are set. - // with the NetworkMapService redesign their meaning is not too well defined. - private val _nodeReady = openFuture<Void?>() - override val nodeReady: CordaFuture<Void?> = _nodeReady + override val nodeReady: OpenFuture<Void?> = openFuture() + private lateinit var notaries: List<NotaryInfo> override val notaryIdentities: List<Party> get() = notaries.map { it.identity } @@ -67,14 +70,14 @@ open class PersistentNetworkMapCache(private val database: CordaPersistence, fun start(notaries: List<NotaryInfo>) { this.notaries = notaries - val otherNodeInfoCount = database.transaction { - session.createQuery( - "select count(*) from ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n join n.legalIdentitiesAndCerts i where i.name != :myLegalName") - .setParameter("myLegalName", myLegalName.toString()) - .singleResult as Long - } - if (otherNodeInfoCount > 0) { - _nodeReady.set(null) + } + + override fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? { + return database.transaction { + val wellKnownParty = identityService.wellKnownPartyFromAnonymous(party) + wellKnownParty?.let { + getNodesByLegalIdentityKey(it.owningKey).firstOrNull() + } } } @@ -123,8 +126,8 @@ open class PersistentNetworkMapCache(private val database: CordaPersistence, override fun getNodesByLegalIdentityKey(identityKey: PublicKey): List<NodeInfo> = nodesByKeyCache[identityKey]!! private val nodesByKeyCache = NonInvalidatingCache<PublicKey, List<NodeInfo>>( - "PersistentNetworkMap_nodesByKey", - 1024) { key -> + cacheFactory = cacheFactory, + name = "PersistentNetworkMap_nodesByKey") { key -> database.transaction { queryByIdentityKey(session, key) } } @@ -143,8 +146,8 @@ open class PersistentNetworkMapCache(private val database: CordaPersistence, } private val identityByLegalNameCache = NonInvalidatingCache<CordaX500Name, Optional<PartyAndCertificate>>( - "PersistentNetworkMap_idByLegalName", - 1024) { name -> + cacheFactory = cacheFactory, + name = "PersistentNetworkMap_idByLegalName") { name -> Optional.ofNullable(database.transaction { queryIdentityByLegalName(session, name) }) } @@ -160,6 +163,7 @@ open class PersistentNetworkMapCache(private val database: CordaPersistence, val previousNode = getNodesByLegalIdentityKey(node.legalIdentities.first().owningKey).firstOrNull() if (previousNode == null) { logger.info("No previous node found") + if (!verifyAndRegisterIdentities(node)) return database.transaction { updateInfoDB(node, session) changePublisher.onNext(MapChange.Added(node)) @@ -169,6 +173,8 @@ open class PersistentNetworkMapCache(private val database: CordaPersistence, return } else if (previousNode != node) { logger.info("Previous node was found as: $previousNode") + // TODO We should be adding any new identities as well + if (!verifyIdentities(node)) return database.transaction { updateInfoDB(node, session) changePublisher.onNext(MapChange.Modified(node, previousNode)) @@ -177,12 +183,30 @@ open class PersistentNetworkMapCache(private val database: CordaPersistence, logger.info("Previous node was identical to incoming one - doing nothing") } } - if (node.legalIdentities[0].name != myLegalName) { - _nodeReady.set(null) - } logger.debug { "Done adding node with info: $node" } } + private fun verifyIdentities(node: NodeInfo): Boolean { + val failures = node.legalIdentitiesAndCerts.mapNotNull { Try.on { it.verify(identityService.trustAnchor) } as? Try.Failure } + if (failures.isNotEmpty()) { + logger.warn("$node has ${failures.size} invalid identities:") + failures.forEach { logger.warn("", it) } + } + return failures.isEmpty() + } + + private fun verifyAndRegisterIdentities(node: NodeInfo): Boolean { + // First verify all the node's identities are valid before registering any of them + return if (verifyIdentities(node)) { + for (identity in node.legalIdentitiesAndCerts) { + identityService.verifyAndRegisterIdentity(identity) + } + true + } else { + false + } + } + override fun removeNode(node: NodeInfo) { logger.info("Removing node with info: $node") synchronized(_changed) { diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt index 85a2a5bc87..dd5d1c4998 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt @@ -65,7 +65,7 @@ class DBCheckpointStorage : CheckpointStorage { override fun getCheckpoint(id: StateMachineRunId): SerializedBytes<Checkpoint>? { val bytes = currentDBSession().get(DBCheckpoint::class.java, id.uuid.toString())?.checkpoint ?: return null - return SerializedBytes<Checkpoint>(bytes) + return SerializedBytes(bytes) } override fun getAllCheckpoints(): Stream<Pair<StateMachineRunId, SerializedBytes<Checkpoint>>> { @@ -78,8 +78,8 @@ class DBCheckpointStorage : CheckpointStorage { } } - override fun getCheckpointCount(connection: Connection): Long = - try { + override fun getCheckpointCount(connection: Connection): Long { + return try { connection.prepareStatement("select count(*) from node_checkpoints").use { ps -> ps.executeQuery().use { rs -> rs.next() @@ -90,5 +90,5 @@ class DBCheckpointStorage : CheckpointStorage { // Happens when the table was not created yet. 0L } - + } } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index 1fe72c50a9..034e91a2cf 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -15,6 +15,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.utilities.AppendOnlyPersistentMapBase +import net.corda.node.utilities.NamedCacheFactory import net.corda.node.utilities.WeightBasedAppendOnlyPersistentMap import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX @@ -31,7 +32,7 @@ typealias TxCacheValue = Pair<SerializedBytes<CoreTransaction>, List<Transaction fun TxCacheValue.toSignedTx() = SignedTransaction(this.first, this.second) fun SignedTransaction.toTxCacheValue() = TxCacheValue(this.txBits, this.sigs) -class DBTransactionStorage(cacheSizeBytes: Long, private val database: CordaPersistence) : WritableTransactionStorage, SingletonSerializeAsToken() { +class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: NamedCacheFactory) : WritableTransactionStorage, SingletonSerializeAsToken() { @Entity @Table(name = "${NODE_DATABASE_PREFIX}transactions") @@ -49,9 +50,10 @@ class DBTransactionStorage(cacheSizeBytes: Long, private val database: CordaPers ) private companion object { - fun createTransactionsMap(maxSizeInBytes: Long) + fun createTransactionsMap(cacheFactory: NamedCacheFactory) : AppendOnlyPersistentMapBase<SecureHash, TxCacheValue, DBTransaction, String> { return WeightBasedAppendOnlyPersistentMap<SecureHash, TxCacheValue, DBTransaction, String>( + cacheFactory = cacheFactory, name = "DBTransactionStorage_transactions", toPersistentEntityKey = { it.toString() }, fromPersistentEntity = { @@ -67,7 +69,6 @@ class DBTransactionStorage(cacheSizeBytes: Long, private val database: CordaPers } }, persistentEntityClass = DBTransaction::class.java, - maxWeight = maxSizeInBytes, weighingFunc = { hash, tx -> hash.size + weighTx(tx) } ) } @@ -86,7 +87,7 @@ class DBTransactionStorage(cacheSizeBytes: Long, private val database: CordaPers } } - private val txStorage = ThreadBox(createTransactionsMap(cacheSizeBytes)) + private val txStorage = ThreadBox(createTransactionsMap(cacheFactory)) override fun addTransaction(transaction: SignedTransaction): Boolean = database.transaction { txStorage.locked { diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index 78a62f2149..ca96ef1ad5 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -18,8 +18,8 @@ import net.corda.core.node.services.vault.AttachmentQueryCriteria import net.corda.core.node.services.vault.AttachmentSort import net.corda.core.serialization.* import net.corda.core.utilities.contextLogger -import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.vault.HibernateAttachmentQueryCriteriaParser +import net.corda.node.utilities.NamedCacheFactory import net.corda.node.utilities.NonInvalidatingCache import net.corda.node.utilities.NonInvalidatingWeightBasedCache import net.corda.nodeapi.exceptions.DuplicateAttachmentException @@ -43,9 +43,8 @@ import javax.persistence.* @ThreadSafe class NodeAttachmentService( metrics: MetricRegistry, - private val database: CordaPersistence, - attachmentContentCacheSize: Long = NodeConfiguration.defaultAttachmentContentCacheSize, - attachmentCacheBound: Long = NodeConfiguration.defaultAttachmentCacheBound + cacheFactory: NamedCacheFactory, + private val database: CordaPersistence ) : AttachmentStorageInternal, SingletonSerializeAsToken() { companion object { private val log = contextLogger() @@ -206,8 +205,8 @@ class NodeAttachmentService( // a problem somewhere else or this needs to be revisited. private val attachmentContentCache = NonInvalidatingWeightBasedCache( + cacheFactory = cacheFactory, name = "NodeAttachmentService_attachmentContent", - maxWeight = attachmentContentCacheSize, weigher = Weigher<SecureHash, Optional<Pair<Attachment, ByteArray>>> { key, value -> key.size + if (value.isPresent) value.get().second.size else 0 }, loadFunction = { Optional.ofNullable(loadAttachmentContent(it)) } ) @@ -228,10 +227,9 @@ class NodeAttachmentService( } private val attachmentCache = NonInvalidatingCache<SecureHash, Optional<Attachment>>( - "NodeAttachmentService_attachemnt", - attachmentCacheBound) { key -> - Optional.ofNullable(createAttachment(key)) - } + cacheFactory = cacheFactory, + name = "NodeAttachmentService_attachmentPresence", + loadFunction = { key -> Optional.ofNullable(createAttachment(key)) }) private fun createAttachment(key: SecureHash): Attachment? { val content = attachmentContentCache.get(key)!! diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodePropertiesPersistentStore.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodePropertiesPersistentStore.kt index ae10d94435..2c48dfd6f9 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodePropertiesPersistentStore.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodePropertiesPersistentStore.kt @@ -57,12 +57,13 @@ class FlowsDrainingModeOperationsImpl(readPhysicalNodeId: () -> String, private override val values = PublishSubject.create<Pair<Boolean, Boolean>>()!! - override fun setEnabled(enabled: Boolean) { - var oldValue: Boolean? = null - persistence.transaction { - oldValue = map.put(nodeSpecificFlowsExecutionModeKey, enabled.toString())?.toBoolean() ?: false + override fun setEnabled(enabled: Boolean, propagateChange: Boolean) { + val oldValue = persistence.transaction { + map.put(nodeSpecificFlowsExecutionModeKey, enabled.toString())?.toBoolean() ?: false + } + if (propagateChange) { + values.onNext(oldValue to enabled) } - values.onNext(oldValue!! to enabled) } override fun isEnabled(): Boolean { diff --git a/node/src/main/kotlin/net/corda/node/services/rpc/ArtemisRpcBroker.kt b/node/src/main/kotlin/net/corda/node/services/rpc/ArtemisRpcBroker.kt index 5fc3c98bc8..af6158e33a 100644 --- a/node/src/main/kotlin/net/corda/node/services/rpc/ArtemisRpcBroker.kt +++ b/node/src/main/kotlin/net/corda/node/services/rpc/ArtemisRpcBroker.kt @@ -8,7 +8,7 @@ import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.NODE_SECU import net.corda.node.internal.artemis.BrokerJaasLoginModule.Companion.RPC_SECURITY_CONFIG import net.corda.node.internal.security.RPCSecurityManager import net.corda.nodeapi.BrokerRpcSslOptions -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.config.MutualSslConfiguration import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration import org.apache.activemq.artemis.core.server.ActiveMQServer @@ -28,17 +28,17 @@ class ArtemisRpcBroker internal constructor( private val maxMessageSize: Int, private val jmxEnabled: Boolean = false, private val baseDirectory: Path, - private val nodeConfiguration: SSLConfiguration, + private val nodeConfiguration: MutualSslConfiguration, private val shouldStartLocalShell: Boolean) : ArtemisBroker { companion object { private val logger = loggerFor<ArtemisRpcBroker>() - fun withSsl(configuration: SSLConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, sslOptions: BrokerRpcSslOptions, securityManager: RPCSecurityManager, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean): ArtemisBroker { + fun withSsl(configuration: MutualSslConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, sslOptions: BrokerRpcSslOptions, securityManager: RPCSecurityManager, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean): ArtemisBroker { return ArtemisRpcBroker(address, adminAddress, sslOptions, true, securityManager, maxMessageSize, jmxEnabled, baseDirectory, configuration, shouldStartLocalShell) } - fun withoutSsl(configuration: SSLConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, securityManager: RPCSecurityManager, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean): ArtemisBroker { + fun withoutSsl(configuration: MutualSslConfiguration, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort, securityManager: RPCSecurityManager, maxMessageSize: Int, jmxEnabled: Boolean, baseDirectory: Path, shouldStartLocalShell: Boolean): ArtemisBroker { return ArtemisRpcBroker(address, adminAddress, null, false, securityManager, maxMessageSize, jmxEnabled, baseDirectory, configuration, shouldStartLocalShell) } } @@ -83,14 +83,14 @@ class ArtemisRpcBroker internal constructor( @Throws(IOException::class, KeyStoreException::class) private fun createArtemisSecurityManager(loginListener: LoginListener): ActiveMQJAASSecurityManager { - val keyStore = nodeConfiguration.loadSslKeyStore().internal - val trustStore = nodeConfiguration.loadTrustStore().internal + val keyStore = nodeConfiguration.keyStore.get() + val trustStore = nodeConfiguration.trustStore.get() val securityConfig = object : SecurityConfiguration() { override fun getAppConfigurationEntry(name: String): Array<AppConfigurationEntry> { val options = mapOf( RPC_SECURITY_CONFIG to RPCJaasConfig(securityManager, loginListener, useSsl), - NODE_SECURITY_CONFIG to NodeJaasConfig(keyStore, trustStore) + NODE_SECURITY_CONFIG to NodeJaasConfig(keyStore.value.internal, trustStore.value.internal) ) return arrayOf(AppConfigurationEntry(name, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options)) } diff --git a/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt index 3859e7cd8c..9e1ace3c2a 100644 --- a/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt @@ -4,12 +4,12 @@ import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.internal.artemis.BrokerJaasLoginModule import net.corda.node.internal.artemis.SecureArtemisConfiguration -import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcAcceptorTcpTransport -import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcInternalAcceptorTcpTransport import net.corda.nodeapi.BrokerRpcSslOptions import net.corda.nodeapi.RPCApi import net.corda.nodeapi.internal.ArtemisMessagingComponent -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcAcceptorTcpTransport +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcInternalAcceptorTcpTransport +import net.corda.nodeapi.internal.config.MutualSslConfiguration import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.core.config.CoreQueueConfiguration import org.apache.activemq.artemis.core.security.Role @@ -17,7 +17,7 @@ import org.apache.activemq.artemis.core.settings.impl.AddressFullMessagePolicy import org.apache.activemq.artemis.core.settings.impl.AddressSettings import java.nio.file.Path -internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int, jmxEnabled: Boolean, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort?, sslOptions: BrokerRpcSslOptions?, useSsl: Boolean, nodeConfiguration: SSLConfiguration, shouldStartLocalShell: Boolean) : SecureArtemisConfiguration() { +internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int, jmxEnabled: Boolean, address: NetworkHostAndPort, adminAddress: NetworkHostAndPort?, sslOptions: BrokerRpcSslOptions?, useSsl: Boolean, nodeConfiguration: MutualSslConfiguration, shouldStartLocalShell: Boolean) : SecureArtemisConfiguration() { val loginListener: (String) -> Unit init { diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt index 8116afc681..3fdaa4b798 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt @@ -69,7 +69,7 @@ class NodeSchemaService(private val extraSchemas: Set<MappedSchema> = emptySet() if (includeNotarySchemas) mapOf(Pair(NodeNotaryV1, SchemaOptions())) else emptyMap() fun internalSchemas() = requiredSchemas.keys + extraSchemas.filter { schema -> // when mapped schemas from the finance module are present, they are considered as internal ones - schema::class.simpleName == "net.corda.finance.schemas.CashSchemaV1" || schema::class.simpleName == "net.corda.finance.schemas.CommercialPaperSchemaV1" } + schema::class.qualifiedName == "net.corda.finance.schemas.CashSchemaV1" || schema::class.qualifiedName == "net.corda.finance.schemas.CommercialPaperSchemaV1" } override val schemaOptions: Map<MappedSchema, SchemaService.SchemaOptions> = requiredSchemas + extraSchemas.associateBy({ it }, { SchemaOptions() }) diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt index ec73a4c3bb..00a0406dbe 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt @@ -4,9 +4,9 @@ import co.paralleluniverse.fibers.Fiber import co.paralleluniverse.fibers.Suspendable import com.codahale.metrics.* import net.corda.core.internal.concurrent.thenMatch -import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializedBytes -import net.corda.core.serialization.serialize +import net.corda.core.serialization.* +import net.corda.core.serialization.internal.CheckpointSerializationContext +import net.corda.core.serialization.internal.checkpointSerialize import net.corda.core.utilities.contextLogger import net.corda.core.utilities.trace import net.corda.node.services.api.CheckpointStorage @@ -27,7 +27,7 @@ class ActionExecutorImpl( private val checkpointStorage: CheckpointStorage, private val flowMessaging: FlowMessaging, private val stateMachineManager: StateMachineManagerInternal, - private val checkpointSerializationContext: SerializationContext, + private val checkpointSerializationContext: CheckpointSerializationContext, metrics: MetricRegistry ) : ActionExecutor { @@ -237,7 +237,7 @@ class ActionExecutorImpl( } private fun serializeCheckpoint(checkpoint: Checkpoint): SerializedBytes<Checkpoint> { - return checkpoint.serialize(context = checkpointSerializationContext) + return checkpoint.checkpointSerialize(context = checkpointSerializationContext) } private fun cancelFlowTimeout(action: Action.CancelFlowTimeout) { diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt index 4cbd80cfd2..3b099e5c9a 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt @@ -12,8 +12,8 @@ import net.corda.core.cordapp.Cordapp import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.internal.* -import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.serialize +import net.corda.core.serialization.internal.CheckpointSerializationContext +import net.corda.core.serialization.internal.checkpointSerialize import net.corda.core.utilities.Try import net.corda.core.utilities.debug import net.corda.core.utilities.trace @@ -69,7 +69,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId, val actionExecutor: ActionExecutor, val stateMachine: StateMachine, val serviceHub: ServiceHubInternal, - val checkpointSerializationContext: SerializationContext, + val checkpointSerializationContext: CheckpointSerializationContext, val unfinishedFibers: ReusableLatch ) @@ -369,7 +369,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId, Event.Suspend( ioRequest = ioRequest, maySkipCheckpoint = skipPersistingCheckpoint, - fiber = this.serialize(context = serializationContext.value) + fiber = this.checkpointSerialize(context = serializationContext.value) ) } catch (throwable: Throwable) { Event.Error(throwable) diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/SessionRejectException.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/SessionRejectException.kt index 90d4432f0d..cea4680411 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/SessionRejectException.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/SessionRejectException.kt @@ -5,4 +5,4 @@ import net.corda.core.CordaException /** * An exception propagated and thrown in case a session initiation fails. */ -class SessionRejectException(reason: String) : CordaException(reason) +class SessionRejectException(message: String) : CordaException(message) diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt index 50252c38be..a47799e0aa 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt @@ -19,6 +19,10 @@ import net.corda.core.internal.concurrent.map import net.corda.core.internal.concurrent.openFuture import net.corda.core.messaging.DataFeed import net.corda.core.serialization.* +import net.corda.core.serialization.internal.CheckpointSerializationContext +import net.corda.core.serialization.internal.CheckpointSerializationDefaults +import net.corda.core.serialization.internal.checkpointDeserialize +import net.corda.core.serialization.internal.checkpointSerialize import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.Try import net.corda.core.utilities.contextLogger @@ -36,7 +40,7 @@ import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.injectOldProgressTracker import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.wrapWithDatabaseTransaction -import net.corda.serialization.internal.SerializeAsTokenContextImpl +import net.corda.serialization.internal.CheckpointSerializeAsTokenContextImpl import net.corda.serialization.internal.withTokenContext import org.apache.activemq.artemis.utils.ReusableLatch import rx.Observable @@ -103,7 +107,7 @@ class SingleThreadedStateMachineManager( private val transitionExecutor = makeTransitionExecutor() private val ourSenderUUID = serviceHub.networkService.ourSenderUUID - private var checkpointSerializationContext: SerializationContext? = null + private var checkpointSerializationContext: CheckpointSerializationContext? = null private var actionExecutor: ActionExecutor? = null override val allStateMachines: List<FlowLogic<*>> @@ -122,8 +126,8 @@ class SingleThreadedStateMachineManager( override fun start(tokenizableServices: List<Any>) { checkQuasarJavaAgentPresence() - val checkpointSerializationContext = SerializationDefaults.CHECKPOINT_CONTEXT.withTokenContext( - SerializeAsTokenContextImpl(tokenizableServices, SerializationDefaults.SERIALIZATION_FACTORY, SerializationDefaults.CHECKPOINT_CONTEXT, serviceHub) + val checkpointSerializationContext = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT.withTokenContext( + CheckpointSerializeAsTokenContextImpl(tokenizableServices, CheckpointSerializationDefaults.CHECKPOINT_SERIALIZATION_FACTORY, CheckpointSerializationDefaults.CHECKPOINT_CONTEXT, serviceHub) ) this.checkpointSerializationContext = checkpointSerializationContext this.actionExecutor = makeActionExecutor(checkpointSerializationContext) @@ -134,6 +138,7 @@ class SingleThreadedStateMachineManager( (fiber as FlowStateMachineImpl<*>).logger.warn("Caught exception from flow", throwable) } serviceHub.networkMapCache.nodeReady.then { + logger.info("Node ready, info: ${serviceHub.myInfo}") resumeRestoredFlows(fibers) flowMessaging.start { _, deduplicationHandler -> executor.execute { @@ -530,7 +535,7 @@ class SingleThreadedStateMachineManager( val resultFuture = openFuture<Any?>() flowStateMachineImpl.transientValues = TransientReference(createTransientValues(flowId, resultFuture)) flowLogic.stateMachine = flowStateMachineImpl - val frozenFlowLogic = (flowLogic as FlowLogic<*>).serialize(context = checkpointSerializationContext!!) + val frozenFlowLogic = (flowLogic as FlowLogic<*>).checkpointSerialize(context = checkpointSerializationContext!!) val flowCorDappVersion = createSubFlowVersion(serviceHub.cordappProvider.getCordappForFlow(flowLogic), serviceHub.myInfo.platformVersion) @@ -612,7 +617,7 @@ class SingleThreadedStateMachineManager( private fun deserializeCheckpoint(serializedCheckpoint: SerializedBytes<Checkpoint>): Checkpoint? { return try { - serializedCheckpoint.deserialize(context = checkpointSerializationContext!!) + serializedCheckpoint.checkpointDeserialize(context = checkpointSerializationContext!!) } catch (exception: Throwable) { logger.error("Encountered unrestorable checkpoint!", exception) null @@ -657,7 +662,7 @@ class SingleThreadedStateMachineManager( val resultFuture = openFuture<Any?>() val fiber = when (flowState) { is FlowState.Unstarted -> { - val logic = flowState.frozenFlowLogic.deserialize(context = checkpointSerializationContext!!) + val logic = flowState.frozenFlowLogic.checkpointDeserialize(context = checkpointSerializationContext!!) val state = StateMachineState( checkpoint = checkpoint, pendingDeduplicationHandlers = initialDeduplicationHandler?.let { listOf(it) } ?: emptyList(), @@ -676,7 +681,7 @@ class SingleThreadedStateMachineManager( fiber } is FlowState.Started -> { - val fiber = flowState.frozenFiber.deserialize(context = checkpointSerializationContext!!) + val fiber = flowState.frozenFiber.checkpointDeserialize(context = checkpointSerializationContext!!) val state = StateMachineState( checkpoint = checkpoint, pendingDeduplicationHandlers = initialDeduplicationHandler?.let { listOf(it) } ?: emptyList(), @@ -741,7 +746,7 @@ class SingleThreadedStateMachineManager( } } - private fun makeActionExecutor(checkpointSerializationContext: SerializationContext): ActionExecutor { + private fun makeActionExecutor(checkpointSerializationContext: CheckpointSerializationContext): ActionExecutor { return ActionExecutorImpl( serviceHub, checkpointStorage, diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/interceptors/FiberDeserializationCheckingInterceptor.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/interceptors/FiberDeserializationCheckingInterceptor.kt index 67b1733a90..37033977de 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/interceptors/FiberDeserializationCheckingInterceptor.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/interceptors/FiberDeserializationCheckingInterceptor.kt @@ -2,9 +2,9 @@ package net.corda.node.services.statemachine.interceptors import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.StateMachineRunId -import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializedBytes -import net.corda.core.serialization.deserialize +import net.corda.core.serialization.* +import net.corda.core.serialization.internal.CheckpointSerializationContext +import net.corda.core.serialization.internal.checkpointDeserialize import net.corda.core.utilities.contextLogger import net.corda.node.services.statemachine.ActionExecutor import net.corda.node.services.statemachine.Event @@ -68,7 +68,7 @@ class FiberDeserializationChecker { private val jobQueue = LinkedBlockingQueue<Job>() private var foundUnrestorableFibers: Boolean = false - fun start(checkpointSerializationContext: SerializationContext) { + fun start(checkpointSerializationContext: CheckpointSerializationContext) { require(checkerThread == null) checkerThread = thread(name = "FiberDeserializationChecker") { while (true) { @@ -76,7 +76,7 @@ class FiberDeserializationChecker { when (job) { is Job.Check -> { try { - job.serializedFiber.deserialize(context = checkpointSerializationContext) + job.serializedFiber.checkpointDeserialize(context = checkpointSerializationContext) } catch (throwable: Throwable) { log.error("Encountered unrestorable checkpoint!", throwable) foundUnrestorableFibers = true diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt index d6963e7130..156640c443 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt @@ -102,7 +102,8 @@ class BFTNonValidatingNotaryService( private fun createMap(): AppendOnlyPersistentMap<StateRef, SecureHash, CommittedState, PersistentStateRef> { return AppendOnlyPersistentMap( - "BFTNonValidatingNotaryService_transactions", + cacheFactory = services.cacheFactory, + name = "BFTNonValidatingNotaryService_transactions", toPersistentEntityKey = { PersistentStateRef(it.txhash.toString(), it.index) }, fromPersistentEntity = { //TODO null check will become obsolete after making DB/JPA columns not nullable diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt index e0ab98b9e5..b6c8ebf437 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt @@ -1,5 +1,6 @@ package net.corda.node.services.transactions +import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.StateRef import net.corda.core.contracts.TimeWindow import net.corda.core.crypto.SecureHash @@ -8,9 +9,10 @@ import net.corda.core.flows.NotarisationRequestSignature import net.corda.core.flows.NotaryError import net.corda.core.flows.StateConsumptionDetails import net.corda.core.identity.Party -import net.corda.core.internal.ThreadBox +import net.corda.core.internal.concurrent.OpenFuture +import net.corda.core.internal.concurrent.openFuture +import net.corda.core.internal.notary.AsyncUniquenessProvider import net.corda.core.internal.notary.NotaryInternalException -import net.corda.core.internal.notary.UniquenessProvider import net.corda.core.internal.notary.isConsumedByTheSameTx import net.corda.core.internal.notary.validateTimeWindow import net.corda.core.schemas.PersistentStateRef @@ -20,17 +22,22 @@ import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.node.utilities.AppendOnlyPersistentMap +import net.corda.node.utilities.NamedCacheFactory +import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import net.corda.nodeapi.internal.persistence.currentDBSession import java.time.Clock import java.time.Instant import java.util.* +import java.util.concurrent.LinkedBlockingQueue import javax.annotation.concurrent.ThreadSafe import javax.persistence.* +import kotlin.concurrent.thread /** A RDBMS backed Uniqueness provider */ @ThreadSafe -class PersistentUniquenessProvider(val clock: Clock) : UniquenessProvider, SingletonSerializeAsToken() { +class PersistentUniquenessProvider(val clock: Clock, val database: CordaPersistence, cacheFactory: NamedCacheFactory) : AsyncUniquenessProvider, SingletonSerializeAsToken() { + @MappedSuperclass class BaseComittedState( @EmbeddedId @@ -63,21 +70,41 @@ class PersistentUniquenessProvider(val clock: Clock) : UniquenessProvider, Singl var requestDate: Instant ) + private data class CommitRequest( + val states: List<StateRef>, + val txId: SecureHash, + val callerIdentity: Party, + val requestSignature: NotarisationRequestSignature, + val timeWindow: TimeWindow?, + val references: List<StateRef>, + val future: OpenFuture<AsyncUniquenessProvider.Result>) + @Entity @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}notary_committed_states") class CommittedState(id: PersistentStateRef, consumingTxHash: String) : BaseComittedState(id, consumingTxHash) - private class InnerState { - val commitLog = createMap() + private val commitLog = createMap(cacheFactory) + + private val requestQueue = LinkedBlockingQueue<CommitRequest>(requestQueueSize) + + /** A request processor thread. */ + private val processorThread = thread(name = "Notary request queue processor", isDaemon = true) { + try { + while (!Thread.interrupted()) { + processRequest(requestQueue.take()) + } + } catch (e: InterruptedException) { + } + log.debug { "Shutting down with ${requestQueue.size} in-flight requests unprocessed." } } - private val mutex = ThreadBox(InnerState()) - companion object { + private const val requestQueueSize = 100_000 private val log = contextLogger() - fun createMap(): AppendOnlyPersistentMap<StateRef, SecureHash, CommittedState, PersistentStateRef> = + fun createMap(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap<StateRef, SecureHash, CommittedState, PersistentStateRef> = AppendOnlyPersistentMap( - "PersistentUniquenessProvider_transactions", + cacheFactory = cacheFactory, + name = "PersistentUniquenessProvider_transactions", toPersistentEntityKey = { PersistentStateRef(it.txhash.toString(), it.index) }, fromPersistentEntity = { //TODO null check will become obsolete after making DB/JPA columns not nullable @@ -99,23 +126,25 @@ class PersistentUniquenessProvider(val clock: Clock) : UniquenessProvider, Singl ) } - override fun commit( + + /** + * Generates and adds a [CommitRequest] to the request queue. If the request queue is full, this method will block + * until space is available. + * + * Returns a future that will complete once the request is processed, containing the commit [Result]. + */ + override fun commitAsync( states: List<StateRef>, txId: SecureHash, callerIdentity: Party, requestSignature: NotarisationRequestSignature, timeWindow: TimeWindow?, references: List<StateRef> - ) { - mutex.locked { - logRequest(txId, callerIdentity, requestSignature) - val conflictingStates = findAlreadyCommitted(states, references, commitLog) - if (conflictingStates.isNotEmpty()) { - handleConflicts(txId, conflictingStates) - } else { - handleNoConflicts(timeWindow, states, txId, commitLog) - } - } + ): CordaFuture<AsyncUniquenessProvider.Result> { + val future = openFuture<AsyncUniquenessProvider.Result>() + val request = CommitRequest(states, txId, callerIdentity, requestSignature, timeWindow, references, future) + requestQueue.put(request) + return future } private fun logRequest(txId: SecureHash, callerIdentity: Party, requestSignature: NotarisationRequestSignature) { @@ -149,6 +178,25 @@ class PersistentUniquenessProvider(val clock: Clock) : UniquenessProvider, Singl return conflictingStates } + private fun commitOne( + states: List<StateRef>, + txId: SecureHash, + callerIdentity: Party, + requestSignature: NotarisationRequestSignature, + timeWindow: TimeWindow?, + references: List<StateRef> + ) { + database.transaction { + logRequest(txId, callerIdentity, requestSignature) + val conflictingStates = findAlreadyCommitted(states, references, commitLog) + if (conflictingStates.isNotEmpty()) { + handleConflicts(txId, conflictingStates) + } else { + handleNoConflicts(timeWindow, states, txId, commitLog) + } + } + } + private fun handleConflicts(txId: SecureHash, conflictingStates: LinkedHashMap<StateRef, StateConsumptionDetails>) { if (isConsumedByTheSameTx(txId.sha256(), conflictingStates)) { log.debug { "Transaction $txId already notarised" } @@ -171,4 +219,26 @@ class PersistentUniquenessProvider(val clock: Clock) : UniquenessProvider, Singl throw NotaryInternalException(outsideTimeWindowError) } } + + private fun processRequest(request: CommitRequest) { + try { + commitOne(request.states, request.txId, request.callerIdentity, request.requestSignature, request.timeWindow, request.references) + respondWithSuccess(request) + } catch (e: Exception) { + log.warn("Error processing commit request", e) + respondWithError(request, e) + } + } + + private fun respondWithError(request: CommitRequest, exception: Exception) { + if (exception is NotaryInternalException) { + request.future.set(AsyncUniquenessProvider.Result.Failure(exception.error)) + } else { + request.future.setException(NotaryInternalException(NotaryError.General(Exception("Internal service error.")))) + } + } + + private fun respondWithSuccess(request: CommitRequest) { + request.future.set(AsyncUniquenessProvider.Result.Success) + } } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLog.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLog.kt index 59a0734ceb..72fa52bdc1 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLog.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLog.kt @@ -20,10 +20,10 @@ import net.corda.core.flows.StateConsumptionDetails import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.notary.isConsumedByTheSameTx import net.corda.core.internal.notary.validateTimeWindow -import net.corda.core.serialization.SerializationDefaults -import net.corda.core.serialization.SerializationFactory -import net.corda.core.serialization.deserialize -import net.corda.core.serialization.serialize +import net.corda.core.serialization.* +import net.corda.core.serialization.internal.CheckpointSerializationDefaults +import net.corda.core.serialization.internal.CheckpointSerializationFactory +import net.corda.core.serialization.internal.checkpointSerialize import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug @@ -200,11 +200,11 @@ class RaftTransactionCommitLog<E, EK>( } class CordaKryoSerializer<T : Any> : TypeSerializer<T> { - private val context = SerializationDefaults.CHECKPOINT_CONTEXT.withEncoding(CordaSerializationEncoding.SNAPPY) - private val factory = SerializationFactory.defaultFactory + private val context = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT.withEncoding(CordaSerializationEncoding.SNAPPY) + private val factory = CheckpointSerializationFactory.defaultFactory override fun write(obj: T, buffer: BufferOutput<*>, serializer: Serializer) { - val serialized = obj.serialize(context = context) + val serialized = obj.checkpointSerialize(context = context) buffer.writeInt(serialized.size) buffer.write(serialized.bytes) } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt index 1e5c46d28b..2192343fa1 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt @@ -28,8 +28,8 @@ import net.corda.core.utilities.debug import net.corda.node.services.config.RaftConfig import net.corda.node.services.transactions.RaftTransactionCommitLog.Commands.CommitTransaction import net.corda.node.utilities.AppendOnlyPersistentMap -import net.corda.nodeapi.internal.config.NodeSSLConfiguration -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.node.utilities.NamedCacheFactory +import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import java.nio.file.Path @@ -51,17 +51,20 @@ import javax.persistence.Table */ @ThreadSafe class RaftUniquenessProvider( - private val transportConfiguration: NodeSSLConfiguration, + private val storagePath: Path, + private val transportConfiguration: MutualSslConfiguration, private val db: CordaPersistence, private val clock: Clock, private val metrics: MetricRegistry, + private val cacheFactory: NamedCacheFactory, private val raftConfig: RaftConfig ) : UniquenessProvider, SingletonSerializeAsToken() { companion object { private val log = contextLogger() - fun createMap(): AppendOnlyPersistentMap<StateRef, Pair<Long, SecureHash>, CommittedState, PersistentStateRef> = + fun createMap(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap<StateRef, Pair<Long, SecureHash>, CommittedState, PersistentStateRef> = AppendOnlyPersistentMap( - "RaftUniquenessProvider_transactions", + cacheFactory = cacheFactory, + name = "RaftUniquenessProvider_transactions", toPersistentEntityKey = { PersistentStateRef(it) }, fromPersistentEntity = { val txId = it.id.txId @@ -96,8 +99,6 @@ class RaftUniquenessProvider( var index: Long = 0 ) - /** Directory storing the Raft log and state machine snapshots */ - private val storagePath: Path = transportConfiguration.baseDirectory private lateinit var _clientFuture: CompletableFuture<CopycatClient> private lateinit var server: CopycatServer @@ -111,7 +112,7 @@ class RaftUniquenessProvider( fun start() { log.info("Creating Copycat server, log stored in: ${storagePath.toAbsolutePath()}") val stateMachineFactory = { - RaftTransactionCommitLog(db, clock, RaftUniquenessProvider.Companion::createMap) + RaftTransactionCommitLog(db, clock, { createMap(cacheFactory) }) } val address = raftConfig.nodeAddress.let { Address(it.host, it.port) } val storage = buildStorage(storagePath) @@ -155,14 +156,14 @@ class RaftUniquenessProvider( .build() } - private fun buildTransport(config: SSLConfiguration): Transport? { + private fun buildTransport(config: MutualSslConfiguration): Transport? { return NettyTransport.builder() .withSsl() .withSslProtocol(SslProtocol.TLSv1_2) - .withKeyStorePath(config.sslKeystore.toString()) - .withKeyStorePassword(config.keyStorePassword) - .withTrustStorePath(config.trustStoreFile.toString()) - .withTrustStorePassword(config.trustStorePassword) + .withKeyStorePath(config.keyStore.path.toString()) + .withKeyStorePassword(config.keyStore.password) + .withTrustStorePath(config.trustStore.path.toString()) + .withTrustStorePassword(config.trustStore.password) .build() } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt index 093c0d905b..51909ea5b1 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt @@ -8,10 +8,10 @@ import java.security.PublicKey /** A simple Notary service that does not perform transaction validation */ class SimpleNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { - override val uniquenessProvider = PersistentUniquenessProvider(services.clock) + override val uniquenessProvider = PersistentUniquenessProvider(services.clock, services.database, services.cacheFactory) override fun createServiceFlow(otherPartySession: FlowSession): NotaryServiceFlow = NonValidatingNotaryFlow(otherPartySession, this) override fun start() {} override fun stop() {} -} \ No newline at end of file +} diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt index 08b4fcb211..6e39a3ea1e 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt @@ -8,10 +8,10 @@ import java.security.PublicKey /** A Notary service that validates the transaction chain of the submitted transaction before committing it */ class ValidatingNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { - override val uniquenessProvider = PersistentUniquenessProvider(services.clock) + override val uniquenessProvider = PersistentUniquenessProvider(services.clock, services.database, services.cacheFactory) override fun createServiceFlow(otherPartySession: FlowSession): NotaryServiceFlow = ValidatingNotaryFlow(otherPartySession, this) override fun start() {} override fun stop() {} -} \ No newline at end of file +} diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index 76c4e2b09d..a3fa9aee01 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -7,6 +7,13 @@ import net.corda.core.internal.uncheckedCast import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultQueryException import net.corda.core.node.services.vault.* +import net.corda.core.node.services.vault.BinaryComparisonOperator.* +import net.corda.core.node.services.vault.CollectionOperator.* +import net.corda.core.node.services.vault.ColumnPredicate.* +import net.corda.core.node.services.vault.EqualityComparisonOperator.* +import net.corda.core.node.services.vault.LikenessOperator.* +import net.corda.core.node.services.vault.NullOperator.IS_NULL +import net.corda.core.node.services.vault.NullOperator.NOT_NULL import net.corda.core.node.services.vault.QueryCriteria.CommonQueryCriteria import net.corda.core.schemas.PersistentState import net.corda.core.schemas.PersistentStateRef @@ -54,54 +61,93 @@ abstract class AbstractQueryCriteriaParser<Q : GenericQueryCriteria<Q,P>, in P: protected fun columnPredicateToPredicate(column: Path<out Any?>, columnPredicate: ColumnPredicate<*>): Predicate { return when (columnPredicate) { - is ColumnPredicate.EqualityComparison -> { - val literal = columnPredicate.rightLiteral - when (columnPredicate.operator) { - EqualityComparisonOperator.EQUAL -> criteriaBuilder.equal(column, literal) - EqualityComparisonOperator.NOT_EQUAL -> criteriaBuilder.notEqual(column, literal) - } - } - is ColumnPredicate.BinaryComparison -> { - val literal: Comparable<Any?>? = uncheckedCast(columnPredicate.rightLiteral) - @Suppress("UNCHECKED_CAST") - column as Path<Comparable<Any?>?> - when (columnPredicate.operator) { - BinaryComparisonOperator.GREATER_THAN -> criteriaBuilder.greaterThan(column, literal) - BinaryComparisonOperator.GREATER_THAN_OR_EQUAL -> criteriaBuilder.greaterThanOrEqualTo(column, literal) - BinaryComparisonOperator.LESS_THAN -> criteriaBuilder.lessThan(column, literal) - BinaryComparisonOperator.LESS_THAN_OR_EQUAL -> criteriaBuilder.lessThanOrEqualTo(column, literal) - } - } - is ColumnPredicate.Likeness -> { - @Suppress("UNCHECKED_CAST") - column as Path<String?> - when (columnPredicate.operator) { - LikenessOperator.LIKE -> criteriaBuilder.like(column, columnPredicate.rightLiteral) - LikenessOperator.NOT_LIKE -> criteriaBuilder.notLike(column, columnPredicate.rightLiteral) - } - } - is ColumnPredicate.CollectionExpression -> { - when (columnPredicate.operator) { - CollectionOperator.IN -> column.`in`(columnPredicate.rightLiteral) - CollectionOperator.NOT_IN -> criteriaBuilder.not(column.`in`(columnPredicate.rightLiteral)) - } - } - is ColumnPredicate.Between -> { - @Suppress("UNCHECKED_CAST") - column as Path<Comparable<Any?>?> - val fromLiteral: Comparable<Any?>? = uncheckedCast(columnPredicate.rightFromLiteral) - val toLiteral: Comparable<Any?>? = uncheckedCast(columnPredicate.rightToLiteral) - criteriaBuilder.between(column, fromLiteral, toLiteral) - } - is ColumnPredicate.NullExpression -> { - when (columnPredicate.operator) { - NullOperator.IS_NULL -> criteriaBuilder.isNull(column) - NullOperator.NOT_NULL -> criteriaBuilder.isNotNull(column) - } - } + is EqualityComparison -> equalityComparisonToPredicate(column, columnPredicate) + is BinaryComparison -> binaryComparisonToPredicate(column, columnPredicate) + is Likeness -> likeComparisonToPredicate(column, columnPredicate) + is CollectionExpression -> collectionComparisonToPredicate(column, columnPredicate) + is Between -> betweenComparisonToPredicate(column, columnPredicate) + is NullExpression -> nullComparisonToPredicate(column, columnPredicate) else -> throw VaultQueryException("Not expecting $columnPredicate") } } + + private fun equalityComparisonToPredicate(column: Path<out Any?>, columnPredicate: EqualityComparison<*>): Predicate { + val literal = columnPredicate.rightLiteral + return if (literal is String) { + @Suppress("UNCHECKED_CAST") + column as Path<String?> + when (columnPredicate.operator) { + EQUAL -> criteriaBuilder.equal(column, literal) + EQUAL_IGNORE_CASE -> criteriaBuilder.equal(criteriaBuilder.upper(column), literal.toUpperCase()) + NOT_EQUAL -> criteriaBuilder.notEqual(column, literal) + NOT_EQUAL_IGNORE_CASE -> criteriaBuilder.notEqual(criteriaBuilder.upper(column), literal.toUpperCase()) + } + } else { + when (columnPredicate.operator) { + EQUAL, EQUAL_IGNORE_CASE -> criteriaBuilder.equal(column, literal) + NOT_EQUAL, NOT_EQUAL_IGNORE_CASE -> criteriaBuilder.notEqual(column, literal) + } + } + } + + private fun binaryComparisonToPredicate(column: Path<out Any?>, columnPredicate: BinaryComparison<*>): Predicate { + val literal: Comparable<Any?>? = uncheckedCast(columnPredicate.rightLiteral) + @Suppress("UNCHECKED_CAST") + column as Path<Comparable<Any?>?> + return when (columnPredicate.operator) { + GREATER_THAN -> criteriaBuilder.greaterThan(column, literal) + GREATER_THAN_OR_EQUAL -> criteriaBuilder.greaterThanOrEqualTo(column, literal) + LESS_THAN -> criteriaBuilder.lessThan(column, literal) + LESS_THAN_OR_EQUAL -> criteriaBuilder.lessThanOrEqualTo(column, literal) + } + } + + private fun likeComparisonToPredicate(column: Path<out Any?>, columnPredicate: Likeness): Predicate { + @Suppress("UNCHECKED_CAST") + column as Path<String?> + return when (columnPredicate.operator) { + LIKE -> criteriaBuilder.like(column, columnPredicate.rightLiteral) + LIKE_IGNORE_CASE -> criteriaBuilder.like(criteriaBuilder.upper(column), columnPredicate.rightLiteral.toUpperCase()) + NOT_LIKE -> criteriaBuilder.notLike(column, columnPredicate.rightLiteral) + NOT_LIKE_IGNORE_CASE -> criteriaBuilder.notLike(criteriaBuilder.upper(column), columnPredicate.rightLiteral.toUpperCase()) + } + } + + private fun collectionComparisonToPredicate(column: Path<out Any?>, columnPredicate: CollectionExpression<*>): Predicate { + val literal = columnPredicate.rightLiteral + return if (literal.any { it is String }) { + @Suppress("UNCHECKED_CAST") + column as Path<String?> + @Suppress("UNCHECKED_CAST") + literal as Collection<String> + when (columnPredicate.operator) { + IN -> column.`in`(literal) + IN_IGNORE_CASE -> criteriaBuilder.upper(column).`in`(literal.map { it.toUpperCase() }) + NOT_IN -> criteriaBuilder.not(column.`in`(literal)) + NOT_IN_IGNORE_CASE -> criteriaBuilder.not(criteriaBuilder.upper(column).`in`(literal.map { it.toUpperCase() })) + } + } else { + when (columnPredicate.operator) { + IN, IN_IGNORE_CASE -> column.`in`(literal) + NOT_IN, NOT_IN_IGNORE_CASE -> criteriaBuilder.not(column.`in`(literal)) + } + } + } + + private fun betweenComparisonToPredicate(column: Path<out Any?>, columnPredicate: Between<*>): Predicate { + @Suppress("UNCHECKED_CAST") + column as Path<Comparable<Any?>?> + val fromLiteral: Comparable<Any?>? = uncheckedCast(columnPredicate.rightFromLiteral) + val toLiteral: Comparable<Any?>? = uncheckedCast(columnPredicate.rightToLiteral) + return criteriaBuilder.between(column, fromLiteral, toLiteral) + } + + private fun nullComparisonToPredicate(column: Path<out Any?>, columnPredicate: NullExpression<*>): Predicate { + return when (columnPredicate.operator) { + IS_NULL -> criteriaBuilder.isNull(column) + NOT_NULL -> criteriaBuilder.isNotNull(column) + } + } } class HibernateAttachmentQueryCriteriaParser(override val criteriaBuilder: CriteriaBuilder, @@ -474,7 +520,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat // state status stateTypes = criteria.status if (criteria.status != Vault.StateStatus.ALL) { - val predicateID = Pair(VaultSchemaV1.VaultStates::stateStatus.name, EqualityComparisonOperator.EQUAL) + val predicateID = Pair(VaultSchemaV1.VaultStates::stateStatus.name, EQUAL) if (commonPredicates.containsKey(predicateID)) { val existingStatus = ((commonPredicates[predicateID] as ComparisonPredicate).rightHandOperand as LiteralExpression).literal if (existingStatus != criteria.status) { @@ -487,23 +533,23 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat } // state relevance. - if (criteria.isRelevant != Vault.RelevancyStatus.ALL) { - val predicateID = Pair(VaultSchemaV1.VaultStates::isRelevant.name, EqualityComparisonOperator.EQUAL) + if (criteria.relevancyStatus != Vault.RelevancyStatus.ALL) { + val predicateID = Pair(VaultSchemaV1.VaultStates::relevancyStatus.name, EQUAL) if (commonPredicates.containsKey(predicateID)) { val existingStatus = ((commonPredicates[predicateID] as ComparisonPredicate).rightHandOperand as LiteralExpression).literal - if (existingStatus != criteria.isRelevant) { - log.warn("Overriding previous attribute [${VaultSchemaV1.VaultStates::isRelevant.name}] value $existingStatus with ${criteria.status}") - commonPredicates.replace(predicateID, criteriaBuilder.equal(vaultStates.get<Vault.RelevancyStatus>(VaultSchemaV1.VaultStates::isRelevant.name), criteria.isRelevant)) + if (existingStatus != criteria.relevancyStatus) { + log.warn("Overriding previous attribute [${VaultSchemaV1.VaultStates::relevancyStatus.name}] value $existingStatus with ${criteria.status}") + commonPredicates.replace(predicateID, criteriaBuilder.equal(vaultStates.get<Vault.RelevancyStatus>(VaultSchemaV1.VaultStates::relevancyStatus.name), criteria.relevancyStatus)) } } else { - commonPredicates[predicateID] = criteriaBuilder.equal(vaultStates.get<Vault.RelevancyStatus>(VaultSchemaV1.VaultStates::isRelevant.name), criteria.isRelevant) + commonPredicates[predicateID] = criteriaBuilder.equal(vaultStates.get<Vault.RelevancyStatus>(VaultSchemaV1.VaultStates::relevancyStatus.name), criteria.relevancyStatus) } } // contract state types val contractStateTypes = deriveContractStateTypes(criteria.contractStateTypes) if (contractStateTypes.isNotEmpty()) { - val predicateID = Pair(VaultSchemaV1.VaultStates::contractStateClassName.name, CollectionOperator.IN) + val predicateID = Pair(VaultSchemaV1.VaultStates::contractStateClassName.name, IN) if (commonPredicates.containsKey(predicateID)) { val existingTypes = (commonPredicates[predicateID]!!.expressions[0] as InPredicate<*>).values.map { (it as LiteralExpression).literal }.toSet() if (existingTypes != contractStateTypes) { diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index a815b34a0a..5264fb3b81 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.strands.Strand import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.containsAny import net.corda.core.internal.* import net.corda.core.messaging.DataFeed import net.corda.core.node.ServicesForResolution @@ -113,10 +114,12 @@ class NodeVaultService( // For EVERY state to be committed to the vault, this checks whether it is spendable by the recording // node. The behaviour is as follows: // - // 1) All vault updates marked as RELEVANT will, of, course all have isRelevant = true. - // 2) For ALL_VISIBLE updates, those which are not relevant according to the relevancy rules will have isRelevant = false. + // 1) All vault updates marked as RELEVANT will, of course, all have relevancy_status = 1 in the + // "vault_states" table. + // 2) For ALL_VISIBLE updates, those which are not relevant according to the relevancy rules will have + // relevancy_status = 0 in the "vault_states" table. // - // This is useful when it comes to querying for fungible states, when we do not want non-relevant states + // This is useful when it comes to querying for fungible states, when we do not want irrelevant states // included in the result. // // The same functionality could be obtained by passing in a list of participants to the vault query, @@ -134,7 +137,7 @@ class NodeVaultService( contractStateClassName = stateAndRef.value.state.data.javaClass.name, stateStatus = Vault.StateStatus.UNCONSUMED, recordedTime = clock.instant(), - isRelevant = if (isRelevant) Vault.RelevancyStatus.RELEVANT else Vault.RelevancyStatus.NOT_RELEVANT + relevancyStatus = if (isRelevant) Vault.RelevancyStatus.RELEVANT else Vault.RelevancyStatus.NOT_RELEVANT ) stateToAdd.stateRef = PersistentStateRef(stateAndRef.key) session.save(stateToAdd) @@ -402,7 +405,7 @@ class NodeVaultService( val enrichedCriteria = QueryCriteria.VaultQueryCriteria( contractStateTypes = setOf(contractStateType), softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)), - isRelevant = Vault.RelevancyStatus.RELEVANT + relevancyStatus = Vault.RelevancyStatus.RELEVANT ) val results = queryBy(contractStateType, enrichedCriteria.and(eligibleStatesQuery), sorter) @@ -433,7 +436,7 @@ class NodeVaultService( is OwnableState -> (state.participants.map { it.owningKey } + state.owner.owningKey).toSet() else -> state.participants.map { it.owningKey } } - return keysToCheck.any { it in myKeys } + return keysToCheck.any { it.containsAny(myKeys) } } @Throws(VaultQueryException::class) @@ -511,7 +514,7 @@ class NodeVaultService( vaultState.notary, vaultState.lockId, vaultState.lockUpdateTime, - vaultState.isRelevant)) + vaultState.relevancyStatus)) } else { // TODO: improve typing of returned other results log.debug { "OtherResults: ${Arrays.toString(result.toArray())}" } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt index 23072b2996..e12f8fc7ac 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt @@ -61,8 +61,8 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio var lockId: String? = null, /** Used to determine whether a state abides by the relevancy rules of the recording node */ - @Column(name = "is_relevant", nullable = false) - var isRelevant: Vault.RelevancyStatus, + @Column(name = "relevancy_status", nullable = false) + var relevancyStatus: Vault.RelevancyStatus, /** refers to the last time a lock was taken (reserved) or updated (released, re-reserved) */ @Column(name = "lock_timestamp", nullable = true) diff --git a/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt b/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt index 274ceb6c5a..81f080e425 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt @@ -309,21 +309,20 @@ abstract class AppendOnlyPersistentMapBase<K, V, E, out EK>( // Open for tests to override open class AppendOnlyPersistentMap<K, V, E, out EK>( + cacheFactory: NamedCacheFactory, name: String, toPersistentEntityKey: (K) -> EK, fromPersistentEntity: (E) -> Pair<K, V>, toPersistentEntity: (key: K, value: V) -> E, - persistentEntityClass: Class<E>, - cacheBound: Long = 1024 + persistentEntityClass: Class<E> ) : AppendOnlyPersistentMapBase<K, V, E, EK>( toPersistentEntityKey, fromPersistentEntity, toPersistentEntity, persistentEntityClass) { - //TODO determine cacheBound based on entity class later or with node config allowing tuning, or using some heuristic based on heap size override val cache = NonInvalidatingCache( + cacheFactory = cacheFactory, name = name, - bound = cacheBound, loadFunction = { key: K -> // This gets called if a value is read and the cache has no Transactional for this key yet. val value: V? = loadValue(key) @@ -355,12 +354,12 @@ open class AppendOnlyPersistentMap<K, V, E, out EK>( // Same as above, but with weighted values (e.g. memory footprint sensitive). class WeightBasedAppendOnlyPersistentMap<K, V, E, out EK>( + cacheFactory: NamedCacheFactory, name: String, toPersistentEntityKey: (K) -> EK, fromPersistentEntity: (E) -> Pair<K, V>, toPersistentEntity: (key: K, value: V) -> E, persistentEntityClass: Class<E>, - maxWeight: Long, weighingFunc: (K, Transactional<V>) -> Int ) : AppendOnlyPersistentMapBase<K, V, E, EK>( toPersistentEntityKey, @@ -368,8 +367,8 @@ class WeightBasedAppendOnlyPersistentMap<K, V, E, out EK>( toPersistentEntity, persistentEntityClass) { override val cache = NonInvalidatingWeightBasedCache( - name, - maxWeight = maxWeight, + cacheFactory = cacheFactory, + name = name, weigher = Weigher { key, value -> weighingFunc(key, value) }, loadFunction = { key: K -> val value: V? = loadValue(key) diff --git a/node/src/main/kotlin/net/corda/node/utilities/NamedThreadFactory.kt b/node/src/main/kotlin/net/corda/node/utilities/NamedThreadFactory.kt index 9d3734776d..3782a1e8f7 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/NamedThreadFactory.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/NamedThreadFactory.kt @@ -1,6 +1,5 @@ package net.corda.node.utilities -import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.ThreadFactory import java.util.concurrent.atomic.AtomicInteger @@ -10,19 +9,12 @@ import java.util.concurrent.atomic.AtomicInteger * via an executor. It will use an underlying thread factory to create the actual thread * and then override the thread name with the prefix and an ever increasing number */ -class NamedThreadFactory(private val name: String, private val underlyingFactory: ThreadFactory) : ThreadFactory { - val threadNumber = AtomicInteger(1) - override fun newThread(runnable: Runnable?): Thread { - val thread = underlyingFactory.newThread(runnable) +class NamedThreadFactory(private val name: String, + private val delegate: ThreadFactory = Executors.defaultThreadFactory()) : ThreadFactory { + private val threadNumber = AtomicInteger(1) + override fun newThread(runnable: Runnable): Thread { + val thread = delegate.newThread(runnable) thread.name = name + "-" + threadNumber.getAndIncrement() return thread } } - -/** - * Create a single thread executor with a NamedThreadFactory based on the default thread factory - * defined in java.util.concurrent.Executors - */ -fun newNamedSingleThreadExecutor(name: String): ExecutorService { - return Executors.newSingleThreadExecutor(NamedThreadFactory(name, Executors.defaultThreadFactory())) -} diff --git a/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt b/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt new file mode 100644 index 0000000000..d11c9667c2 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt @@ -0,0 +1,54 @@ +package net.corda.node.utilities + +import com.codahale.metrics.MetricRegistry +import com.github.benmanes.caffeine.cache.Cache +import com.github.benmanes.caffeine.cache.CacheLoader +import com.github.benmanes.caffeine.cache.Caffeine +import com.github.benmanes.caffeine.cache.LoadingCache +import net.corda.core.internal.buildNamed +import net.corda.core.serialization.SerializeAsToken +import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.node.services.config.NodeConfiguration + +/** + * Allow passing metrics and config to caching implementations. + */ +interface NamedCacheFactory : SerializeAsToken { + /** + * Build a new cache factory of the same type that incorporates metrics. + */ + fun bindWithMetrics(metricRegistry: MetricRegistry): NamedCacheFactory + + /** + * Build a new cache factory of the same type that incorporates the associated configuration. + */ + fun bindWithConfig(nodeConfiguration: NodeConfiguration): NamedCacheFactory + + fun <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String): Cache<K, V> + fun <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String, loader: CacheLoader<K, V>): LoadingCache<K, V> +} + +class DefaultNamedCacheFactory private constructor(private val metricRegistry: MetricRegistry?, private val nodeConfiguration: NodeConfiguration?) : NamedCacheFactory, SingletonSerializeAsToken() { + constructor() : this(null, null) + + override fun bindWithMetrics(metricRegistry: MetricRegistry): NamedCacheFactory = DefaultNamedCacheFactory(metricRegistry, this.nodeConfiguration) + override fun bindWithConfig(nodeConfiguration: NodeConfiguration): NamedCacheFactory = DefaultNamedCacheFactory(this.metricRegistry, nodeConfiguration) + + override fun <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String): Cache<K, V> { + checkNotNull(metricRegistry) + checkNotNull(nodeConfiguration) + return caffeine.maximumSize(1024).buildNamed<K, V>(name) + } + + override fun <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String, loader: CacheLoader<K, V>): LoadingCache<K, V> { + checkNotNull(metricRegistry) + checkNotNull(nodeConfiguration) + val configuredCaffeine = when (name) { + "DBTransactionStorage_transactions" -> caffeine.maximumWeight(nodeConfiguration!!.transactionCacheSizeBytes) + "NodeAttachmentService_attachmentContent" -> caffeine.maximumWeight(nodeConfiguration!!.attachmentContentCacheSizeBytes) + "NodeAttachmentService_attachmentPresence" -> caffeine.maximumSize(nodeConfiguration!!.attachmentCacheBound) + else -> caffeine.maximumSize(1024) + } + return configuredCaffeine.buildNamed<K, V>(name, loader) + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt b/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt index 814ef0102e..2cf4904282 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt @@ -4,19 +4,18 @@ import com.github.benmanes.caffeine.cache.CacheLoader import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.LoadingCache import com.github.benmanes.caffeine.cache.Weigher -import net.corda.core.internal.buildNamed class NonInvalidatingCache<K, V> private constructor( val cache: LoadingCache<K, V> ) : LoadingCache<K, V> by cache { - constructor(name: String, bound: Long, loadFunction: (K) -> V) : - this(buildCache(name, bound, loadFunction)) + constructor(cacheFactory: NamedCacheFactory, name: String, loadFunction: (K) -> V) : + this(buildCache(cacheFactory, name, loadFunction)) private companion object { - private fun <K, V> buildCache(name: String, bound: Long, loadFunction: (K) -> V): LoadingCache<K, V> { - val builder = Caffeine.newBuilder().maximumSize(bound) - return builder.buildNamed(name, NonInvalidatingCacheLoader(loadFunction)) + private fun <K, V> buildCache(cacheFactory: NamedCacheFactory, name: String, loadFunction: (K) -> V): LoadingCache<K, V> { + val builder = Caffeine.newBuilder() + return cacheFactory.buildNamed(builder, name, NonInvalidatingCacheLoader(loadFunction)) } } @@ -33,13 +32,13 @@ class NonInvalidatingCache<K, V> private constructor( class NonInvalidatingWeightBasedCache<K, V> private constructor( val cache: LoadingCache<K, V> ) : LoadingCache<K, V> by cache { - constructor (name: String, maxWeight: Long, weigher: Weigher<K, V>, loadFunction: (K) -> V) : - this(buildCache(name, maxWeight, weigher, loadFunction)) + constructor (cacheFactory: NamedCacheFactory, name: String, weigher: Weigher<K, V>, loadFunction: (K) -> V) : + this(buildCache(cacheFactory, name, weigher, loadFunction)) private companion object { - private fun <K, V> buildCache(name: String, maxWeight: Long, weigher: Weigher<K, V>, loadFunction: (K) -> V): LoadingCache<K, V> { - val builder = Caffeine.newBuilder().maximumWeight(maxWeight).weigher(weigher) - return builder.buildNamed(name, NonInvalidatingCache.NonInvalidatingCacheLoader(loadFunction)) + private fun <K, V> buildCache(cacheFactory: NamedCacheFactory, name: String, weigher: Weigher<K, V>, loadFunction: (K) -> V): LoadingCache<K, V> { + val builder = Caffeine.newBuilder().weigher(weigher) + return cacheFactory.buildNamed(builder, name, NonInvalidatingCache.NonInvalidatingCacheLoader(loadFunction)) } } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/utilities/logging/AsyncLoggerContextSelectorNoThreadLocal.kt b/node/src/main/kotlin/net/corda/node/utilities/logging/AsyncLoggerContextSelectorNoThreadLocal.kt new file mode 100644 index 0000000000..daa86cd62c --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/utilities/logging/AsyncLoggerContextSelectorNoThreadLocal.kt @@ -0,0 +1,43 @@ +package net.corda.node.utilities.logging + +import net.corda.nodeapi.internal.addShutdownHook +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.core.LoggerContext +import org.apache.logging.log4j.core.async.AsyncLoggerContext +import org.apache.logging.log4j.core.selector.ClassLoaderContextSelector +import org.apache.logging.log4j.core.util.Constants +import org.apache.logging.log4j.util.PropertiesUtil +import java.net.URI + +class AsyncLoggerContextSelectorNoThreadLocal : ClassLoaderContextSelector() { + companion object { + /** + * Returns `true` if the user specified this selector as the Log4jContextSelector, to make all loggers + * asynchronous. + * + * @return `true` if all loggers are asynchronous, `false` otherwise. + */ + @JvmStatic + fun isSelected(): Boolean { + return AsyncLoggerContextSelectorNoThreadLocal::class.java.name == PropertiesUtil.getProperties().getStringProperty(Constants.LOG4J_CONTEXT_SELECTOR) + } + } + + init { + // if we use async log4j logging, we need to shut down the logging to flush the loggers on exit + addShutdownHook { LogManager.shutdown() } + } + + override fun createContext(name: String?, configLocation: URI?): LoggerContext { + return AsyncLoggerContext(name, null, configLocation).also { it.setUseThreadLocals(false) } + } + + override fun toContextMapKey(loader: ClassLoader?): String { + // LOG4J2-666 ensure unique name across separate instances created by webapp classloaders + return "AsyncContextNoThreadLocal@" + Integer.toHexString(System.identityHashCode(loader)) + } + + override fun defaultContextName(): String { + return "DefaultAsyncContextNoThreadLocal@" + Thread.currentThread().name + } +} diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt index ff37ecafad..3e422bc969 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt @@ -52,7 +52,9 @@ class HTTPNetworkRegistrationService(compatibilityZoneURL: URL, val versionInfo: } override fun submitRequest(request: PKCS10CertificationRequest): String { - return String(registrationURL.post(OpaqueBytes(request.encoded), "Client-Version" to "${versionInfo.platformVersion}")) + return String(registrationURL.post(OpaqueBytes(request.encoded), + "Platform-Version" to "${versionInfo.platformVersion}", + "Client-Version" to versionInfo.releaseVersion)) } } diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt index 4ec5319c28..3d4e735498 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt @@ -6,7 +6,8 @@ import net.corda.core.internal.* import net.corda.core.utilities.contextLogger import net.corda.node.NodeRegistrationOption import net.corda.node.services.config.NodeConfiguration -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.config.CertificateStore +import net.corda.nodeapi.internal.config.CertificateStoreSupplier import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509Utilities @@ -33,7 +34,8 @@ import javax.security.auth.x500.X500Principal * needed. */ // TODO: Use content signer instead of keypairs. -open class NetworkRegistrationHelper(private val config: SSLConfiguration, +open class NetworkRegistrationHelper(private val certificatesDirectory: Path, + private val signingCertificateStore: CertificateStoreSupplier, private val myLegalName: CordaX500Name, private val emailAddress: String, private val certService: NetworkRegistrationService, @@ -44,14 +46,12 @@ open class NetworkRegistrationHelper(private val config: SSLConfiguration, private val nextIdleDuration: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1))) { companion object { - const val SELF_SIGNED_PRIVATE_KEY = "Self Signed Private Key" + const val SELF_SIGNED_PRIVATE_KEY = "SelfSignedPrivateKey" val logger = contextLogger() } - private val requestIdStore = config.certificatesDirectory / "certificate-request-id.txt" - // TODO: Use different password for private key. - private val privateKeyPassword = config.keyStorePassword - private val rootTrustStore: X509KeyStore + private val requestIdStore = certificatesDirectory / "certificate-request-id.txt" + protected val rootTrustStore: X509KeyStore protected val rootCert: X509Certificate init { @@ -75,12 +75,14 @@ open class NetworkRegistrationHelper(private val config: SSLConfiguration, * @throws CertificateRequestException if the certificate retrieved by doorman is invalid. */ fun buildKeystore() { - config.certificatesDirectory.createDirectories() - val nodeKeyStore = config.loadNodeKeyStore(createNew = true) + certificatesDirectory.createDirectories() + val nodeKeyStore = signingCertificateStore.get(createNew = true) if (keyAlias in nodeKeyStore) { println("Certificate already exists, Corda node will now terminate...") return } + // TODO: Use different password for private key. + val privateKeyPassword = nodeKeyStore.password val tlsCrlIssuerCert = validateAndGetTlsCrlIssuerCert() if (tlsCrlIssuerCert == null && isTlsCrlIssuerCertRequired()) { System.err.println("""tlsCrlIssuerCert config does not match the root certificate issuer and nor is there any other certificate in the trust store with a matching issuer. @@ -89,7 +91,7 @@ open class NetworkRegistrationHelper(private val config: SSLConfiguration, throw IllegalArgumentException("TLS CRL issuer certificate not found in the trust store.") } - val keyPair = nodeKeyStore.loadOrCreateKeyPair(SELF_SIGNED_PRIVATE_KEY) + val keyPair = nodeKeyStore.loadOrCreateKeyPair(SELF_SIGNED_PRIVATE_KEY, privateKeyPassword) val requestId = try { submitOrResumeCertificateSigningRequest(keyPair) @@ -110,7 +112,7 @@ open class NetworkRegistrationHelper(private val config: SSLConfiguration, throw certificateRequestException } validateCertificates(keyPair.public, certificates) - storePrivateKeyWithCertificates(nodeKeyStore, keyPair, certificates, keyAlias) + storePrivateKeyWithCertificates(nodeKeyStore, keyPair, certificates, keyAlias, privateKeyPassword) onSuccess(keyPair, certificates, tlsCrlIssuerCert?.subjectX500Principal?.toX500Name()) // All done, clean up temp files. requestIdStore.deleteIfExists() @@ -148,25 +150,32 @@ open class NetworkRegistrationHelper(private val config: SSLConfiguration, println("Certificate signing request approved, storing private key with the certificate chain.") } - private fun storePrivateKeyWithCertificates(nodeKeystore: X509KeyStore, keyPair: KeyPair, certificates: List<X509Certificate>, keyAlias: String) { + private fun storePrivateKeyWithCertificates(nodeKeystore: CertificateStore, keyPair: KeyPair, certificates: List<X509Certificate>, keyAlias: String, keyPassword: String) { // Save private key and certificate chain to the key store. - nodeKeystore.setPrivateKey(keyAlias, keyPair.private, certificates, keyPassword = config.keyStorePassword) - nodeKeystore.internal.deleteEntry(SELF_SIGNED_PRIVATE_KEY) - nodeKeystore.save() - println("Private key '$keyAlias' and certificate stored in ${config.nodeKeystore}.") + with(nodeKeystore.value) { + setPrivateKey(keyAlias, keyPair.private, certificates, keyPassword = keyPassword) + // The key was temporarily stored as SELF_SIGNED_PRIVATE_KEY, but now that it's signed by the Doorman we + // can delete this old record. + internal.deleteEntry(SELF_SIGNED_PRIVATE_KEY) + save() + } + println("Private key '$keyAlias' and certificate stored in node signing keystore.") } - private fun X509KeyStore.loadOrCreateKeyPair(alias: String): KeyPair { + private fun CertificateStore.loadOrCreateKeyPair(alias: String, privateKeyPassword: String = password): KeyPair { // Create or load self signed keypair from the key store. // We use the self sign certificate to store the key temporarily in the keystore while waiting for the request approval. if (alias !in this) { + // NODE_CA should be TLS compatible due to the cert hierarchy structure. val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val selfSignCert = X509Utilities.createSelfSignedCACertificate(myLegalName.x500Principal, keyPair) // Save to the key store. - setPrivateKey(alias, keyPair.private, listOf(selfSignCert), keyPassword = privateKeyPassword) - save() + with(value) { + setPrivateKey(alias, keyPair.private, listOf(selfSignCert), keyPassword = privateKeyPassword) + save() + } } - return getCertificateAndKeyPair(alias, privateKeyPassword).keyPair + return query { getCertificateAndKeyPair(alias, privateKeyPassword) }.keyPair } /** @@ -243,7 +252,9 @@ open class NetworkRegistrationHelper(private val config: SSLConfiguration, class NodeRegistrationException(cause: Throwable?) : IOException("Unable to contact node registration service", cause) class NodeRegistrationHelper(private val config: NodeConfiguration, certService: NetworkRegistrationService, regConfig: NodeRegistrationOption, computeNextIdleDoormanConnectionPollInterval: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1))) : - NetworkRegistrationHelper(config, + NetworkRegistrationHelper( + config.certificatesDirectory, + config.signingCertificateStore, config.myLegalName, config.emailAddress, certService, @@ -263,7 +274,7 @@ class NodeRegistrationHelper(private val config: NodeConfiguration, certService: } private fun createSSLKeystore(nodeCAKeyPair: KeyPair, certificates: List<X509Certificate>, tlsCertCrlIssuer: X500Name?) { - config.loadSslKeyStore(createNew = true).update { + config.p2pSslOptions.keyStore.get(createNew = true).update { println("Generating SSL certificate for node messaging service.") val sslKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val sslCert = X509Utilities.createCertificate( @@ -277,17 +288,26 @@ class NodeRegistrationHelper(private val config: NodeConfiguration, certService: logger.info("Generated TLS certificate: $sslCert") setPrivateKey(CORDA_CLIENT_TLS, sslKeyPair.private, listOf(sslCert) + certificates) } - println("SSL private key and certificate stored in ${config.sslKeystore}.") + println("SSL private key and certificate stored in ${config.p2pSslOptions.keyStore.path}.") } private fun createTruststore(rootCertificate: X509Certificate) { // Save root certificates to trust store. - config.loadTrustStore(createNew = true).update { + config.p2pSslOptions.trustStore.get(createNew = true).update { + if (this.aliases().hasNext()) { + logger.warn("The node's trust store already exists. The following certificates will be overridden: ${this.aliases().asSequence()}") + } println("Generating trust store for corda node.") // Assumes certificate chain always starts with client certificate and end with root certificate. setCertificate(CORDA_ROOT_CA, rootCertificate) + // Copy remaining certificates from the network-trust-store + rootTrustStore.aliases().asSequence().filter { it != CORDA_ROOT_CA }.forEach { + val certificate = rootTrustStore.getCertificate(it) + logger.info("Copying trusted certificate to the node's trust store: Alias: $it, Certificate: $certificate") + setCertificate(it, certificate) + } } - println("Node trust store stored in ${config.trustStoreFile}.") + println("Node trust store stored in ${config.p2pSslOptions.trustStore.path}.") } override fun validateAndGetTlsCrlIssuerCert(): X509Certificate? { @@ -296,8 +316,9 @@ class NodeRegistrationHelper(private val config: NodeConfiguration, certService: if (principalMatchesCertificatePrincipal(tlsCertCrlIssuer, rootCert)) { return rootCert } - return if (config.trustStoreFile.exists()) { - findMatchingCertificate(tlsCertCrlIssuer, config.loadTrustStore()) + val trustStore = config.p2pSslOptions.trustStore.getOptional() + return if (trustStore != null) { + findMatchingCertificate(tlsCertCrlIssuer, trustStore.value) } else { null } diff --git a/node/src/main/resources/log4j2.component.properties b/node/src/main/resources/log4j2.component.properties new file mode 100644 index 0000000000..1b55982139 --- /dev/null +++ b/node/src/main/resources/log4j2.component.properties @@ -0,0 +1,2 @@ +Log4jContextSelector=net.corda.node.utilities.logging.AsyncLoggerContextSelectorNoThreadLocal +AsyncLogger.RingBufferSize=262144 \ No newline at end of file diff --git a/node/src/main/resources/migration/node-core.changelog-tx-mapping.xml b/node/src/main/resources/migration/node-core.changelog-tx-mapping.xml index 65ff155ac6..6ea7eb39d5 100644 --- a/node/src/main/resources/migration/node-core.changelog-tx-mapping.xml +++ b/node/src/main/resources/migration/node-core.changelog-tx-mapping.xml @@ -3,6 +3,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"> <changeSet author="R3.Corda" id="add_tx_mapping_column"> + <preConditions onFail="MARK_RAN"><not><columnExists tableName="node_transactions" columnName="state_machine_run_id"/></not></preConditions> <addColumn tableName="node_transactions"> <column name="state_machine_run_id" type="NVARCHAR(36)"> <constraints nullable="true"/> diff --git a/node/src/main/resources/migration/vault-schema.changelog-v5.xml b/node/src/main/resources/migration/vault-schema.changelog-v5.xml index 6ed365a363..dbb3ba0121 100644 --- a/node/src/main/resources/migration/vault-schema.changelog-v5.xml +++ b/node/src/main/resources/migration/vault-schema.changelog-v5.xml @@ -1,15 +1,19 @@ <?xml version="1.1" encoding="UTF-8" standalone="no"?> <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" - xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"> - <changeSet author="R3.Corda" id="add_is_relevant_column"> - <addColumn tableName="vault_states"> - <column name="is_relevant" type="INT"/> - </addColumn> - <update tableName="vault_states"> - <column name="is_relevant" valueNumeric="0"/> - </update> - <addNotNullConstraint tableName="vault_states" columnName="is_relevant" columnDataType="INT" /> + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"> + <changeSet author="R3.Corda" id="add_relevancy_status_column"> + <preConditions onFail="MARK_RAN"> + <not> + <columnExists tableName="vault_states" columnName="relevancy_status"/> + </not> + </preConditions> + <addColumn tableName="vault_states"> + <column name="relevancy_status" type="INT"/> + </addColumn> + <update tableName="vault_states"> + <column name="relevancy_status" valueNumeric="0"/> + </update> + <addNotNullConstraint tableName="vault_states" columnName="relevancy_status" columnDataType="INT"/> </changeSet> </databaseChangeLog> diff --git a/node/src/main/resources/net.corda.node.internal.NodeStartup.yml b/node/src/main/resources/net.corda.node.internal.NodeStartup.yml new file mode 100644 index 0000000000..a67e7cee5a --- /dev/null +++ b/node/src/main/resources/net.corda.node.internal.NodeStartup.yml @@ -0,0 +1,141 @@ +- commandName: "<main class>" + positionalParams: [] + params: + - parameterName: "--base-directory" + parameterType: "java.nio.file.Path" + required: false + multiParam: true + acceptableValues: [] + - parameterName: "--bootstrap-raft-cluster" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--clear-network-map-cache" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--config-file" + parameterType: "java.nio.file.Path" + required: false + multiParam: true + acceptableValues: [] + - parameterName: "--dev-mode" + parameterType: "java.lang.Boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--initial-registration" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--install-shell-extensions" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--just-generate-node-info" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--just-generate-rpc-ssl-settings" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--log-to-console" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--logging-level" + parameterType: "org.slf4j.event.Level" + required: false + multiParam: false + acceptableValues: + - "ERROR" + - "WARN" + - "INFO" + - "DEBUG" + - "TRACE" + - parameterName: "--network-root-truststore" + parameterType: "java.nio.file.Path" + required: false + multiParam: true + acceptableValues: [] + - parameterName: "--network-root-truststore-password" + parameterType: "java.lang.String" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--no-local-shell" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--on-unknown-config-keys" + parameterType: "net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy" + required: false + multiParam: false + acceptableValues: + - "FAIL" + - "WARN" + - "IGNORE" + - parameterName: "--sshd" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--sshd-port" + parameterType: "int" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--verbose" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "-b" + parameterType: "java.nio.file.Path" + required: false + multiParam: true + acceptableValues: [] + - parameterName: "-c" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "-d" + parameterType: "java.lang.Boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "-f" + parameterType: "java.nio.file.Path" + required: false + multiParam: true + acceptableValues: [] + - parameterName: "-n" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "-p" + parameterType: "java.lang.String" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "-t" + parameterType: "java.nio.file.Path" + required: false + multiParam: true + acceptableValues: [] + - parameterName: "-v" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] \ No newline at end of file diff --git a/node/src/main/resources/reference.conf b/node/src/main/resources/reference.conf index e1ba8a3ed0..a09a38df1d 100644 --- a/node/src/main/resources/reference.conf +++ b/node/src/main/resources/reference.conf @@ -3,6 +3,7 @@ keyStorePassword = "cordacadevpass" trustStorePassword = "trustpass" crlCheckSoftFail = true lazyBridgeStart = true +additionalP2PAddresses = [] dataSourceProperties = { dataSourceClassName = org.h2.jdbcx.JdbcDataSource dataSource.url = "jdbc:h2:file:"${baseDirectory}"/persistence;DB_CLOSE_ON_EXIT=FALSE;WRITE_DELAY=0;LOCK_TIMEOUT=10000" diff --git a/node/src/test/kotlin/net/corda/node/NodeArgsParserTest.kt b/node/src/test/kotlin/net/corda/node/NodeArgsParserTest.kt deleted file mode 100644 index bdd34d7bba..0000000000 --- a/node/src/test/kotlin/net/corda/node/NodeArgsParserTest.kt +++ /dev/null @@ -1,189 +0,0 @@ -package net.corda.node - -import joptsimple.OptionException -import net.corda.core.internal.delete -import net.corda.core.internal.div -import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy -import net.corda.nodeapi.internal.crypto.X509KeyStore -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.assertThatExceptionOfType -import org.junit.BeforeClass -import org.junit.Test -import org.slf4j.event.Level -import java.nio.file.Path -import java.nio.file.Paths -import kotlin.test.assertEquals -import kotlin.test.assertNotNull - -class NodeArgsParserTest { - private val parser = NodeArgsParser() - - companion object { - private lateinit var workingDirectory: Path - private lateinit var buildDirectory: Path - - @BeforeClass - @JvmStatic - fun initDirectories() { - workingDirectory = Paths.get(".").normalize().toAbsolutePath() - buildDirectory = workingDirectory.resolve("build") - } - } - - @Test - fun `no command line arguments`() { - assertThat(parser.parse()).isEqualTo(CmdLineOptions( - baseDirectory = workingDirectory, - configFile = workingDirectory / "node.conf", - logToConsole = false, - loggingLevel = Level.INFO, - nodeRegistrationOption = null, - isVersion = false, - noLocalShell = false, - sshdServer = false, - justGenerateNodeInfo = false, - justGenerateRpcSslCerts = false, - bootstrapRaftCluster = false, - unknownConfigKeysPolicy = UnknownConfigKeysPolicy.FAIL, - devMode = false, - clearNetworkMapCache = false)) - } - - @Test - fun `base-directory with relative path`() { - val expectedBaseDir = Paths.get("tmp").normalize().toAbsolutePath() - val cmdLineOptions = parser.parse("--base-directory", "tmp") - assertThat(cmdLineOptions.baseDirectory).isEqualTo(expectedBaseDir) - assertThat(cmdLineOptions.configFile).isEqualTo(expectedBaseDir / "node.conf") - } - - @Test - fun `base-directory with absolute path`() { - val baseDirectory = Paths.get("tmp").normalize().toAbsolutePath() - val cmdLineOptions = parser.parse("--base-directory", baseDirectory.toString()) - assertThat(cmdLineOptions.baseDirectory).isEqualTo(baseDirectory) - assertThat(cmdLineOptions.configFile).isEqualTo(baseDirectory / "node.conf") - } - - @Test - fun `config-file with relative path`() { - val cmdLineOptions = parser.parse("--config-file", "different.conf") - assertThat(cmdLineOptions.baseDirectory).isEqualTo(workingDirectory) - assertThat(cmdLineOptions.configFile).isEqualTo(workingDirectory / "different.conf") - } - - @Test - fun `config-file with absolute path`() { - val configFile = Paths.get("tmp", "a.conf").normalize().toAbsolutePath() - val cmdLineOptions = parser.parse("--config-file", configFile.toString()) - assertThat(cmdLineOptions.baseDirectory).isEqualTo(workingDirectory) - assertThat(cmdLineOptions.configFile).isEqualTo(configFile) - } - - @Test - fun `base-directory without argument`() { - assertThatExceptionOfType(OptionException::class.java).isThrownBy { - parser.parse("--base-directory") - }.withMessageContaining("base-directory") - } - - @Test - fun `config-file without argument`() { - assertThatExceptionOfType(OptionException::class.java).isThrownBy { - parser.parse("--config-file") - }.withMessageContaining("config-file") - } - - @Test - fun `log-to-console`() { - val cmdLineOptions = parser.parse("--log-to-console") - assertThat(cmdLineOptions.logToConsole).isTrue() - } - - @Test - fun `logging-level`() { - for (level in Level.values()) { - val cmdLineOptions = parser.parse("--logging-level", level.name) - assertThat(cmdLineOptions.loggingLevel).isEqualTo(level) - } - } - - @Test - fun `logging-level without argument`() { - assertThatExceptionOfType(OptionException::class.java).isThrownBy { - parser.parse("--logging-level") - }.withMessageContaining("logging-level") - } - - @Test - fun `logging-level with invalid argument`() { - assertThatExceptionOfType(OptionException::class.java).isThrownBy { - parser.parse("--logging-level", "not-a-level") - }.withMessageContaining("logging-level") - } - - @Test - fun `initial-registration`() { - // Create this temporary file in the "build" directory so that "clean" can delete it. - val truststorePath = buildDirectory / "truststore" / "file.jks" - assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy { - parser.parse("--initial-registration", "--network-root-truststore", "$truststorePath", "--network-root-truststore-password", "password-test") - }.withMessageContaining("Network root trust store path").withMessageContaining("doesn't exist") - - X509KeyStore.fromFile(truststorePath, "dummy_password", createNew = true) - try { - val cmdLineOptions = parser.parse("--initial-registration", "--network-root-truststore", "$truststorePath", "--network-root-truststore-password", "password-test") - assertNotNull(cmdLineOptions.nodeRegistrationOption) - assertEquals(truststorePath.toAbsolutePath(), cmdLineOptions.nodeRegistrationOption?.networkRootTrustStorePath) - assertEquals("password-test", cmdLineOptions.nodeRegistrationOption?.networkRootTrustStorePassword) - } finally { - truststorePath.delete() - } - } - - @Test - fun version() { - val cmdLineOptions = parser.parse("--version") - assertThat(cmdLineOptions.isVersion).isTrue() - } - - @Test - fun `generate node infos`() { - val cmdLineOptions = parser.parse("--just-generate-node-info") - assertThat(cmdLineOptions.justGenerateNodeInfo).isTrue() - } - - @Test - fun `clear network map cache`() { - val cmdLineOptions = parser.parse("--clear-network-map-cache") - assertThat(cmdLineOptions.clearNetworkMapCache).isTrue() - } - - @Test - fun `bootstrap raft cluster`() { - val cmdLineOptions = parser.parse("--bootstrap-raft-cluster") - assertThat(cmdLineOptions.bootstrapRaftCluster).isTrue() - } - - @Test - fun `on-unknown-config-keys options`() { - UnknownConfigKeysPolicy.values().forEach { onUnknownConfigKeyPolicy -> - val cmdLineOptions = parser.parse("--on-unknown-config-keys", onUnknownConfigKeyPolicy.name) - assertThat(cmdLineOptions.unknownConfigKeysPolicy).isEqualTo(onUnknownConfigKeyPolicy) - } - } - - @Test - fun `invalid argument`() { - assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy { - parser.parse("foo") - }.withMessageContaining("Unrecognized argument(s): foo") - } - - @Test - fun `invalid arguments`() { - assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy { - parser.parse("foo", "bar") - }.withMessageContaining("Unrecognized argument(s): foo, bar") - } -} diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeStartupCompatibilityTest.kt b/node/src/test/kotlin/net/corda/node/internal/NodeStartupCompatibilityTest.kt new file mode 100644 index 0000000000..283814b936 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/internal/NodeStartupCompatibilityTest.kt @@ -0,0 +1,5 @@ +package net.corda.node.internal + +import net.corda.testing.CliBackwardsCompatibleTest + +class NodeStartupCompatibilityTest : CliBackwardsCompatibleTest(NodeStartup::class.java) \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeStartupTest.kt b/node/src/test/kotlin/net/corda/node/internal/NodeStartupTest.kt new file mode 100644 index 0000000000..947c6f0e73 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/internal/NodeStartupTest.kt @@ -0,0 +1,52 @@ +package net.corda.node.internal + +import net.corda.core.internal.div +import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy +import org.assertj.core.api.Assertions.assertThat +import org.junit.BeforeClass +import org.junit.Test +import org.slf4j.event.Level +import picocli.CommandLine +import java.nio.file.Path +import java.nio.file.Paths + +class NodeStartupTest { + private val startup = NodeStartup() + + companion object { + private lateinit var workingDirectory: Path + + @BeforeClass + @JvmStatic + fun initDirectories() { + workingDirectory = Paths.get(".").normalize().toAbsolutePath() + } + } + + @Test + fun `no command line arguments`() { + CommandLine.populateCommand(startup) + assertThat(startup.cmdLineOptions.baseDirectory).isEqualTo(workingDirectory) + assertThat(startup.cmdLineOptions.configFile).isEqualTo(workingDirectory / "node.conf") + assertThat(startup.verbose).isEqualTo(false) + assertThat(startup.loggingLevel).isEqualTo(Level.INFO) + assertThat(startup.cmdLineOptions.nodeRegistrationOption).isEqualTo(null) + assertThat(startup.cmdLineOptions.noLocalShell).isEqualTo(false) + assertThat(startup.cmdLineOptions.sshdServer).isEqualTo(false) + assertThat(startup.cmdLineOptions.justGenerateNodeInfo).isEqualTo(false) + assertThat(startup.cmdLineOptions.justGenerateRpcSslCerts).isEqualTo(false) + assertThat(startup.cmdLineOptions.bootstrapRaftCluster).isEqualTo(false) + assertThat(startup.cmdLineOptions.unknownConfigKeysPolicy).isEqualTo(UnknownConfigKeysPolicy.FAIL) + assertThat(startup.cmdLineOptions.devMode).isEqualTo(null) + assertThat(startup.cmdLineOptions.clearNetworkMapCache).isEqualTo(false) + assertThat(startup.cmdLineOptions.networkRootTrustStorePath).isEqualTo(workingDirectory / "certificates" / "network-root-truststore.jks") + } + + @Test + fun `--base-directory`() { + CommandLine.populateCommand(startup, "--base-directory", (workingDirectory / "another-base-dir").toString()) + assertThat(startup.cmdLineOptions.baseDirectory).isEqualTo(workingDirectory / "another-base-dir") + assertThat(startup.cmdLineOptions.configFile).isEqualTo(workingDirectory / "another-base-dir" / "node.conf") + assertThat(startup.cmdLineOptions.networkRootTrustStorePath).isEqualTo(workingDirectory / "another-base-dir" / "certificates" / "network-root-truststore.jks") + } +} diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt b/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt index f0e9b2b17d..6134aa348f 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt @@ -58,12 +58,9 @@ class NodeTest { @Test fun `generateAndSaveNodeInfo works`() { val configuration = createConfig(ALICE_NAME) - val platformVersion = 789 + val info = VersionInfo(789, "3.0", "SNAPSHOT", "R3") configureDatabase(configuration.dataSourceProperties, configuration.database, { null }, { null }).use { - val versionInfo = rigorousMock<VersionInfo>().also { - doReturn(platformVersion).whenever(it).platformVersion - } - val node = Node(configuration, versionInfo, initialiseSerialization = false) + val node = Node(configuration, info, initialiseSerialization = false) assertEquals(node.generateNodeInfo(), node.generateNodeInfo()) // Node info doesn't change (including the serial) } } @@ -87,9 +84,8 @@ class NodeTest { // Save some NodeInfo session.save(persistentNodeInfo) } - val node = Node(configuration, rigorousMock<VersionInfo>().also { - doReturn(10).whenever(it).platformVersion - }, initialiseSerialization = false) + val versionInfo = VersionInfo(10, "3.0", "SNAPSHOT", "R3") + val node = Node(configuration, versionInfo, initialiseSerialization = false) assertThat(getAllInfos(it)).isNotEmpty node.clearNetworkMapCache() assertThat(getAllInfos(it)).isEmpty() @@ -97,7 +93,7 @@ class NodeTest { } @Test - fun `Node can start with multiple keypairs for it's identity`() { + fun `Node can start with multiple keypairs for its identity`() { val configuration = createConfig(ALICE_NAME) val (nodeInfo1, _) = createNodeInfoAndSigned(ALICE_NAME) val (nodeInfo2, _) = createNodeInfoAndSigned(ALICE_NAME) @@ -135,6 +131,8 @@ class NodeTest { val node = Node(configuration, rigorousMock<VersionInfo>().also { doReturn(10).whenever(it).platformVersion + doReturn("test-vendor").whenever(it).vendor + doReturn("1.0").whenever(it).releaseVersion }, initialiseSerialization = false) //this throws an exception with old behaviour diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt index 8d1f712399..d622f834f8 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt @@ -3,6 +3,8 @@ package net.corda.node.internal.cordapp import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import net.corda.core.node.services.AttachmentStorage +import net.corda.node.VersionInfo +import net.corda.nodeapi.internal.PLATFORM_VERSION import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.internal.MockCordappConfigProvider import net.corda.testing.services.MockAttachmentStorage @@ -75,7 +77,7 @@ class CordappProviderImplTests { fun `test cordapp configuration`() { val configProvider = MockCordappConfigProvider() configProvider.cordappConfigs[isolatedCordappName] = validConfig - val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR)) + val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR), VersionInfo.UNKNOWN) val provider = CordappProviderImpl(loader, configProvider, attachmentStore).apply { start(whitelistedContractImplementations) } val expected = provider.getAppContext(provider.cordapps.first()).config @@ -84,7 +86,7 @@ class CordappProviderImplTests { } private fun newCordappProvider(vararg urls: URL): CordappProviderImpl { - val loader = JarScanningCordappLoader.fromJarUrls(urls.toList()) + val loader = JarScanningCordappLoader.fromJarUrls(urls.toList(), VersionInfo.UNKNOWN) return CordappProviderImpl(loader, stubConfigProvider, attachmentStore).apply { start(whitelistedContractImplementations) } } } diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt index 1ab3482533..e71786f8b6 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt @@ -2,7 +2,9 @@ package net.corda.node.internal.cordapp import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.* +import net.corda.node.VersionInfo import net.corda.node.cordapp.CordappLoader +import net.corda.nodeapi.internal.PLATFORM_VERSION import net.corda.testing.node.internal.cordappsForPackages import net.corda.testing.node.internal.getTimestampAsDirectoryName import net.corda.testing.node.internal.packageInDirectory @@ -46,7 +48,7 @@ class JarScanningCordappLoaderTest { fun `test that classes that aren't in cordapps aren't loaded`() { // Basedir will not be a corda node directory so the dummy flow shouldn't be recognised as a part of a cordapp val loader = JarScanningCordappLoader.fromDirectories(listOf(Paths.get("."))) - assertThat(loader.cordapps).containsOnly(JarScanningCordappLoader.coreCordapp) + assertThat(loader.cordapps).containsOnly(loader.coreCordapp) } @Test @@ -57,7 +59,7 @@ class JarScanningCordappLoaderTest { val actual = loader.cordapps.toTypedArray() assertThat(actual).hasSize(2) - val actualCordapp = actual.single { it != JarScanningCordappLoader.coreCordapp } + val actualCordapp = actual.single { it != loader.coreCordapp } assertThat(actualCordapp.contractClassNames).isEqualTo(listOf(isolatedContractId)) assertThat(actualCordapp.initiatedFlows.single().name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Acceptor") assertThat(actualCordapp.rpcFlows).isEmpty() @@ -91,7 +93,7 @@ class JarScanningCordappLoaderTest { @Test fun `sub-packages are ignored`() { - val loader = cordappLoaderForPackages(listOf("net.corda", testScanPackage)) + val loader = cordappLoaderForPackages(listOf("net.corda.core", testScanPackage)) val cordapps = loader.cordapps.filter { LoaderTestFlow::class.java in it.initiatedFlows } assertThat(cordapps).hasSize(1) } @@ -101,13 +103,70 @@ class JarScanningCordappLoaderTest { @Test fun `cordapp classloader can load cordapp classes`() { val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("isolated.jar")!! - val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR)) + val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR), VersionInfo.UNKNOWN) loader.appClassLoader.loadClass(isolatedContractId) loader.appClassLoader.loadClass(isolatedFlowName) } - private fun cordappLoaderForPackages(packages: Iterable<String>): CordappLoader { + @Test + fun `cordapp classloader sets target and min version to 1 if not specified`() { + val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/no-min-or-target-version.jar")!! + val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN) + loader.cordapps.filter { !it.info.shortName.equals("corda-core") }.forEach { + assertThat(it.info.targetPlatformVersion).isEqualTo(1) + assertThat(it.info.minimumPlatformVersion).isEqualTo(1) + } + } + + @Test + fun `cordapp classloader returns correct values for minPlatformVersion and targetVersion`() { + // load jar with min and target version in manifest + // make sure classloader extracts correct values + val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!! + val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN) + // exclude the core cordapp + val cordapp = loader.cordapps.filter { it.cordappClasses.contains("net.corda.core.internal.cordapp.CordappImpl")}.single() + assertThat(cordapp.info.targetPlatformVersion).isEqualTo(3) + assertThat(cordapp.info.minimumPlatformVersion).isEqualTo(2) + } + + @Test + fun `cordapp classloader sets target version to min version if target version is not specified`() { + // load jar with minVersion but not targetVersion in manifest + val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-no-target.jar")!! + val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN) + // exclude the core cordapp + val cordapp = loader.cordapps.filter { it.cordappClasses.contains("net.corda.core.internal.cordapp.CordappImpl")}.single() + assertThat(cordapp.info.targetPlatformVersion).isEqualTo(2) + assertThat(cordapp.info.minimumPlatformVersion).isEqualTo(2) + } + + @Test + fun `cordapp classloader does not load apps when their min platform version is greater than the platform version`() { + val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!! + val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1)) + // exclude the core cordapp + assertThat(loader.cordapps.size).isEqualTo(1) + } + + @Test + fun `cordapp classloader does load apps when their min platform version is less than the platform version`() { + val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!! + val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1000)) + // exclude the core cordapp + assertThat(loader.cordapps.size).isEqualTo(2) + } + + @Test + fun `cordapp classloader does load apps when their min platform version is equal to the platform version`() { + val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!! + val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 2)) + // exclude the core cordapp + assertThat(loader.cordapps.size).isEqualTo(2) + } + + private fun cordappLoaderForPackages(packages: Iterable<String>, versionInfo: VersionInfo = VersionInfo.UNKNOWN): CordappLoader { val cordapps = cordappsForPackages(packages) return testDirectory().let { directory -> diff --git a/node/src/test/kotlin/net/corda/node/serialization/kryo/KryoTests.kt b/node/src/test/kotlin/net/corda/node/serialization/kryo/KryoTests.kt index b15374b7ac..5598f38f67 100644 --- a/node/src/test/kotlin/net/corda/node/serialization/kryo/KryoTests.kt +++ b/node/src/test/kotlin/net/corda/node/serialization/kryo/KryoTests.kt @@ -5,7 +5,6 @@ import com.esotericsoftware.kryo.KryoException import com.esotericsoftware.kryo.KryoSerializable import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output -import com.esotericsoftware.kryo.pool.KryoPool import com.google.common.primitives.Ints import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever @@ -13,6 +12,10 @@ import net.corda.core.contracts.PrivacySalt import net.corda.core.crypto.* import net.corda.core.internal.FetchDataFlow import net.corda.core.serialization.* +import net.corda.core.serialization.internal.CheckpointSerializationContext +import net.corda.core.serialization.internal.CheckpointSerializationFactory +import net.corda.core.serialization.internal.checkpointDeserialize +import net.corda.core.serialization.internal.checkpointSerialize import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.sequence @@ -36,16 +39,6 @@ import java.util.* import kotlin.collections.ArrayList import kotlin.test.* -class TestScheme : AbstractKryoSerializationScheme() { - override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean { - return magic == kryoMagic && target != SerializationContext.UseCase.RPCClient - } - - override fun rpcClientKryoPool(context: SerializationContext): KryoPool = throw UnsupportedOperationException() - - override fun rpcServerKryoPool(context: SerializationContext): KryoPool = throw UnsupportedOperationException() -} - @RunWith(Parameterized::class) class KryoTests(private val compression: CordaSerializationEncoding?) { companion object { @@ -55,18 +48,17 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { fun compression() = arrayOf<CordaSerializationEncoding?>(null) + CordaSerializationEncoding.values() } - private lateinit var factory: SerializationFactory - private lateinit var context: SerializationContext + private lateinit var factory: CheckpointSerializationFactory + private lateinit var context: CheckpointSerializationContext @Before fun setup() { - factory = SerializationFactoryImpl().apply { registerScheme(TestScheme()) } - context = SerializationContextImpl(kryoMagic, + factory = CheckpointSerializationFactory(KryoSerializationScheme) + context = CheckpointSerializationContextImpl( javaClass.classLoader, AllWhitelist, emptyMap(), true, - SerializationContext.UseCase.Storage, compression, rigorousMock<EncodingWhitelist>().also { if (compression != null) doReturn(true).whenever(it).acceptEncoding(compression) @@ -77,15 +69,15 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { fun `simple data class`() { val birthday = Instant.parse("1984-04-17T00:30:00.00Z") val mike = Person("mike", birthday) - val bits = mike.serialize(factory, context) - assertThat(bits.deserialize(factory, context)).isEqualTo(Person("mike", birthday)) + val bits = mike.checkpointSerialize(factory, context) + assertThat(bits.checkpointDeserialize(factory, context)).isEqualTo(Person("mike", birthday)) } @Test fun `null values`() { val bob = Person("bob", null) - val bits = bob.serialize(factory, context) - assertThat(bits.deserialize(factory, context)).isEqualTo(Person("bob", null)) + val bits = bob.checkpointSerialize(factory, context) + assertThat(bits.checkpointDeserialize(factory, context)).isEqualTo(Person("bob", null)) } @Test @@ -93,10 +85,10 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { val noReferencesContext = context.withoutReferences() val obj : ByteSequence = Ints.toByteArray(0x01234567).sequence() val originalList : ArrayList<ByteSequence> = ArrayList<ByteSequence>().apply { this += obj } - val deserialisedList = originalList.serialize(factory, noReferencesContext).deserialize(factory, noReferencesContext) + val deserialisedList = originalList.checkpointSerialize(factory, noReferencesContext).checkpointDeserialize(factory, noReferencesContext) originalList += obj deserialisedList += obj - assertThat(deserialisedList.serialize(factory, noReferencesContext)).isEqualTo(originalList.serialize(factory, noReferencesContext)) + assertThat(deserialisedList.checkpointSerialize(factory, noReferencesContext)).isEqualTo(originalList.checkpointSerialize(factory, noReferencesContext)) } @Test @@ -113,14 +105,14 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { this += instant this += instant } - assertThat(listWithSameInstances.serialize(factory, noReferencesContext)).isEqualTo(listWithCopies.serialize(factory, noReferencesContext)) + assertThat(listWithSameInstances.checkpointSerialize(factory, noReferencesContext)).isEqualTo(listWithCopies.checkpointSerialize(factory, noReferencesContext)) } @Test fun `cyclic object graph`() { val cyclic = Cyclic(3) - val bits = cyclic.serialize(factory, context) - assertThat(bits.deserialize(factory, context)).isEqualTo(cyclic) + val bits = cyclic.checkpointSerialize(factory, context) + assertThat(bits.checkpointDeserialize(factory, context)).isEqualTo(cyclic) } @Test @@ -132,7 +124,7 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { signature.verify(bitsToSign) assertThatThrownBy { signature.verify(wrongBits) } - val deserialisedKeyPair = keyPair.serialize(factory, context).deserialize(factory, context) + val deserialisedKeyPair = keyPair.checkpointSerialize(factory, context).checkpointDeserialize(factory, context) val deserialisedSignature = deserialisedKeyPair.sign(bitsToSign) deserialisedSignature.verify(bitsToSign) assertThatThrownBy { deserialisedSignature.verify(wrongBits) } @@ -140,28 +132,28 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { @Test fun `write and read Kotlin object singleton`() { - val serialised = TestSingleton.serialize(factory, context) - val deserialised = serialised.deserialize(factory, context) + val serialised = TestSingleton.checkpointSerialize(factory, context) + val deserialised = serialised.checkpointDeserialize(factory, context) assertThat(deserialised).isSameAs(TestSingleton) } @Test fun `check Kotlin EmptyList can be serialised`() { - val deserialisedList: List<Int> = emptyList<Int>().serialize(factory, context).deserialize(factory, context) + val deserialisedList: List<Int> = emptyList<Int>().checkpointSerialize(factory, context).checkpointDeserialize(factory, context) assertEquals(0, deserialisedList.size) assertEquals<Any>(Collections.emptyList<Int>().javaClass, deserialisedList.javaClass) } @Test fun `check Kotlin EmptySet can be serialised`() { - val deserialisedSet: Set<Int> = emptySet<Int>().serialize(factory, context).deserialize(factory, context) + val deserialisedSet: Set<Int> = emptySet<Int>().checkpointSerialize(factory, context).checkpointDeserialize(factory, context) assertEquals(0, deserialisedSet.size) assertEquals<Any>(Collections.emptySet<Int>().javaClass, deserialisedSet.javaClass) } @Test fun `check Kotlin EmptyMap can be serialised`() { - val deserialisedMap: Map<Int, Int> = emptyMap<Int, Int>().serialize(factory, context).deserialize(factory, context) + val deserialisedMap: Map<Int, Int> = emptyMap<Int, Int>().checkpointSerialize(factory, context).checkpointDeserialize(factory, context) assertEquals(0, deserialisedMap.size) assertEquals<Any>(Collections.emptyMap<Int, Int>().javaClass, deserialisedMap.javaClass) } @@ -169,7 +161,7 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { @Test fun `InputStream serialisation`() { val rubbish = ByteArray(12345) { (it * it * 0.12345).toByte() } - val readRubbishStream: InputStream = rubbish.inputStream().serialize(factory, context).deserialize(factory, context) + val readRubbishStream: InputStream = rubbish.inputStream().checkpointSerialize(factory, context).checkpointDeserialize(factory, context) for (i in 0..12344) { assertEquals(rubbish[i], readRubbishStream.read().toByte()) } @@ -179,7 +171,7 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { @Test fun `InputStream serialisation does not write trailing garbage`() { val byteArrays = listOf("123", "456").map { it.toByteArray() } - val streams = byteArrays.map { it.inputStream() }.serialize(factory, context).deserialize(factory, context).iterator() + val streams = byteArrays.map { it.inputStream() }.checkpointSerialize(factory, context).checkpointDeserialize(factory, context).iterator() byteArrays.forEach { assertArrayEquals(it, streams.next().readBytes()) } assertFalse(streams.hasNext()) } @@ -190,16 +182,16 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { val testBytes = testString.toByteArray() val meta = SignableData(testBytes.sha256(), SignatureMetadata(1, Crypto.findSignatureScheme(ALICE_PUBKEY).schemeNumberID)) - val serializedMetaData = meta.serialize(factory, context).bytes - val meta2 = serializedMetaData.deserialize<SignableData>(factory, context) + val serializedMetaData = meta.checkpointSerialize(factory, context).bytes + val meta2 = serializedMetaData.checkpointDeserialize<SignableData>(factory, context) assertEquals(meta2, meta) } @Test fun `serialize - deserialize Logger`() { - val storageContext: SerializationContext = context // TODO: make it storage context + val storageContext: CheckpointSerializationContext = context val logger = LoggerFactory.getLogger("aName") - val logger2 = logger.serialize(factory, storageContext).deserialize(factory, storageContext) + val logger2 = logger.checkpointSerialize(factory, storageContext).checkpointDeserialize(factory, storageContext) assertEquals(logger.name, logger2.name) assertTrue(logger === logger2) } @@ -211,7 +203,7 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { SecureHash.sha256(rubbish), rubbish.size, rubbish.inputStream() - ).serialize(factory, context).deserialize(factory, context) + ).checkpointSerialize(factory, context).checkpointDeserialize(factory, context) for (i in 0..12344) { assertEquals(rubbish[i], readRubbishStream.read().toByte()) } @@ -238,8 +230,8 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32 )) - val serializedBytes = expected.serialize(factory, context) - val actual = serializedBytes.deserialize(factory, context) + val serializedBytes = expected.checkpointSerialize(factory, context) + val actual = serializedBytes.checkpointDeserialize(factory, context) assertEquals(expected, actual) } @@ -286,15 +278,14 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { } } Tmp() - val factory = SerializationFactoryImpl().apply { registerScheme(TestScheme()) } - val context = SerializationContextImpl(kryoMagic, + val factory = CheckpointSerializationFactory(KryoSerializationScheme) + val context = CheckpointSerializationContextImpl( javaClass.classLoader, AllWhitelist, emptyMap(), true, - SerializationContext.UseCase.P2P, null) - pt.serialize(factory, context) + pt.checkpointSerialize(factory, context) } @Test @@ -302,7 +293,7 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { val exception = IllegalArgumentException("fooBar") val toBeSuppressedOnSenderSide = IllegalStateException("bazz1") exception.addSuppressed(toBeSuppressedOnSenderSide) - val exception2 = exception.serialize(factory, context).deserialize(factory, context) + val exception2 = exception.checkpointSerialize(factory, context).checkpointDeserialize(factory, context) assertEquals(exception.message, exception2.message) assertEquals(1, exception2.suppressed.size) @@ -317,7 +308,7 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { @Test fun `serialize - deserialize Exception no suppressed`() { val exception = IllegalArgumentException("fooBar") - val exception2 = exception.serialize(factory, context).deserialize(factory, context) + val exception2 = exception.checkpointSerialize(factory, context).checkpointDeserialize(factory, context) assertEquals(exception.message, exception2.message) assertEquals(0, exception2.suppressed.size) @@ -331,7 +322,7 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { fun `serialize - deserialize HashNotFound`() { val randomHash = SecureHash.randomSHA256() val exception = FetchDataFlow.HashNotFound(randomHash) - val exception2 = exception.serialize(factory, context).deserialize(factory, context) + val exception2 = exception.checkpointSerialize(factory, context).checkpointDeserialize(factory, context) assertEquals(randomHash, exception2.requested) } @@ -339,17 +330,17 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { fun `compression has the desired effect`() { compression ?: return val data = ByteArray(12345).also { Random(0).nextBytes(it) }.let { it + it } - val compressed = data.serialize(factory, context) + val compressed = data.checkpointSerialize(factory, context) assertEquals(.5, compressed.size.toDouble() / data.size, .03) - assertArrayEquals(data, compressed.deserialize(factory, context)) + assertArrayEquals(data, compressed.checkpointDeserialize(factory, context)) } @Test fun `a particular encoding can be banned for deserialization`() { compression ?: return doReturn(false).whenever(context.encodingWhitelist).acceptEncoding(compression) - val compressed = "whatever".serialize(factory, context) - catchThrowable { compressed.deserialize(factory, context) }.run { + val compressed = "whatever".checkpointSerialize(factory, context) + catchThrowable { compressed.checkpointDeserialize(factory, context) }.run { assertSame<Any>(KryoException::class.java, javaClass) assertEquals(encodingNotPermittedFormat.format(compression), message) } @@ -360,10 +351,10 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { class Holder(val holder: ByteArray) val obj = Holder(ByteArray(20000)) - val uncompressedSize = obj.serialize(factory, context.withEncoding(null)).size - val compressedSize = obj.serialize(factory, context.withEncoding(CordaSerializationEncoding.SNAPPY)).size + val uncompressedSize = obj.checkpointSerialize(factory, context.withEncoding(null)).size + val compressedSize = obj.checkpointSerialize(factory, context.withEncoding(CordaSerializationEncoding.SNAPPY)).size // If these need fixing, sounds like Kryo wire format changed and checkpoints might not surive an upgrade. assertEquals(20222, uncompressedSize) assertEquals(1111, compressedSize) } -} \ No newline at end of file +} diff --git a/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt index 927608421c..091a47d168 100644 --- a/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt @@ -8,6 +8,7 @@ import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.UnknownAnonymousPartyException import net.corda.node.internal.configureDatabase +import net.corda.node.utilities.TestingNamedCacheFactory import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.x509Certificates @@ -46,7 +47,7 @@ class PersistentIdentityServiceTests { @Before fun setup() { - identityService = PersistentIdentityService() + identityService = PersistentIdentityService(TestingNamedCacheFactory()) database = configureDatabase( makeTestDataSourceProperties(), DatabaseConfig(), @@ -218,7 +219,7 @@ class PersistentIdentityServiceTests { identityService.verifyAndRegisterIdentity(anonymousBob) // Create new identity service mounted onto same DB - val newPersistentIdentityService = PersistentIdentityService().also { + val newPersistentIdentityService = PersistentIdentityService(TestingNamedCacheFactory()).also { it.database = database it.start(DEV_ROOT_CA.certificate) } diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt index de6c357ff2..ca5ae8827c 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt @@ -2,9 +2,9 @@ package net.corda.node.services.network import net.corda.core.crypto.generateKeyPair import net.corda.core.node.services.NetworkMapCache +import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME -import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.testing.core.getTestPartyAndCertificate import net.corda.testing.core.singleIdentity import net.corda.testing.node.internal.InternalMockNetwork diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt index 99c915325d..492f2c39d1 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt @@ -5,6 +5,7 @@ import net.corda.core.crypto.sha256 import net.corda.core.internal.sign import net.corda.core.serialization.serialize import net.corda.core.utilities.seconds +import net.corda.node.VersionInfo import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME @@ -40,7 +41,8 @@ class NetworkMapClientTest { fun setUp() { server = NetworkMapServer(cacheTimeout) val address = server.start() - networkMapClient = NetworkMapClient(URL("http://$address")).apply { start(DEV_ROOT_CA.certificate) } + networkMapClient = NetworkMapClient(URL("http://$address"), + VersionInfo(1, "TEST", "TEST", "TEST")).apply { start(DEV_ROOT_CA.certificate) } } @After diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt index ff4de39797..5cf1aa2b02 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt @@ -9,10 +9,12 @@ import net.corda.core.crypto.sign import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.* +import net.corda.core.internal.concurrent.openFuture import net.corda.core.messaging.ParametersUpdateInfo import net.corda.core.node.NodeInfo import net.corda.core.serialization.serialize import net.corda.core.utilities.millis +import net.corda.node.VersionInfo import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY import net.corda.nodeapi.internal.NodeInfoAndSigned @@ -26,8 +28,8 @@ import net.corda.testing.internal.DEV_ROOT_CA import net.corda.testing.internal.TestNodeInfoBuilder import net.corda.testing.internal.createNodeInfoAndSigned import net.corda.testing.node.internal.network.NetworkMapServer -import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.After import org.junit.Before import org.junit.Rule @@ -54,6 +56,7 @@ class NetworkMapUpdaterTest { private val nodeInfoDir = baseDir / NODE_INFO_DIRECTORY private val scheduler = TestScheduler() private val fileWatcher = NodeInfoWatcher(baseDir, scheduler) + private val nodeReadyFuture = openFuture<Void?>() private val networkMapCache = createMockNetworkMapCache() private lateinit var server: NetworkMapServer private lateinit var networkMapClient: NetworkMapClient @@ -63,7 +66,8 @@ class NetworkMapUpdaterTest { fun setUp() { server = NetworkMapServer(cacheExpiryMs.millis) val address = server.start() - networkMapClient = NetworkMapClient(URL("http://$address")).apply { start(DEV_ROOT_CA.certificate) } + networkMapClient = NetworkMapClient(URL("http://$address"), + VersionInfo(1, "TEST", "TEST", "TEST")).apply { start(DEV_ROOT_CA.certificate) } } @After @@ -98,16 +102,18 @@ class NetworkMapUpdaterTest { startUpdater() networkMapClient.publish(signedNodeInfo2) + assertThat(nodeReadyFuture).isNotDone() // TODO: Remove sleep in unit test. Thread.sleep(2L * cacheExpiryMs) verify(networkMapCache, times(2)).addNode(any()) verify(networkMapCache, times(1)).addNode(nodeInfo1) verify(networkMapCache, times(1)).addNode(nodeInfo2) + assertThat(nodeReadyFuture).isDone() NodeInfoWatcher.saveToFile(nodeInfoDir, fileNodeInfoAndSigned) networkMapClient.publish(signedNodeInfo3) networkMapClient.publish(signedNodeInfo4) - scheduler.advanceTimeBy(10, TimeUnit.SECONDS) + advanceTime() // TODO: Remove sleep in unit test. Thread.sleep(2L * cacheExpiryMs) // 4 node info from network map, and 1 from file. @@ -134,7 +140,7 @@ class NetworkMapUpdaterTest { networkMapClient.publish(signedNodeInfo4) startUpdater() - scheduler.advanceTimeBy(10, TimeUnit.SECONDS) + advanceTime() // TODO: Remove sleep in unit test. Thread.sleep(2L * cacheExpiryMs) @@ -160,7 +166,7 @@ class NetworkMapUpdaterTest { @Test fun `receive node infos from directory, without a network map`() { - setUpdater() + setUpdater(netMapClient = null) val fileNodeInfoAndSigned = createNodeInfoAndSigned("Info from file") // Not subscribed yet. @@ -169,10 +175,12 @@ class NetworkMapUpdaterTest { startUpdater() NodeInfoWatcher.saveToFile(nodeInfoDir, fileNodeInfoAndSigned) - scheduler.advanceTimeBy(10, TimeUnit.SECONDS) + assertThat(nodeReadyFuture).isNotDone() + advanceTime() verify(networkMapCache, times(1)).addNode(any()) verify(networkMapCache, times(1)).addNode(fileNodeInfoAndSigned.nodeInfo) + assertThat(nodeReadyFuture).isDone() assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfoAndSigned.nodeInfo.serialize().hash) } @@ -221,7 +229,7 @@ class NetworkMapUpdaterTest { fun `fetch nodes from private network`() { setUpdater(extraNetworkMapKeys = listOf(privateNetUUID)) server.addNodesToPrivateNetwork(privateNetUUID, listOf(ALICE_NAME)) - Assertions.assertThatThrownBy { networkMapClient.getNetworkMap(privateNetUUID).payload.nodeInfoHashes } + assertThatThrownBy { networkMapClient.getNetworkMap(privateNetUUID).payload.nodeInfoHashes } .isInstanceOf(IOException::class.java) .hasMessageContaining("Response Code 404") val (aliceInfo, signedAliceInfo) = createNodeInfoAndSigned(ALICE_NAME) // Goes to private network map @@ -243,7 +251,7 @@ class NetworkMapUpdaterTest { NodeInfoWatcher.saveToFile(nodeInfoDir, fileNodeInfoAndSigned1) NodeInfoWatcher.saveToFile(nodeInfoDir, fileNodeInfoAndSigned2) - scheduler.advanceTimeBy(10, TimeUnit.SECONDS) + advanceTime() verify(networkMapCache, times(2)).addNode(any()) verify(networkMapCache, times(1)).addNode(fileNodeInfoAndSigned1.nodeInfo) verify(networkMapCache, times(1)).addNode(fileNodeInfoAndSigned2.nodeInfo) @@ -251,7 +259,7 @@ class NetworkMapUpdaterTest { // Remove one of the nodes val fileName1 = "${NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX}${fileNodeInfoAndSigned1.nodeInfo.legalIdentities[0].name.serialize().hash}" (nodeInfoDir / fileName1).delete() - scheduler.advanceTimeBy(10, TimeUnit.SECONDS) + advanceTime() verify(networkMapCache, times(1)).removeNode(any()) verify(networkMapCache, times(1)).removeNode(fileNodeInfoAndSigned1.nodeInfo) assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfoAndSigned2.signed.raw.hash) @@ -273,14 +281,14 @@ class NetworkMapUpdaterTest { // Publish to network map the one with lower serial. networkMapClient.publish(serverSignedNodeInfo) startUpdater() - scheduler.advanceTimeBy(10, TimeUnit.SECONDS) + advanceTime() verify(networkMapCache, times(1)).addNode(localNodeInfo) Thread.sleep(2L * cacheExpiryMs) // Node from file has higher serial than the one from NetworkMapServer assertThat(networkMapCache.allNodeHashes).containsOnly(localSignedNodeInfo.signed.raw.hash) val fileName = "${NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX}${localNodeInfo.legalIdentities[0].name.serialize().hash}" (nodeInfoDir / fileName).delete() - scheduler.advanceTimeBy(10, TimeUnit.SECONDS) + advanceTime() verify(networkMapCache, times(1)).removeNode(any()) verify(networkMapCache).removeNode(localNodeInfo) Thread.sleep(2L * cacheExpiryMs) @@ -329,7 +337,7 @@ class NetworkMapUpdaterTest { assert(networkMapCache.allNodeHashes.size == 1) networkMapClient.publish(signedNodeInfo2) Thread.sleep(2L * cacheExpiryMs) - scheduler.advanceTimeBy(10, TimeUnit.SECONDS) + advanceTime() verify(networkMapCache, times(1)).addNode(signedNodeInfo2.verified()) verify(networkMapCache, times(1)).removeNode(signedNodeInfo1.verified()) @@ -339,6 +347,7 @@ class NetworkMapUpdaterTest { private fun createMockNetworkMapCache(): NetworkMapCacheInternal { return mock { + on { nodeReady }.thenReturn(nodeReadyFuture) val data = ConcurrentHashMap<Party, NodeInfo>() on { addNode(any()) }.then { val nodeInfo = it.arguments[0] as NodeInfo @@ -357,4 +366,8 @@ class NetworkMapUpdaterTest { private fun createNodeInfoAndSigned(org: String): NodeInfoAndSigned { return createNodeInfoAndSigned(CordaX500Name(org, "London", "GB")) } -} \ No newline at end of file + + private fun advanceTime() { + scheduler.advanceTimeBy(10, TimeUnit.SECONDS) + } +} diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt index 03c277fb07..08fa5c3a85 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt @@ -9,6 +9,7 @@ import net.corda.core.internal.readObject import net.corda.core.serialization.deserialize import net.corda.core.utilities.days import net.corda.core.utilities.seconds +import net.corda.node.VersionInfo import net.corda.node.internal.NetworkParametersReader import net.corda.nodeapi.internal.network.* import net.corda.testing.common.internal.testNetworkParameters @@ -41,7 +42,7 @@ class NetworkParametersReaderTest { fun setUp() { server = NetworkMapServer(cacheTimeout) val address = server.start() - networkMapClient = NetworkMapClient(URL("http://$address")) + networkMapClient = NetworkMapClient(URL("http://$address"), VersionInfo(1, "TEST", "TEST", "TEST")) networkMapClient.start(DEV_ROOT_CA.certificate) } diff --git a/node/src/test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt index 6fc7abc537..034afa9121 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt @@ -36,7 +36,7 @@ class NodeInfoWatcherTest { val tempFolder = TemporaryFolder() private val scheduler = TestScheduler() - private val testSubscriber = TestSubscriber<NodeInfoUpdate>() + private val testSubscriber = TestSubscriber<List<NodeInfoUpdate>>() private lateinit var nodeInfoAndSigned: NodeInfoAndSigned private lateinit var nodeInfoPath: Path @@ -83,7 +83,7 @@ class NodeInfoWatcherTest { val subscription = nodeInfoWatcher.nodeInfoUpdates().subscribe(testSubscriber) try { advanceTime() - val readNodes = testSubscriber.onNextEvents.distinct() + val readNodes = testSubscriber.onNextEvents.distinct().flatten() assertEquals(0, readNodes.size) } finally { subscription.unsubscribe() @@ -98,7 +98,7 @@ class NodeInfoWatcherTest { advanceTime() try { - val readNodes = testSubscriber.onNextEvents.distinct() + val readNodes = testSubscriber.onNextEvents.distinct().flatten() assertEquals(1, readNodes.size) assertEquals(nodeInfoAndSigned.nodeInfo, (readNodes.first() as? NodeInfoUpdate.Add)?.nodeInfo) } finally { @@ -116,7 +116,8 @@ class NodeInfoWatcherTest { // Ensure the watch service is started. advanceTime() // Check no nodeInfos are read. - assertEquals(0, testSubscriber.valueCount) + + assertEquals(0, testSubscriber.onNextEvents.distinct().flatten().size) createNodeInfoFileInPath() advanceTime() @@ -124,7 +125,7 @@ class NodeInfoWatcherTest { // We need the WatchService to report a change and that might not happen immediately. testSubscriber.awaitValueCount(1, 5, TimeUnit.SECONDS) // The same folder can be reported more than once, so take unique values. - val readNodes = testSubscriber.onNextEvents.distinct() + val readNodes = testSubscriber.onNextEvents.distinct().flatten() assertEquals(nodeInfoAndSigned.nodeInfo, (readNodes.first() as? NodeInfoUpdate.Add)?.nodeInfo) } finally { subscription.unsubscribe() diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/AppendOnlyPersistentMapTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/AppendOnlyPersistentMapTest.kt index 69646a6ccc..5ed16e74a8 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/AppendOnlyPersistentMapTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/AppendOnlyPersistentMapTest.kt @@ -5,6 +5,7 @@ import net.corda.core.utilities.loggerFor import net.corda.node.internal.configureDatabase import net.corda.node.services.schema.NodeSchemaService import net.corda.node.utilities.AppendOnlyPersistentMap +import net.corda.node.utilities.TestingNamedCacheFactory import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.junit.After @@ -271,7 +272,8 @@ class AppendOnlyPersistentMapTest(var scenario: Scenario) { ) class TestMap : AppendOnlyPersistentMap<Long, String, PersistentMapEntry, Long>( - "ApoendOnlyPersistentMap_test", + cacheFactory = TestingNamedCacheFactory(), + name = "ApoendOnlyPersistentMap_test", toPersistentEntityKey = { it }, fromPersistentEntity = { Pair(it.key, it.value) }, toPersistentEntity = { key: Long, value: String -> diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt index bc02aa19f0..7fd4072d38 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt @@ -3,9 +3,9 @@ package net.corda.node.services.persistence import net.corda.core.context.InvocationContext import net.corda.core.flows.FlowLogic import net.corda.core.flows.StateMachineRunId -import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.internal.CheckpointSerializationDefaults import net.corda.core.serialization.SerializedBytes -import net.corda.core.serialization.serialize +import net.corda.core.serialization.internal.checkpointSerialize import net.corda.node.internal.CheckpointIncompatibleException import net.corda.node.internal.CheckpointVerifier import net.corda.node.internal.configureDatabase @@ -189,9 +189,9 @@ class DBCheckpointStorageTests { val logic: FlowLogic<*> = object : FlowLogic<Unit>() { override fun call() {} } - val frozenLogic = logic.serialize(context = SerializationDefaults.CHECKPOINT_CONTEXT) + val frozenLogic = logic.checkpointSerialize(context = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT) val checkpoint = Checkpoint.create(InvocationContext.shell(), FlowStart.Explicit, logic.javaClass, frozenLogic, ALICE, SubFlowVersion.CoreFlow(version)).getOrThrow() - return id to checkpoint.serialize(context = SerializationDefaults.CHECKPOINT_CONTEXT) + return id to checkpoint.checkpointSerialize(context = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT) } } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt index 44703546e2..f0c7c95859 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt @@ -8,8 +8,8 @@ import net.corda.core.crypto.TransactionSignature import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction import net.corda.node.internal.configureDatabase -import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.transactions.PersistentUniquenessProvider +import net.corda.node.utilities.TestingNamedCacheFactory import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.* @@ -154,7 +154,7 @@ class DBTransactionStorageTests { } private fun newTransactionStorage(cacheSizeBytesOverride: Long? = null) { - transactionStorage = DBTransactionStorage(cacheSizeBytesOverride ?: NodeConfiguration.defaultTransactionCacheSize, database) + transactionStorage = DBTransactionStorage(database, TestingNamedCacheFactory(cacheSizeBytesOverride ?: 1024)) } private fun assertTransactionIsRetrievable(transaction: SignedTransaction) { diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateColumnConverterTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateColumnConverterTests.kt index 4832474cac..0c5213eb63 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateColumnConverterTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateColumnConverterTests.kt @@ -9,6 +9,7 @@ import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.CashIssueFlow import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.E2ETestKeyManagementService +import net.corda.node.utilities.TestingNamedCacheFactory import net.corda.testing.core.BOC_NAME import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.MockNetwork @@ -47,7 +48,7 @@ class HibernateColumnConverterTests { val ref = OpaqueBytes.of(0x01) // Create parallel set of key and identity services so that the values are not cached, forcing the node caches to do a lookup. - val identityService = PersistentIdentityService() + val identityService = PersistentIdentityService(TestingNamedCacheFactory()) val originalIdentityService: PersistentIdentityService = bankOfCordaNode.services.identityService as PersistentIdentityService identityService.database = originalIdentityService.database identityService.start(originalIdentityService.trustRoot) diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt index baee00a8f3..70c827b496 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt @@ -15,6 +15,7 @@ import net.corda.core.node.services.vault.Sort import net.corda.core.utilities.getOrThrow import net.corda.node.internal.configureDatabase import net.corda.node.services.transactions.PersistentUniquenessProvider +import net.corda.node.utilities.TestingNamedCacheFactory import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.internal.LogHelper @@ -51,7 +52,7 @@ class NodeAttachmentServiceTest { val dataSourceProperties = makeTestDataSourceProperties() database = configureDatabase(dataSourceProperties, DatabaseConfig(), { null }, { null }) fs = Jimfs.newFileSystem(Configuration.unix()) - storage = NodeAttachmentService(MetricRegistry(), database).also { + storage = NodeAttachmentService(MetricRegistry(), TestingNamedCacheFactory(), database).also { database.transaction { it.start() } diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/ExceptionsSerializationTest.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/ExceptionsSerializationTest.kt new file mode 100644 index 0000000000..e072716f0c --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/ExceptionsSerializationTest.kt @@ -0,0 +1,45 @@ +package net.corda.node.services.statemachine + +import net.corda.core.CordaException +import net.corda.core.node.services.UnknownAnonymousPartyException +import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize +import net.corda.node.internal.AbstractNode +import net.corda.node.utilities.registration.CertificateRequestException +import net.corda.testing.core.SerializationEnvironmentRule +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import kotlin.test.assertEquals + +@RunWith(Parameterized::class) +class ExceptionsSerializationTest(private val initialException: CordaException, @Suppress("UNUSED_PARAMETER") description: String) { + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{1}") + fun data(): Collection<Array<Any>> = listOf( + arrayOf<Any>(SessionRejectException("test"), "SessionRejectException"), + arrayOf<Any>(CertificateRequestException("test"), "CertificateRequestException"), + arrayOf<Any>(UnknownAnonymousPartyException("test"), "UnknownAnonymousPartyException"), + arrayOf<Any>(AbstractNode.DatabaseConfigurationException("test"), "DatabaseConfigurationException") + ) + } + + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() + + @Test + fun testMarshal() { + val fromSerialized = performRoundTripSerialization(initialException) + assertEquals(initialException.message, fromSerialized.message) + } + + private inline fun <reified T : Any> performRoundTripSerialization(obj: T): T { + val serializedForm: SerializedBytes<T> = obj.serialize() + return serializedForm.deserialize() + } +} \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt index 17fe106c99..4bf28c246c 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt @@ -10,6 +10,7 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.internal.notary.NotaryInternalException import net.corda.node.internal.configureDatabase import net.corda.node.services.schema.NodeSchemaService +import net.corda.node.utilities.TestingNamedCacheFactory import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.SerializationEnvironmentRule @@ -28,7 +29,7 @@ import kotlin.test.assertFailsWith class PersistentUniquenessProviderTests { @Rule @JvmField - val testSerialization = SerializationEnvironmentRule() + val testSerialization = SerializationEnvironmentRule(inheritable = true) private val identity = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party private val txID = SecureHash.randomSHA256() private val requestSignature = NotarisationRequestSignature(DigitalSignature.WithKey(NullKeys.NullPublicKey, ByteArray(32)), 0) @@ -49,18 +50,15 @@ class PersistentUniquenessProviderTests { @Test fun `should commit a transaction with unused inputs without exception`() { - database.transaction { - val provider = PersistentUniquenessProvider(Clock.systemUTC()) + val provider = PersistentUniquenessProvider(Clock.systemUTC(), database, TestingNamedCacheFactory()) val inputState = generateStateRef() provider.commit(listOf(inputState), txID, identity, requestSignature) - } } @Test fun `should report a conflict for a transaction with previously used inputs`() { - database.transaction { - val provider = PersistentUniquenessProvider(Clock.systemUTC()) + val provider = PersistentUniquenessProvider(Clock.systemUTC(), database, TestingNamedCacheFactory()) val inputState = generateStateRef() val inputs = listOf(inputState) @@ -76,5 +74,4 @@ class PersistentUniquenessProviderTests { val conflictCause = error.consumedStates[inputState]!! assertEquals(conflictCause.hashOfTransactionId, firstTxId.sha256()) } - } } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index da7081315a..41a244990a 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -34,7 +34,7 @@ import net.corda.testing.contracts.DummyState import net.corda.testing.core.* import net.corda.testing.internal.LogHelper import net.corda.testing.internal.rigorousMock -import net.corda.testing.internal.vault.VaultFiller +import net.corda.testing.internal.vault.* import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestIdentityService import org.assertj.core.api.Assertions.assertThat @@ -48,13 +48,15 @@ import java.math.BigDecimal import java.util.* import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors +import javax.persistence.PersistenceException import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue class NodeVaultServiceTest { private companion object { - val cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName, "net.corda.testing.contracts") + val cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName, "net.corda.testing.contracts", + "net.corda.testing.internal.vault") val dummyCashIssuer = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10) val DUMMY_CASH_ISSUER = dummyCashIssuer.ref(1) val bankOfCorda = TestIdentity(BOC_NAME) @@ -757,16 +759,77 @@ class NodeVaultServiceTest { // Test two. // RelevancyStatus set to NOT_RELEVANT. - val criteriaTwo = VaultQueryCriteria(isRelevant = Vault.RelevancyStatus.NOT_RELEVANT) + val criteriaTwo = VaultQueryCriteria(relevancyStatus = Vault.RelevancyStatus.NOT_RELEVANT) val resultTwo = vaultService.queryBy<DummyState>(criteriaTwo).states.getNumbers() assertEquals(setOf(4, 5), resultTwo) // Test three. // RelevancyStatus set to ALL. - val criteriaThree = VaultQueryCriteria(isRelevant = Vault.RelevancyStatus.RELEVANT) + val criteriaThree = VaultQueryCriteria(relevancyStatus = Vault.RelevancyStatus.RELEVANT) val resultThree = vaultService.queryBy<DummyState>(criteriaThree).states.getNumbers() assertEquals(setOf(1, 3, 6), resultThree) // We should never see 2 or 7. } + + @Test + fun `Unique column constraint failing causes linear state to not persist to vault`() { + fun createTx(): SignedTransaction { + return services.signInitialTransaction(TransactionBuilder(DUMMY_NOTARY).apply { + addOutputState(UniqueDummyLinearContract.State(listOf(megaCorp.party), "Dummy linear id"), UNIQUE_DUMMY_LINEAR_CONTRACT_PROGRAM_ID) + addCommand(DummyCommandData, listOf(megaCorp.publicKey)) + }) + } + + services.recordTransactions(StatesToRecord.ONLY_RELEVANT, listOf(createTx())) + assertThatExceptionOfType(PersistenceException::class.java).isThrownBy { + services.recordTransactions(StatesToRecord.ONLY_RELEVANT, listOf(createTx())) + } + assertEquals(1, database.transaction { + vaultService.queryBy<UniqueDummyLinearContract.State>().states.size + }) + } + + @Test + fun `Unique column constraint failing causes fungible state to not persist to vault`() { + fun createTx(): SignedTransaction { + return services.signInitialTransaction(TransactionBuilder(DUMMY_NOTARY).apply { + addOutputState(UniqueDummyFungibleContract.State(10.DOLLARS `issued by` DUMMY_CASH_ISSUER, megaCorp.party), UNIQUE_DUMMY_FUNGIBLE_CONTRACT_PROGRAM_ID) + addCommand(DummyCommandData, listOf(megaCorp.publicKey)) + }) + } + + services.recordTransactions(StatesToRecord.ONLY_RELEVANT, listOf(createTx())) + assertThatExceptionOfType(PersistenceException::class.java).isThrownBy { + services.recordTransactions(StatesToRecord.ONLY_RELEVANT, listOf(createTx())) + } + assertEquals(1, database.transaction { + vaultService.queryBy<UniqueDummyFungibleContract.State>().states.size + }) + assertEquals(10.DOLLARS.quantity, database.transaction { + vaultService.queryBy<UniqueDummyFungibleContract.State>().states.first().state.data.amount.quantity + }) + } + + @Test + fun `Unique column constraint failing causes all states in transaction to fail`() { + fun createTx(): SignedTransaction { + return services.signInitialTransaction(TransactionBuilder(DUMMY_NOTARY).apply { + addOutputState(UniqueDummyLinearContract.State(listOf(megaCorp.party), "Dummy linear id"), UNIQUE_DUMMY_LINEAR_CONTRACT_PROGRAM_ID) + addOutputState(DummyDealContract.State(listOf(megaCorp.party), "Dummy linear id"), DUMMY_DEAL_PROGRAM_ID) + addCommand(DummyCommandData, listOf(megaCorp.publicKey)) + }) + } + + services.recordTransactions(StatesToRecord.ONLY_RELEVANT, listOf(createTx())) + assertThatExceptionOfType(PersistenceException::class.java).isThrownBy { + services.recordTransactions(StatesToRecord.ONLY_RELEVANT, listOf(createTx())) + } + assertEquals(1, database.transaction { + vaultService.queryBy<UniqueDummyLinearContract.State>().states.size + }) + assertEquals(1, database.transaction { + vaultService.queryBy<DummyDealContract.State>().states.size + }) + } } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultFlowTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultFlowTest.kt new file mode 100644 index 0000000000..ea1a78704c --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultFlowTest.kt @@ -0,0 +1,88 @@ +package net.corda.node.services.vault + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatingFlow +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party +import net.corda.core.node.services.queryBy +import net.corda.core.transactions.TransactionBuilder +import net.corda.testing.core.DummyCommandData +import net.corda.testing.core.singleIdentity +import net.corda.testing.internal.vault.DUMMY_DEAL_PROGRAM_ID +import net.corda.testing.internal.vault.DummyDealContract +import net.corda.testing.internal.vault.UNIQUE_DUMMY_LINEAR_CONTRACT_PROGRAM_ID +import net.corda.testing.internal.vault.UniqueDummyLinearContract +import net.corda.testing.node.MockNetwork +import net.corda.testing.node.MockNetworkNotarySpec +import net.corda.testing.node.MockNodeParameters +import net.corda.testing.node.StartedMockNode +import org.assertj.core.api.Assertions +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.concurrent.ExecutionException +import kotlin.test.assertEquals + +class VaultFlowTest { + + private lateinit var mockNetwork: MockNetwork + private lateinit var partyA: StartedMockNode + private lateinit var partyB: StartedMockNode + private lateinit var notaryNode: MockNetworkNotarySpec + + @Before + fun setup() { + notaryNode = MockNetworkNotarySpec(CordaX500Name("Notary", "London", "GB")) + mockNetwork = MockNetwork( + listOf( + "net.corda.node.services.vault", "net.corda.testing.internal.vault" + ), + notarySpecs = listOf(notaryNode), + threadPerNode = true, + networkSendManuallyPumped = false + ) + partyA = mockNetwork.createNode(MockNodeParameters(legalName = CordaX500Name("PartyA", "Berlin", "DE"))) + partyB = mockNetwork.createNode(MockNodeParameters(legalName = CordaX500Name("PartyB", "Berlin", "DE"))) + mockNetwork.startNodes() + } + + @After + fun tearDown() { + mockNetwork.stopNodes() + } + + @Test + fun `Unique column constraint failing causes states to not persist to vaults`() { + partyA.startFlow(Initiator(listOf(partyA.info.singleIdentity(), partyB.info.singleIdentity()))).get() + Assertions.assertThatExceptionOfType(ExecutionException::class.java).isThrownBy { + partyA.startFlow(Initiator(listOf(partyA.info.singleIdentity(), partyB.info.singleIdentity()))).get() + } + assertEquals(1, partyA.transaction { + partyA.services.vaultService.queryBy<UniqueDummyLinearContract.State>().states.size + }) + assertEquals(1, partyB.transaction { + partyB.services.vaultService.queryBy<UniqueDummyLinearContract.State>().states.size + }) + assertEquals(1, partyA.transaction { + partyA.services.vaultService.queryBy<DummyDealContract.State>().states.size + }) + assertEquals(1, partyB.transaction { + partyB.services.vaultService.queryBy<DummyDealContract.State>().states.size + }) + } +} + +@InitiatingFlow +class Initiator(private val participants: List<Party>) : FlowLogic<Unit>() { + @Suspendable + override fun call() { + val stx = serviceHub.signInitialTransaction(TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first()).apply { + addOutputState(UniqueDummyLinearContract.State(participants, "Dummy linear id"), UNIQUE_DUMMY_LINEAR_CONTRACT_PROGRAM_ID) + addOutputState(DummyDealContract.State(participants, "linear id"), DUMMY_DEAL_PROGRAM_ID) + addCommand(DummyCommandData, listOf(ourIdentity.owningKey)) + }) + subFlow(FinalityFlow(stx)) + } +} diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 62e1b43023..0d9cbe9f68 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -800,6 +800,139 @@ abstract class VaultQueryTestsBase : VaultQueryParties { } } + @Test + fun `logical operator case insensitive EQUAL`() { + database.transaction { + listOf(USD, GBP, CHF).forEach { + vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER) + } + val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.equal("gBp", false) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultService.queryBy<Cash.State>(criteria) + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `logical operator case insensitive EQUAL does not affect numbers`() { + database.transaction { + listOf(USD, GBP, CHF).forEach { + vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER) + } + val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.equal(10000, false) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultService.queryBy<Cash.State>(criteria) + assertThat(results.states).hasSize(3) + } + } + + @Test + fun `logical operator case insensitive NOT_EQUAL does not return results containing the same characters as the case insensitive string`() { + database.transaction { + listOf(USD, GBP, CHF).forEach { + vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER) + } + val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.notEqual("gBp", false) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultService.queryBy<Cash.State>(criteria) + assertThat(results.states).hasSize(2) + } + } + + @Test + fun `logical operator case insensitive NOT_EQUAL does not affect numbers`() { + database.transaction { + listOf(USD, CHF).forEach { + vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER) + } + vaultFiller.fillWithSomeTestCash(AMOUNT(50, GBP), notaryServices, 1, DUMMY_CASH_ISSUER) + val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.notEqual(10000, false) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultService.queryBy<Cash.State>(criteria) + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `logical operator case insensitive IN`() { + database.transaction { + listOf(USD, GBP, CHF).forEach { + vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER) + } + val currencies = listOf("cHf", "gBp") + val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.`in`(currencies, false) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultService.queryBy<Cash.State>(criteria) + assertThat(results.states).hasSize(2) + } + } + + @Test + fun `logical operator case insensitive IN does not affect numbers`() { + database.transaction { + vaultFiller.fillWithSomeTestCash(AMOUNT(100, USD), notaryServices, 1, DUMMY_CASH_ISSUER) + vaultFiller.fillWithSomeTestCash(AMOUNT(200, CHF), notaryServices, 1, DUMMY_CASH_ISSUER) + vaultFiller.fillWithSomeTestCash(AMOUNT(50, GBP), notaryServices, 1, DUMMY_CASH_ISSUER) + val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.`in`(listOf(10000L, 20000L), false) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultService.queryBy<Cash.State>(criteria) + assertThat(results.states).hasSize(2) + } + } + + @Test + fun `logical operator case insensitive NOT IN does not return results containing the same characters as the case insensitive strings`() { + database.transaction { + listOf(USD, GBP, CHF).forEach { + vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER) + } + val currencies = listOf("cHf", "gBp") + val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.notIn(currencies, false) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultService.queryBy<Cash.State>(criteria) + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `logical operator case insensitive NOT_IN does not affect numbers`() { + database.transaction { + vaultFiller.fillWithSomeTestCash(AMOUNT(100, USD), notaryServices, 1, DUMMY_CASH_ISSUER) + vaultFiller.fillWithSomeTestCash(AMOUNT(200, CHF), notaryServices, 1, DUMMY_CASH_ISSUER) + vaultFiller.fillWithSomeTestCash(AMOUNT(50, GBP), notaryServices, 1, DUMMY_CASH_ISSUER) + val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.notIn(listOf(10000L, 20000L), false) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultService.queryBy<Cash.State>(criteria) + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `logical operator case insensitive LIKE`() { + database.transaction { + listOf(USD, GBP, CHF).forEach { + vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER) + } + val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.like("%bP", false) } // GPB + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultService.queryBy<Cash.State>(criteria) + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `logical operator NOT LIKE does not return results containing the same characters as the case insensitive string`() { + database.transaction { + listOf(USD, GBP, CHF).forEach { + vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER) + } + val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.notLike("%bP", false) } // GPB + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultService.queryBy<Cash.State>(criteria) + assertThat(results.states).hasSize(2) + } + } + @Test fun `aggregate functions without group clause`() { database.transaction { diff --git a/node/src/test/kotlin/net/corda/node/utilities/TLSAuthenticationTests.kt b/node/src/test/kotlin/net/corda/node/utilities/TLSAuthenticationTests.kt index ef933d5ccc..463b6511cd 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/TLSAuthenticationTests.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/TLSAuthenticationTests.kt @@ -64,8 +64,7 @@ class TLSAuthenticationTests { // Default supported TLS schemes for Corda nodes. private val CORDA_TLS_CIPHER_SUITES = arrayOf( "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256" + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" ) @Test @@ -100,22 +99,6 @@ class TLSAuthenticationTests { testConnect(serverSocket, clientSocket, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256") } - @Test - fun `All EC K1`() { - val (serverSocketFactory, clientSocketFactory) = buildTLSFactories( - rootCAScheme = Crypto.ECDSA_SECP256K1_SHA256, - intermediateCAScheme = Crypto.ECDSA_SECP256K1_SHA256, - client1CAScheme = Crypto.ECDSA_SECP256K1_SHA256, - client1TLSScheme = Crypto.ECDSA_SECP256K1_SHA256, - client2CAScheme = Crypto.ECDSA_SECP256K1_SHA256, - client2TLSScheme = Crypto.ECDSA_SECP256K1_SHA256 - ) - - val (serverSocket, clientSocket) = buildTLSSockets(serverSocketFactory, clientSocketFactory, 0, 0) - - testConnect(serverSocket, clientSocket, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256") - } - // Server's public key type is the one selected if users use different key types (e.g RSA and EC R1). @Test fun `Server RSA - Client EC R1 - CAs all EC R1`() { @@ -162,22 +145,6 @@ class TLSAuthenticationTests { testConnect(serverSocket, clientSocket, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256") } - @Test - fun `Server EC K1 - Client EC R1 - CAs all RSA`() { - val (serverSocketFactory, clientSocketFactory) = buildTLSFactories( - rootCAScheme = Crypto.RSA_SHA256, - intermediateCAScheme = Crypto.RSA_SHA256, - client1CAScheme = Crypto.RSA_SHA256, - client1TLSScheme = Crypto.ECDSA_SECP256K1_SHA256, - client2CAScheme = Crypto.RSA_SHA256, - client2TLSScheme = Crypto.ECDSA_SECP256R1_SHA256 - ) - - val (serverSocket, clientSocket) = buildTLSSockets(serverSocketFactory, clientSocketFactory, 0, 0) - testConnect(serverSocket, clientSocket, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256") - } - - @Test fun `Server EC R1 - Client RSA - Mixed CAs`() { val (serverSocketFactory, clientSocketFactory) = buildTLSFactories( @@ -185,7 +152,7 @@ class TLSAuthenticationTests { intermediateCAScheme = Crypto.RSA_SHA256, client1CAScheme = Crypto.RSA_SHA256, client1TLSScheme = Crypto.ECDSA_SECP256R1_SHA256, - client2CAScheme = Crypto.ECDSA_SECP256K1_SHA256, + client2CAScheme = Crypto.ECDSA_SECP256R1_SHA256, client2TLSScheme = Crypto.RSA_SHA256 ) @@ -193,27 +160,6 @@ class TLSAuthenticationTests { testConnect(serverSocket, clientSocket, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256") } - @Test - fun `All RSA - avoid ECC for DH`() { - val (serverSocketFactory, clientSocketFactory) = buildTLSFactories( - rootCAScheme = Crypto.RSA_SHA256, - intermediateCAScheme = Crypto.RSA_SHA256, - client1CAScheme = Crypto.RSA_SHA256, - client1TLSScheme = Crypto.RSA_SHA256, - client2CAScheme = Crypto.RSA_SHA256, - client2TLSScheme = Crypto.RSA_SHA256 - ) - - val (serverSocket, clientSocket) = buildTLSSockets( - serverSocketFactory, - clientSocketFactory, - 0, - 0, - CORDA_TLS_CIPHER_SUITES, - arrayOf("TLS_DHE_RSA_WITH_AES_128_GCM_SHA256")) // Second client accepts DHE only. - testConnect(serverSocket, clientSocket, "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256") - } - // According to RFC 5246 (TLS 1.2), section 7.4.1.2 ClientHello cipher_suites: // This is a list of the cryptographic options supported by the client, with the client's first preference first. // diff --git a/node/src/test/kotlin/net/corda/node/utilities/TestingNamedCacheFactory.kt b/node/src/test/kotlin/net/corda/node/utilities/TestingNamedCacheFactory.kt new file mode 100644 index 0000000000..4582246e09 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/utilities/TestingNamedCacheFactory.kt @@ -0,0 +1,33 @@ +package net.corda.node.utilities + +import com.codahale.metrics.MetricRegistry +import com.github.benmanes.caffeine.cache.Cache +import com.github.benmanes.caffeine.cache.CacheLoader +import com.github.benmanes.caffeine.cache.Caffeine +import com.github.benmanes.caffeine.cache.LoadingCache +import net.corda.core.internal.buildNamed +import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.node.services.config.MB +import net.corda.node.services.config.NodeConfiguration + +class TestingNamedCacheFactory private constructor(private val sizeOverride: Long, private val metricRegistry: MetricRegistry?, private val nodeConfiguration: NodeConfiguration?) : NamedCacheFactory, SingletonSerializeAsToken() { + constructor(sizeOverride: Long = 1024) : this(sizeOverride, null, null) + + override fun bindWithMetrics(metricRegistry: MetricRegistry): NamedCacheFactory = TestingNamedCacheFactory(sizeOverride, metricRegistry, this.nodeConfiguration) + override fun bindWithConfig(nodeConfiguration: NodeConfiguration): NamedCacheFactory = TestingNamedCacheFactory(sizeOverride, this.metricRegistry, nodeConfiguration) + + override fun <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String): Cache<K, V> { + // Does not check metricRegistry or nodeConfiguration, because for tests we don't care. + return caffeine.maximumSize(sizeOverride).buildNamed<K, V>(name) + } + + override fun <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String, loader: CacheLoader<K, V>): LoadingCache<K, V> { + // Does not check metricRegistry or nodeConfiguration, because for tests we don't care. + val configuredCaffeine = when (name) { + "DBTransactionStorage_transactions" -> caffeine.maximumWeight(1.MB) + "NodeAttachmentService_attachmentContent" -> caffeine.maximumWeight(1.MB) + else -> caffeine.maximumSize(sizeOverride) + } + return configuredCaffeine.buildNamed<K, V>(name, loader) + } +} \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt index eaae962161..5f60bb6618 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt @@ -16,14 +16,17 @@ import net.corda.core.internal.toX500Name import net.corda.core.utilities.seconds import net.corda.node.NodeRegistrationOption import net.corda.node.services.config.NodeConfiguration -import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA +import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_ALIAS_PREFIX +import net.corda.nodeapi.internal.crypto.X509Utilities.createSelfSignedCACertificate import net.corda.testing.core.ALICE_NAME import net.corda.testing.internal.createDevIntermediateCaCertPath import net.corda.testing.internal.rigorousMock +import net.corda.testing.internal.stubs.CertificateStoreStubs import org.assertj.core.api.Assertions.* import org.bouncycastle.asn1.x509.GeneralName import org.bouncycastle.asn1.x509.GeneralSubtree @@ -37,7 +40,9 @@ import java.security.PublicKey import java.security.cert.CertPathValidatorException import java.security.cert.X509Certificate import javax.security.auth.x500.X500Principal +import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertTrue class NetworkRegistrationHelperTest { private val fs = Jimfs.newFileSystem(unix()) @@ -52,10 +57,13 @@ class NetworkRegistrationHelperTest { val baseDirectory = fs.getPath("/baseDir").createDirectories() abstract class AbstractNodeConfiguration : NodeConfiguration + + val certificatesDirectory = baseDirectory / "certificates" config = rigorousMock<AbstractNodeConfiguration>().also { doReturn(baseDirectory).whenever(it).baseDirectory - doReturn("trustpass").whenever(it).trustStorePassword - doReturn("cordacadevpass").whenever(it).keyStorePassword + doReturn(certificatesDirectory).whenever(it).certificatesDirectory + doReturn(CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory)).whenever(it).p2pSslOptions + doReturn(CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory)).whenever(it).signingCertificateStore doReturn(nodeLegalName).whenever(it).myLegalName doReturn("").whenever(it).emailAddress doReturn(null).whenever(it).tlsCertCrlDistPoint @@ -71,30 +79,30 @@ class NetworkRegistrationHelperTest { @Test fun `successful registration`() { - assertThat(config.nodeKeystore).doesNotExist() - assertThat(config.sslKeystore).doesNotExist() - assertThat(config.trustStoreFile).doesNotExist() + assertThat(config.signingCertificateStore.getOptional()).isNull() + assertThat(config.p2pSslOptions.keyStore.getOptional()).isNull() + assertThat(config.p2pSslOptions.trustStore.getOptional()).isNull() - val rootAndIntermediateCA = createDevIntermediateCaCertPath().also { saveNetworkTrustStore(it.first.certificate) } + val rootAndIntermediateCA = createDevIntermediateCaCertPath().also { saveNetworkTrustStore(CORDA_ROOT_CA to it.first.certificate) } createRegistrationHelper(rootAndIntermediateCA = rootAndIntermediateCA).buildKeystore() - val nodeKeystore = config.loadNodeKeyStore() - val sslKeystore = config.loadSslKeyStore() - val trustStore = config.loadTrustStore() + val nodeKeystore = config.signingCertificateStore.get() + val sslKeystore = config.p2pSslOptions.keyStore.get() + val trustStore = config.p2pSslOptions.trustStore.get() nodeKeystore.run { assertFalse(contains(X509Utilities.CORDA_INTERMEDIATE_CA)) assertFalse(contains(X509Utilities.CORDA_ROOT_CA)) assertFalse(contains(X509Utilities.CORDA_CLIENT_TLS)) - assertThat(CertRole.extract(getCertificate(X509Utilities.CORDA_CLIENT_CA))).isEqualTo(CertRole.NODE_CA) + assertThat(CertRole.extract(this[X509Utilities.CORDA_CLIENT_CA])).isEqualTo(CertRole.NODE_CA) } sslKeystore.run { assertFalse(contains(X509Utilities.CORDA_CLIENT_CA)) assertFalse(contains(X509Utilities.CORDA_INTERMEDIATE_CA)) assertFalse(contains(X509Utilities.CORDA_ROOT_CA)) - val nodeTlsCertChain = getCertificateChain(X509Utilities.CORDA_CLIENT_TLS) + val nodeTlsCertChain = query { getCertificateChain(X509Utilities.CORDA_CLIENT_TLS) } assertThat(nodeTlsCertChain).hasSize(4) // The TLS cert has the same subject as the node CA cert assertThat(CordaX500Name.build(nodeTlsCertChain[0].subjectX500Principal)).isEqualTo(nodeLegalName) @@ -104,7 +112,7 @@ class NetworkRegistrationHelperTest { trustStore.run { assertFalse(contains(X509Utilities.CORDA_CLIENT_CA)) assertFalse(contains(X509Utilities.CORDA_INTERMEDIATE_CA)) - assertThat(getCertificate(X509Utilities.CORDA_ROOT_CA)).isEqualTo(rootAndIntermediateCA.first.certificate) + assertThat(this[X509Utilities.CORDA_ROOT_CA]).isEqualTo(rootAndIntermediateCA.first.certificate) } } @@ -119,7 +127,7 @@ class NetworkRegistrationHelperTest { @Test fun `node CA with incorrect cert role`() { val nodeCaCertPath = createNodeCaCertPath(type = CertificateType.TLS) - saveNetworkTrustStore(nodeCaCertPath.last()) + saveNetworkTrustStore(CORDA_ROOT_CA to nodeCaCertPath.last()) val registrationHelper = createFixedResponseRegistrationHelper(nodeCaCertPath) assertThatExceptionOfType(CertificateRequestException::class.java) .isThrownBy { registrationHelper.buildKeystore() } @@ -130,19 +138,39 @@ class NetworkRegistrationHelperTest { fun `node CA with incorrect subject`() { val invalidName = CordaX500Name("Foo", "MU", "GB") val nodeCaCertPath = createNodeCaCertPath(legalName = invalidName) - saveNetworkTrustStore(nodeCaCertPath.last()) + saveNetworkTrustStore(CORDA_ROOT_CA to nodeCaCertPath.last()) val registrationHelper = createFixedResponseRegistrationHelper(nodeCaCertPath) assertThatExceptionOfType(CertificateRequestException::class.java) .isThrownBy { registrationHelper.buildKeystore() } .withMessageContaining(invalidName.toString()) } + @Test + fun `multiple certificates are copied to the node's trust store`() { + val extraTrustedCertAlias = "trusted_test" + val extraTrustedCert = createSelfSignedCACertificate( + X500Principal("O=Test Trusted CA,L=MU,C=GB"), + Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) + val rootAndIntermediateCA = createDevIntermediateCaCertPath().also { + saveNetworkTrustStore(CORDA_ROOT_CA to it.first.certificate, extraTrustedCertAlias to extraTrustedCert) + } + + val registrationHelper = createRegistrationHelper(rootAndIntermediateCA = rootAndIntermediateCA) + registrationHelper.buildKeystore() + val trustStore = config.p2pSslOptions.trustStore.get() + trustStore.run { + assertTrue(contains(extraTrustedCertAlias)) + assertTrue(contains(CORDA_ROOT_CA)) + assertEquals(extraTrustedCert, get(extraTrustedCertAlias)) + } + } + @Test fun `wrong root cert in truststore`() { - val wrongRootCert = X509Utilities.createSelfSignedCACertificate( + val wrongRootCert = createSelfSignedCACertificate( X500Principal("O=Foo,L=MU,C=GB"), Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) - saveNetworkTrustStore(wrongRootCert) + saveNetworkTrustStore(CORDA_ROOT_CA to wrongRootCert) val registrationHelper = createRegistrationHelper() assertThatThrownBy { @@ -152,27 +180,27 @@ class NetworkRegistrationHelperTest { @Test fun `create service identity cert`() { - assertThat(config.nodeKeystore).doesNotExist() - assertThat(config.sslKeystore).doesNotExist() - assertThat(config.trustStoreFile).doesNotExist() + assertThat(config.signingCertificateStore.getOptional()).isNull() + assertThat(config.p2pSslOptions.keyStore.getOptional()).isNull() + assertThat(config.p2pSslOptions.trustStore.getOptional()).isNull() - val rootAndIntermediateCA = createDevIntermediateCaCertPath().also { saveNetworkTrustStore(it.first.certificate) } + val rootAndIntermediateCA = createDevIntermediateCaCertPath().also { saveNetworkTrustStore(CORDA_ROOT_CA to it.first.certificate) } createRegistrationHelper(CertRole.SERVICE_IDENTITY, rootAndIntermediateCA).buildKeystore() - val nodeKeystore = config.loadNodeKeyStore() + val nodeKeystore = config.signingCertificateStore.get() - assertThat(config.sslKeystore).doesNotExist() - assertThat(config.trustStoreFile).doesNotExist() + assertThat(config.p2pSslOptions.keyStore.getOptional()).isNull() + assertThat(config.p2pSslOptions.trustStore.getOptional()).isNull() - val serviceIdentityAlias = "${DevIdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX}-private-key" + val serviceIdentityAlias = "$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key" nodeKeystore.run { assertFalse(contains(X509Utilities.CORDA_INTERMEDIATE_CA)) assertFalse(contains(X509Utilities.CORDA_ROOT_CA)) assertFalse(contains(X509Utilities.CORDA_CLIENT_TLS)) assertFalse(contains(X509Utilities.CORDA_CLIENT_CA)) - assertThat(CertRole.extract(getCertificate(serviceIdentityAlias))).isEqualTo(CertRole.SERVICE_IDENTITY) + assertThat(CertRole.extract(this[serviceIdentityAlias])).isEqualTo(CertRole.SERVICE_IDENTITY) } } @@ -223,23 +251,32 @@ class NetworkRegistrationHelperTest { return when (certRole) { CertRole.NODE_CA -> NodeRegistrationHelper(config, certService, NodeRegistrationOption(config.certificatesDirectory / networkRootTrustStoreFileName, networkRootTrustStorePassword)) CertRole.SERVICE_IDENTITY -> NetworkRegistrationHelper( - config, + config.certificatesDirectory, + config.signingCertificateStore, config.myLegalName, config.emailAddress, certService, config.certificatesDirectory / networkRootTrustStoreFileName, networkRootTrustStorePassword, - "${DevIdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX}-private-key", + "$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key", CertRole.SERVICE_IDENTITY) else -> throw IllegalArgumentException("Unsupported cert role.") } } - private fun saveNetworkTrustStore(rootCert: X509Certificate) { + /** + * Saves given certificates into the truststore. + * + * @param trustedCertificates pairs containing the alias under which the given certificate needs to be stored and + * the certificate itself. + */ + private fun saveNetworkTrustStore(vararg trustedCertificates: Pair<String, X509Certificate>) { config.certificatesDirectory.createDirectories() val rootTruststorePath = config.certificatesDirectory / networkRootTrustStoreFileName X509KeyStore.fromFile(rootTruststorePath, networkRootTrustStorePassword, createNew = true).update { - setCertificate(X509Utilities.CORDA_ROOT_CA, rootCert) + trustedCertificates.forEach { + setCertificate(it.first, it.second) + } } } } diff --git a/node/src/test/resources/net/corda/node/internal/cordapp/versions/min-2-no-target.jar b/node/src/test/resources/net/corda/node/internal/cordapp/versions/min-2-no-target.jar new file mode 100644 index 0000000000..4496915841 Binary files /dev/null and b/node/src/test/resources/net/corda/node/internal/cordapp/versions/min-2-no-target.jar differ diff --git a/node/src/test/resources/net/corda/node/internal/cordapp/versions/min-2-target-3.jar b/node/src/test/resources/net/corda/node/internal/cordapp/versions/min-2-target-3.jar new file mode 100644 index 0000000000..1b66fba425 Binary files /dev/null and b/node/src/test/resources/net/corda/node/internal/cordapp/versions/min-2-target-3.jar differ diff --git a/node/src/test/resources/net/corda/node/internal/cordapp/versions/no-min-or-target-version.jar b/node/src/test/resources/net/corda/node/internal/cordapp/versions/no-min-or-target-version.jar new file mode 100644 index 0000000000..408e70145d Binary files /dev/null and b/node/src/test/resources/net/corda/node/internal/cordapp/versions/no-min-or-target-version.jar differ diff --git a/release-tools/jiraReleaseChecker.py b/release-tools/jiraReleaseChecker.py new file mode 100755 index 0000000000..47e1e82fb3 --- /dev/null +++ b/release-tools/jiraReleaseChecker.py @@ -0,0 +1,246 @@ +#!/usr/bin/python + +#------------------------------------------------------------------------------- +# +# Usage +# ======= +# +# ./jiraReleaseChecker.py <oldTag> <jiraTag> <jiraUser> [-m mode] +# ./jiraReleaseChecker.py release-V3.1 "Corda 3.3" some.user@r3.com [-m not-in-jira] +# +# <oldTag> is the point prior to the current branches head in history from +# which to inspect commits. Normally this will be the tag of the previous +# release. e.g. +# +# master ---------------------------------------------- +# \ +# release/4 -----------+--------------+------------ +# / / +# release/4.0 release/4.1 +# +# The current release in the above example will be 4.2 and those commits +# extend from 4.1 having been backported from master. Thus <oldTag> is +# release/4.1 +# +# <jiraTag> should refer to the version string used within +# the R3 Corda Jira to track the release. For example, for 3.3 this would be +# "Corda 3.3" +# +# <jiraUser> should be a registered user able to authenticate with the +# R3 Jira system. Authentication and password management is handled through +# the native OS keyring implementation. +# +# The script should be run on the relevant release branch within the git +# repository. +# +# Modes +# ------- +# +# The tool can operate in 3 modes +# +# * rst - The default when omitted. Will take the combined lists +# of issues fixed from both Jira and commit summaries and +# format that list in such a way it can be included within +# the release notes for the next release. Will include hyper +# links to the R3 Jira for each ticket. +# * not-in-jira - Print a list of tickets that are included in commit +# summaries but are not tagged in Jira as fixed in the release +# * not-in-commit - Print a list of tickets that are tagged in Jira but that +# are not mentioned in any commit summary, +# +# Pre Requisites +# ================ +# +# pip +# pyjira +# gitpython +# keyring (optional) +# +# Installation +# -------------- +# Should be a simple matter of ``pip install <package>`` +# +# Issues +# ======== +# +# Doesn't really handle many errors all that well, also gives no mechanism +# to enter a correct password into the keyring if a wrong one is added which +# isn't great but for now this should do +# +#------------------------------------------------------------------------------- + +import re +import sys +import getpass +import argparse + +try : + import keyring +except ImportError : + disableKeyring = True +else : + disableKeyring = False + +from jira import JIRA +from git import Repo + +#------------------------------------------------------------------------------- + +R3_JIRA_ADDR = "https://r3-cev.atlassian.net" +JIRA_MAX_RESULTS = 50 + +#------------------------------------------------------------------------------- + +# +# For a given user (provide via the command line) authenticate with Jira and +# return an interface object instance +# +def jiraLogin(user) : + password = keyring.get_password ('jira', user) if not disableKeyring else None + + if not password: + password = getpass.getpass("Please enter your JIRA password, " + + "it will be stored in your OS Keyring: ") + if not disableKeyring : + keyring.set_password ('jira', user, password) + + return JIRA(R3_JIRA_ADDR, auth=(user, password)) + +#------------------------------------------------------------------------------- + +# +# Cope with Jira REST API paginating query results +# +def jiraQuery (jira, query) : + offset = 0 + results = JIRA_MAX_RESULTS + rtn = [] + while (results == JIRA_MAX_RESULTS) : + issues = jira.search_issues(query, maxResults=JIRA_MAX_RESULTS, startAt=offset) + results = len(issues) + if results > 0 : + offset += JIRA_MAX_RESULTS + rtn += issues + + return rtn + +#------------------------------------------------------------------------------- + +# +# Take a Jira issue and format it in such a way we can include it as a line +# item in the release notes formatted with a hyperlink to the issue in Jira +# +def issueToRST(issue) : + return "* %s [`%s <%s/browse/%s>`_]" % ( + issue.fields.summary, + issue.key, + R3_JIRA_ADDR, + issue.key) + +#------------------------------------------------------------------------------- + +# +# Get a list of jiras from Jira where those jiras are marked as fixed +# in some specific version (set on the command line). +# +# Optionally, an already authenticated Jira connection instance can be +# provided to avoid re-authenticating. The authenticated object +# is returned for reuse. +# +def getJirasFromJira(args_, jira_ = None) : + jira = jiraLogin(args_.jiraUser) if jira_ == None else jira_ + + return jiraQuery(jira, \ + 'project in (Corda, Ent) And fixVersion in ("%s") and status in (Done)' % (args_.jiraTag)) \ + , jira + +#------------------------------------------------------------------------------- + +def getJiraIdsFromJira(args_, jira_ = None) : + jira = jiraLogin(args_.jiraUser) if jira_ == None else jira_ + + jirasFromJira, _ = jiraQuery(jira, \ + 'project in (Corda, Ent) And fixVersion in ("%s") and status in (Done)' % (args_.jiraTag)) \ + , jira + + return [ j.key for j in jirasFromJira ], jira + +#------------------------------------------------------------------------------- + +def getJiraIdsFromCommits(args_) : + jiraMatch = re.compile("(CORDA-\d+|ENT-\d+)") + repo = Repo(".", search_parent_directories = True) + + jirasFromCommits = [] + for commit in list (repo.iter_commits ("%s.." % (args_.oldTag))) : + jirasFromCommits += jiraMatch.findall(commit.summary) + + return jirasFromCommits + +#------------------------------------------------------------------------------- + +# +# Take the set of all tickets completed in a release (the union of those +# tagged in Jira and those marked in commit summaries) and format them +# for inclusion in the release notes (rst format). +# +def rst (args_) : + jiraIdsFromCommits = getJiraIdsFromCommits(args_) + jirasFromJira, jiraObj = getJirasFromJira(args_) + + jiraIdsFromJira = [ jira.key for jira in jirasFromJira ] + + # + # Grab the set of JIRA's that aren't tagged as fixed in the release but are + # mentioned in a commit and pull down the JIRA information for those so as + # to get access to their summary + # + extraJiras = set(jiraIdsFromCommits).difference(jiraIdsFromJira) + jirasFromJira += jiraQuery(jiraObj, "key in (%s)" % (", ".join(extraJiras))) + + for jira in jirasFromJira : + print issueToRST(jira) + +#------------------------------------------------------------------------------- + +def notInJira(args_) : + jiraIdsFromCommits = getJiraIdsFromCommits(args_) + jiraIdsFromJira, _ = getJiraIdsFromJira(args_) + + print 'Issues mentioned in commits but not set as "fixed in" in Jira' + + for jiraId in set(jiraIdsFromJira).difference(jiraIdsFromCommits) : + print jiraId + +#------------------------------------------------------------------------------- + +def notInCommit(args_) : + jiraIdsFromCommits = getJiraIdsFromCommits(args_) + jiraIdsFromJira, _ = getJiraIdsFromJira(args_) + + print 'Issues tagged in Jira as fixed but not mentioned in any commit summary' + + for jiraId in set(jiraIdsFromCommits).difference(jiraIdsFromJira) : + print jiraId + +#------------------------------------------------------------------------------- + +if __name__ == "__main__" : + parser = argparse.ArgumentParser() + parser.add_argument("-m", "--mode", help="display a square of a given number", + choices = [ "rst", "not-in-jira", "not-in-commit"]) + parser.add_argument("oldTag", help="The previous release tag") + parser.add_argument("jiraTag", help="The current Jira release") + parser.add_argument("jiraUser", help="Who to authenticate with Jira as") + + args = parser.parse_args() + + if not args.mode : args.mode = "rst" + + if args.mode == "rst" : rst(args) + elif args.mode == "not-in-jira" : notInJira(args) + elif args.mode == "not-in-commit" : notInCommit(args) + +#------------------------------------------------------------------------------- + + diff --git a/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt b/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt index e47bc11f0d..5e2d1aa04b 100644 --- a/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt +++ b/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt @@ -17,7 +17,7 @@ class AttachmentDemoTest { @Test fun `attachment demo using a 10MB zip file`() { val numOfExpectedBytes = 10_000_000 - driver(DriverParameters(portAllocation = PortAllocation.Incremental(20000))) { + driver(DriverParameters(portAllocation = PortAllocation.Incremental(20000), startNodesInProcess = true)) { val demoUser = listOf(User("demo", "demo", setOf(all()))) val (nodeA, nodeB) = listOf( startNode(providedName = DUMMY_BANK_A_NAME, rpcUsers = demoUser, maximumHeapSize = "1g"), diff --git a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt index 0941dc888d..ef87d0dbde 100644 --- a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt +++ b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt @@ -4,8 +4,8 @@ import net.corda.core.internal.div import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_BANK_B_NAME import net.corda.testing.driver.DriverParameters -import net.corda.testing.node.User import net.corda.testing.driver.driver +import net.corda.testing.node.User /** * This file is exclusively for being able to run your nodes through an IDE (as opposed to running deployNodes) diff --git a/samples/notary-demo/README.md b/samples/notary-demo/README.md index 569c56351b..ee474a8d18 100644 --- a/samples/notary-demo/README.md +++ b/samples/notary-demo/README.md @@ -2,17 +2,13 @@ Notary demo ----------- This demo shows a party getting transactions notarised by either a single-node or a distributed notary service. -All versions of the demo start two counterparty nodes. - -One of the counterparties will generate transactions that transfer a self-issued asset to the other party and submit -them for notarisation. The Raft (https://raft.github.io/) version of the demo will start three distributed notary nodes. The BFT SMaRt (https://bft-smart.github.io/library/) version of the demo will start four distributed notary nodes. The output will display a list of notarised transaction IDs and corresponding signer public keys. In the Raft distributed notary, every node in the cluster can service client requests, and one signature is sufficient to satisfy the notary composite key requirement. -In the BFT SMaRt distributed notary, three signatures are required. +In the BFT-SMaRt distributed notary, three signatures are required. You will notice that successive transactions get signed by different members of the cluster (usually allocated in a random order). To run the Raft version of the demo from the command line in Unix: @@ -22,7 +18,7 @@ To run the Raft version of the demo from the command line in Unix: Single notaries). 2. Run ``./samples/notary-demo/build/nodes/nodesRaft/runnodes``, which will start the nodes in separate terminal windows/tabs. Wait until a "Node started up and registered in ..." message appears on each of the terminals -3. Run ``./gradlew samples:notary-demo:notarise`` to make a call to the "Party" node to initiate notarisation requests +3. Run ``./gradlew samples:notary-demo:notarise`` to make a call to the "Alice Corp" node to initiate notarisation requests In a few seconds you will see a message "Notarised 10 transactions" with a list of transaction ids and the signer public keys To run from the command line in Windows: @@ -32,7 +28,7 @@ To run from the command line in Windows: Single notaries). 2. Run ``samples\notary-demo\build\nodes\nodesRaft\runnodes``, which will start the nodes in separate terminal windows/tabs. Wait until a "Node started up and registered in ..." message appears on each of the terminals -3. Run ``gradlew samples:notary-demo:notarise`` to make a call to the "Party" node to initiate notarisation requests +3. Run ``gradlew samples:notary-demo:notarise`` to make a call to the "Alice Corp" node to initiate notarisation requests In a few seconds you will see a message "Notarised 10 transactions" with a list of transaction ids and the signer public keys To run the BFT SMaRt notary demo, use ``nodesBFT`` instead of ``nodesRaft`` in the path (you will see messages from notary nodes diff --git a/samples/notary-demo/build.gradle b/samples/notary-demo/build.gradle index 081546b3ff..586d67109b 100644 --- a/samples/notary-demo/build.gradle +++ b/samples/notary-demo/build.gradle @@ -62,14 +62,6 @@ task deployNodesSingle(type: Cordform, dependsOn: 'jar') { } rpcUsers = [[user: "demou", password: "demop", permissions: ["ALL"]]] } - node { - name "O=Bob Plc,L=Rome,C=IT" - p2pPort 10005 - rpcSettings { - address "localhost:10006" - adminAddress "localhost:10106" - } - } node { name "O=Notary Service,L=Zurich,C=CH" p2pPort 10009 @@ -95,14 +87,6 @@ task deployNodesCustom(type: Cordform, dependsOn: 'jar') { } rpcUsers = [[user: "demou", password: "demop", permissions: ["ALL"]]] } - node { - name "O=Bob Plc,L=Rome,C=IT" - p2pPort 10005 - rpcSettings { - address "localhost:10006" - adminAddress "localhost:10106" - } - } node { name "O=Notary Service,L=Zurich,C=CH" p2pPort 10009 @@ -128,14 +112,6 @@ task deployNodesRaft(type: Cordform, dependsOn: 'jar') { } rpcUsers = [[user: "demou", password: "demop", permissions: ["ALL"]]] } - node { - name "O=Bob Plc,L=Rome,C=IT" - p2pPort 10005 - rpcSettings { - address "localhost:10006" - adminAddress "localhost:10106" - } - } node { name "O=Notary Service 0,L=Zurich,C=CH" p2pPort 10009 @@ -200,14 +176,6 @@ task deployNodesBFT(type: Cordform, dependsOn: 'jar') { } rpcUsers = [[user: "demou", password: "demop", permissions: ["ALL"]]] } - node { - name "O=Bob Plc,L=Rome,C=IT" - p2pPort 10005 - rpcSettings { - address "localhost:10006" - adminAddress "localhost:10106" - } - } node { name "O=Notary Service 0,L=Zurich,C=CH" p2pPort 10009 diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt index dd1f424d1d..ad684fc489 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt @@ -13,6 +13,7 @@ import net.corda.core.node.services.CordaService import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionWithSignatures import net.corda.core.transactions.WireTransaction +import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.transactions.PersistentUniquenessProvider import java.security.PublicKey import java.security.SignatureException @@ -25,8 +26,8 @@ import java.security.SignatureException */ // START 1 @CordaService -class MyCustomValidatingNotaryService(override val services: AppServiceHub, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { - override val uniquenessProvider = PersistentUniquenessProvider(services.clock) +class MyCustomValidatingNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { + override val uniquenessProvider = PersistentUniquenessProvider(services.clock, services.database, services.cacheFactory) override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic<Void?> = MyValidatingNotaryFlow(otherPartySession, this) diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt index 1de2b92219..ee994cf5bc 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt @@ -3,7 +3,6 @@ package net.corda.notarydemo import net.corda.client.rpc.CordaRPCClient import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.toStringShort -import net.corda.core.identity.PartyAndCertificate import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow import net.corda.core.transactions.SignedTransaction @@ -12,6 +11,7 @@ import net.corda.core.utilities.getOrThrow import net.corda.notarydemo.flows.DummyIssueAndMove import net.corda.notarydemo.flows.RPCStartableNotaryFlowClient import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.TestIdentity import java.util.concurrent.Future fun main(args: Array<String>) { @@ -29,13 +29,8 @@ private class NotaryDemoClientApi(val rpc: CordaRPCOps) { checkNotNull(id) { "No unique notary identity, try cleaning the node directories." } } - private val counterparty by lazy { - val parties = rpc.networkMapSnapshot() - parties.fold(ArrayList<PartyAndCertificate>()) { acc, elem -> - acc.addAll(elem.legalIdentitiesAndCerts.filter { it.name == BOB_NAME }) - acc - }.single().party - } + /** A dummy identity. */ + private val counterparty = TestIdentity(BOB_NAME).party /** Makes calls to the node rpc to start transaction notarisation. */ fun notarise(count: Int) { diff --git a/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt index 324cdee343..86153de552 100644 --- a/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt +++ b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt @@ -8,8 +8,8 @@ import net.corda.testing.core.BOC_NAME import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_BANK_B_NAME import net.corda.testing.driver.DriverParameters -import net.corda.testing.node.User import net.corda.testing.driver.driver +import net.corda.testing.node.User import net.corda.traderdemo.flow.CommercialPaperIssueFlow import net.corda.traderdemo.flow.SellerFlow diff --git a/serialization-deterministic/build.gradle b/serialization-deterministic/build.gradle index 4b146c6b80..74190fd20a 100644 --- a/serialization-deterministic/build.gradle +++ b/serialization-deterministic/build.gradle @@ -11,8 +11,8 @@ def javaHome = System.getProperty('java.home') def jarBaseName = "corda-${project.name}".toString() configurations { - runtimeLibraries - runtimeArtifacts.extendsFrom runtimeLibraries + deterministicLibraries + deterministicArtifacts.extendsFrom deterministicLibraries } dependencies { @@ -20,10 +20,10 @@ dependencies { // Configure these by hand. It should be a minimal subset of dependencies, // and without any obviously non-deterministic ones such as Hibernate. - runtimeLibraries project(path: ':core-deterministic', configuration: 'runtimeArtifacts') - runtimeLibraries "org.apache.qpid:proton-j:$protonj_version" - runtimeLibraries "org.iq80.snappy:snappy:$snappy_version" - runtimeLibraries "com.google.guava:guava:$guava_version" + deterministicLibraries project(path: ':core-deterministic', configuration: 'deterministicArtifacts') + deterministicLibraries "org.apache.qpid:proton-j:$protonj_version" + deterministicLibraries "org.iq80.snappy:snappy:$snappy_version" + deterministicLibraries "com.google.guava:guava:$guava_version" } jar { @@ -108,7 +108,7 @@ task determinise(type: ProGuardTask) { libraryjars file("$javaHome/lib/rt.jar") libraryjars file("$javaHome/lib/jce.jar") - configurations.runtimeLibraries.forEach { + configurations.deterministicLibraries.forEach { libraryjars it, filter: '!META-INF/versions/**' } @@ -142,7 +142,7 @@ task checkDeterminism(type: ProGuardTask, dependsOn: jdkTask) { libraryjars deterministic_rt_jar - configurations.runtimeLibraries.forEach { + configurations.deterministicLibraries.forEach { libraryjars it, filter: '!META-INF/versions/**' } @@ -162,12 +162,12 @@ assemble.dependsOn checkDeterminism def deterministicJar = metafix.outputs.files.singleFile artifacts { - runtimeArtifacts file: deterministicJar, name: jarBaseName, type: 'jar', extension: 'jar', builtBy: metafix + deterministicArtifacts file: deterministicJar, name: jarBaseName, type: 'jar', extension: 'jar', builtBy: metafix publish file: deterministicJar, name: jarBaseName, type: 'jar', extension: 'jar', builtBy: metafix } publish { - dependenciesFrom configurations.runtimeArtifacts + dependenciesFrom configurations.deterministicArtifacts publishSources = false publishJavadoc = false name jarBaseName diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/CheckpointSerializationScheme.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/CheckpointSerializationScheme.kt new file mode 100644 index 0000000000..c370084e7a --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/CheckpointSerializationScheme.kt @@ -0,0 +1,49 @@ +package net.corda.serialization.internal + +import net.corda.core.KeepForDJVM +import net.corda.core.crypto.SecureHash +import net.corda.core.serialization.* +import net.corda.core.serialization.internal.CheckpointSerializationContext + +@KeepForDJVM +data class CheckpointSerializationContextImpl @JvmOverloads constructor( + override val deserializationClassLoader: ClassLoader, + override val whitelist: ClassWhitelist, + override val properties: Map<Any, Any>, + override val objectReferencesEnabled: Boolean, + override val encoding: SerializationEncoding?, + override val encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist) : CheckpointSerializationContext { + private val builder = AttachmentsClassLoaderBuilder(properties, deserializationClassLoader) + + /** + * {@inheritDoc} + * + * We need to cache the AttachmentClassLoaders to avoid too many contexts, since the class loader is part of cache key for the context. + */ + override fun withAttachmentsClassLoader(attachmentHashes: List<SecureHash>): CheckpointSerializationContext { + properties[attachmentsClassLoaderEnabledPropertyName] as? Boolean == true || return this + val classLoader = builder.build(attachmentHashes) ?: return this + return withClassLoader(classLoader) + } + + override fun withProperty(property: Any, value: Any): CheckpointSerializationContext { + return copy(properties = properties + (property to value)) + } + + override fun withoutReferences(): CheckpointSerializationContext { + return copy(objectReferencesEnabled = false) + } + + override fun withClassLoader(classLoader: ClassLoader): CheckpointSerializationContext { + return copy(deserializationClassLoader = classLoader) + } + + override fun withWhitelisted(clazz: Class<*>): CheckpointSerializationContext { + return copy(whitelist = object : ClassWhitelist { + override fun hasListed(type: Class<*>): Boolean = whitelist.hasListed(type) || type.name == clazz.name + }) + } + + override fun withEncoding(encoding: SerializationEncoding?) = copy(encoding = encoding) + override fun withEncodingWhitelist(encodingWhitelist: EncodingWhitelist) = copy(encodingWhitelist = encodingWhitelist) +} \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializeAsTokenContextImpl.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializeAsTokenContextImpl.kt index d028e91168..785ce47597 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializeAsTokenContextImpl.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializeAsTokenContextImpl.kt @@ -3,14 +3,14 @@ package net.corda.serialization.internal import net.corda.core.DeleteForDJVM import net.corda.core.node.ServiceHub -import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationFactory -import net.corda.core.serialization.SerializeAsToken -import net.corda.core.serialization.SerializeAsTokenContext +import net.corda.core.serialization.* +import net.corda.core.serialization.internal.CheckpointSerializationContext +import net.corda.core.serialization.internal.CheckpointSerializationFactory val serializationContextKey = SerializeAsTokenContext::class.java fun SerializationContext.withTokenContext(serializationContext: SerializeAsTokenContext): SerializationContext = this.withProperty(serializationContextKey, serializationContext) +fun CheckpointSerializationContext.withTokenContext(serializationContext: SerializeAsTokenContext): CheckpointSerializationContext = this.withProperty(serializationContextKey, serializationContext) /** * A context for mapping SerializationTokens to/from SerializeAsTokens. @@ -55,6 +55,53 @@ class SerializeAsTokenContextImpl(override val serviceHub: ServiceHub, init: Ser } } + override fun getSingleton(className: String) = classNameToSingleton[className] + ?: throw IllegalStateException("Unable to find tokenized instance of $className in context $this") +} + +/** + * A context for mapping SerializationTokens to/from SerializeAsTokens. + * + * A context is initialised with an object containing all the instances of [SerializeAsToken] to eagerly register all the tokens. + * In our case this can be the [ServiceHub]. + * + * Then it is a case of using the companion object methods on [SerializeAsTokenSerializer] to set and clear context as necessary + * when serializing to enable/disable tokenization. + */ +@DeleteForDJVM +class CheckpointSerializeAsTokenContextImpl(override val serviceHub: ServiceHub, init: SerializeAsTokenContext.() -> Unit) : SerializeAsTokenContext { + constructor(toBeTokenized: Any, serializationFactory: CheckpointSerializationFactory, context: CheckpointSerializationContext, serviceHub: ServiceHub) : this(serviceHub, { + serializationFactory.serialize(toBeTokenized, context.withTokenContext(this)) + }) + + private val classNameToSingleton = mutableMapOf<String, SerializeAsToken>() + private var readOnly = false + + init { + /** + * Go ahead and eagerly serialize the object to register all of the tokens in the context. + * + * This results in the toToken() method getting called for any [SingletonSerializeAsToken] instances which + * are encountered in the object graph as they are serialized and will therefore register the token to + * object mapping for those instances. We then immediately set the readOnly flag to stop further adhoc or + * accidental registrations from occuring as these could not be deserialized in a deserialization-first + * scenario if they are not part of this iniital context construction serialization. + */ + init(this) + readOnly = true + } + + override fun putSingleton(toBeTokenized: SerializeAsToken) { + val className = toBeTokenized.javaClass.name + if (className !in classNameToSingleton) { + // Only allowable if we are in SerializeAsTokenContext init (readOnly == false) + if (readOnly) { + throw UnsupportedOperationException("Attempt to write token for lazy registered $className. All tokens should be registered during context construction.") + } + classNameToSingleton[className] = toBeTokenized + } + } + override fun getSingleton(className: String) = classNameToSingleton[className] ?: throw IllegalStateException("Unable to find tokenized instance of $className in context $this") } \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/UseCaseAwareness.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/UseCaseAwareness.kt index ebe030b81d..2ce03e1e3b 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/UseCaseAwareness.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/UseCaseAwareness.kt @@ -13,3 +13,11 @@ fun checkUseCase(allowedUseCases: EnumSet<SerializationContext.UseCase>) { throw IllegalStateException("UseCase '${currentContext.useCase}' is not within '$allowedUseCases'") } } + +fun checkUseCase(allowedUseCase: SerializationContext.UseCase) { + val currentContext: SerializationContext = SerializationFactory.currentFactory?.currentContext + ?: throw IllegalStateException("Current context is not set") + if (allowedUseCase != currentContext.useCase) { + throw IllegalStateException("UseCase '${currentContext.useCase}' is not '$allowedUseCase'") + } +} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt index f35c808077..94c0d2223f 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt @@ -163,8 +163,6 @@ abstract class AbstractAMQPSerializationScheme( return synchronized(serializerFactoriesForContexts) { serializerFactoriesForContexts.computeIfAbsent(Pair(context.whitelist, context.deserializationClassLoader)) { when (context.useCase) { - SerializationContext.UseCase.Checkpoint -> - throw IllegalStateException("AMQP should not be used for checkpoint serialization.") SerializationContext.UseCase.RPCClient -> rpcClientSerializerFactory(context) SerializationContext.UseCase.RPCServer -> diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt index 913accf079..6869e71d45 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt @@ -49,7 +49,7 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory) "$typeName[]" } else { - val arrayType = if (type.asClass()!!.componentType.isPrimitive) "[p]" else "[]" + val arrayType = if (type.asClass().componentType.isPrimitive) "[p]" else "[]" "${type.componentType().typeName}$arrayType" } } @@ -93,7 +93,7 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory) } open fun <T> List<T>.toArrayOfType(type: Type): Any { - val elementType = type.asClass() ?: throw AMQPNotSerializableException(type, "Unexpected array element type $type") + val elementType = type.asClass() val list = this return java.lang.reflect.Array.newInstance(elementType, this.size).apply { (0..lastIndex).forEach { java.lang.reflect.Array.set(this, it, list[it]) } @@ -105,7 +105,7 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory) // the array since Kotlin won't allow an implicit cast from Int (as they're stored as 16bit ints) to Char class CharArraySerializer(factory: SerializerFactory) : ArraySerializer(Array<Char>::class.java, factory) { override fun <T> List<T>.toArrayOfType(type: Type): Any { - val elementType = type.asClass() ?: throw AMQPNotSerializableException(type, "Unexpected array element type $type") + val elementType = type.asClass() val list = this return java.lang.reflect.Array.newInstance(elementType, this.size).apply { (0..lastIndex).forEach { java.lang.reflect.Array.set(this, it, (list[it] as Int).toChar()) } @@ -159,11 +159,7 @@ class PrimCharArraySerializer(factory: SerializerFactory) : PrimArraySerializer( } override fun <T> List<T>.toArrayOfType(type: Type): Any { - val elementType = type.asClass() ?: throw AMQPNotSerializableException( - type, - "Unexpected array element type $type", - "blob is corrupt") - + val elementType = type.asClass() val list = this return java.lang.reflect.Array.newInstance(elementType, this.size).apply { val array = this diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CorDappCustomSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CorDappCustomSerializer.kt index 9506fbd510..f1b509c940 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CorDappCustomSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CorDappCustomSerializer.kt @@ -93,8 +93,8 @@ class CorDappCustomSerializer( * For 3rd party plugin serializers we are going to exist on exact type matching. i.e. we will * not support base class serializers for derivedtypes */ - override fun isSerializerFor(clazz: Class<*>) : Boolean { - return type.asClass()?.let { TypeToken.of(it) == TypeToken.of(clazz) } ?: false - } + override fun isSerializerFor(clazz: Class<*>) = + TypeToken.of(type.asClass()) == TypeToken.of(clazz) + } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt index f99a842758..cbd54f08c2 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt @@ -67,7 +67,7 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor { override fun isSerializerFor(clazz: Class<*>): Boolean = clazz == this.clazz override val type: Type get() = clazz override val typeDescriptor: Symbol by lazy { - Symbol.valueOf("$DESCRIPTOR_DOMAIN:${SerializerFingerPrinter().fingerprintForDescriptors(superClassSerializer.typeDescriptor.toString(), nameForType(clazz))}") + Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForDescriptors(superClassSerializer.typeDescriptor.toString(), nameForType(clazz))}") } private val typeNotation: TypeNotation = RestrictedType( SerializerFactory.nameForType(clazz), diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt index 6d75e3a553..b9c50f7250 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt @@ -156,7 +156,7 @@ class DeserializationInput constructor( "is outside of the bounds for the list of size: ${objectHistory.size}") val objectRetrieved = objectHistory[objectIndex] - if (!objectRetrieved::class.java.isSubClassOf(type.asClass()!!)) { + if (!objectRetrieved::class.java.isSubClassOf(type.asClass())) { throw AMQPNotSerializableException( type, "Existing reference type mismatch. Expected: '$type', found: '${objectRetrieved::class.java}' " + diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt index 2dfb36b503..5e7010c71c 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt @@ -80,7 +80,7 @@ class EnumEvolutionSerializer( val renameRules: List<RenameSchemaTransform>? = uncheckedCast(transforms[TransformTypes.Rename]) // What values exist on the enum as it exists on the class path - val localValues = new.type.asClass()!!.enumConstants.map { it.toString() } + val localValues = new.type.asClass().enumConstants.map { it.toString() } val conversions: MutableMap<String, String> = localValues .union(defaultRules?.map { it.new }?.toSet() ?: emptySet()) @@ -130,7 +130,7 @@ class EnumEvolutionSerializer( throw AMQPNotSerializableException(type, "No rule to evolve enum constant $type::$enumName") } - return type.asClass()!!.enumConstants[ordinals[conversions[enumName]]!!] + return type.asClass().enumConstants[ordinals[conversions[enumName]]!!] } override fun writeClassInfo(output: SerializationOutput) { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumSerializer.kt index 34b6697901..1bb12190f2 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumSerializer.kt @@ -34,7 +34,7 @@ class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: Seria ): Any { val enumName = (obj as List<*>)[0] as String val enumOrd = obj[1] as Int - val fromOrd = type.asClass()!!.enumConstants[enumOrd] as Enum<*>? + val fromOrd = type.asClass().enumConstants[enumOrd] as Enum<*>? if (enumName != fromOrd?.name) { throw AMQPNotSerializableException( diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt index 0aec3e6832..79a3f85c00 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt @@ -32,7 +32,7 @@ abstract class EvolutionSerializer( clazz: Type, factory: SerializerFactory, protected val oldReaders: Map<String, OldParam>, - override val kotlinConstructor: KFunction<Any>? + override val kotlinConstructor: KFunction<Any> ) : ObjectSerializer(clazz, factory) { // explicitly set as empty to indicate it's unused by this type of serializer override val propertySerializers = PropertySerializersEvolution() @@ -74,7 +74,7 @@ abstract class EvolutionSerializer( * TODO: rename annotation */ private fun getEvolverConstructor(type: Type, oldArgs: Map<String, OldParam>): KFunction<Any>? { - val clazz: Class<*> = type.asClass()!! + val clazz: Class<*> = type.asClass() if (!clazz.isConcreteClass) return null @@ -189,7 +189,7 @@ abstract class EvolutionSerializer( // return the synthesised object which is, given the absence of a constructor, a no op val constructor = getEvolverConstructor(new.type, readersAsSerialized) ?: return new - val classProperties = new.type.asClass()?.propertyDescriptors() ?: emptyMap() + val classProperties = new.type.asClass().propertyDescriptors() return if (classProperties.isNotEmpty() && constructor.parameters.isEmpty()) { makeWithSetters(new, factory, constructor, readersAsSerialized, classProperties) @@ -210,7 +210,7 @@ class EvolutionSerializerViaConstructor( clazz: Type, factory: SerializerFactory, oldReaders: Map<String, EvolutionSerializer.OldParam>, - kotlinConstructor: KFunction<Any>?, + kotlinConstructor: KFunction<Any>, private val constructorArgs: Array<Any?>) : EvolutionSerializer(clazz, factory, oldReaders, kotlinConstructor) { /** * Unlike a normal [readObject] call where we simply apply the parameter deserialisers @@ -242,7 +242,7 @@ class EvolutionSerializerViaSetters( clazz: Type, factory: SerializerFactory, oldReaders: Map<String, EvolutionSerializer.OldParam>, - kotlinConstructor: KFunction<Any>?, + kotlinConstructor: KFunction<Any>, private val setters: Map<String, PropertyAccessor>) : EvolutionSerializer(clazz, factory, oldReaders, kotlinConstructor) { override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt index 6b698dd057..d03e3e6da1 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt @@ -3,15 +3,15 @@ package net.corda.serialization.internal.amqp import com.google.common.hash.Hasher import com.google.common.hash.Hashing import net.corda.core.KeepForDJVM +import net.corda.core.internal.isConcreteClass import net.corda.core.internal.kotlinObjectInstance -import net.corda.core.utilities.loggerFor import net.corda.core.utilities.toBase64 -import java.io.NotSerializableException +import net.corda.serialization.internal.amqp.SerializerFactory.Companion.isPrimitive import java.lang.reflect.* import java.util.* /** - * Should be implemented by classes which wish to provide plugable fingerprinting og types for a [SerializerFactory] + * Should be implemented by classes which wish to provide pluggable fingerprinting on types for a [SerializerFactory] */ @KeepForDJVM interface FingerPrinter { @@ -20,34 +20,13 @@ interface FingerPrinter { * of said type such that any modification to any sub element wll generate a different fingerprint */ fun fingerprint(type: Type): String - - /** - * If required, associate an instance of the fingerprinter with a specific serializer factory - */ - fun setOwner(factory: SerializerFactory) } /** * Implementation of the finger printing mechanism used by default */ @KeepForDJVM -class SerializerFingerPrinter : FingerPrinter { - private var factory: SerializerFactory? = null - - private val ARRAY_HASH: String = "Array = true" - private val ENUM_HASH: String = "Enum = true" - private val ALREADY_SEEN_HASH: String = "Already seen = true" - private val NULLABLE_HASH: String = "Nullable = true" - private val NOT_NULLABLE_HASH: String = "Nullable = false" - private val ANY_TYPE_HASH: String = "Any type = true" - private val TYPE_VARIABLE_HASH: String = "Type variable = true" - private val WILDCARD_TYPE_HASH: String = "Wild card = true" - - private val logger by lazy { loggerFor<Schema>() } - - override fun setOwner(factory: SerializerFactory) { - this.factory = factory - } +class SerializerFingerPrinter(val factory: SerializerFactory) : FingerPrinter { /** * The method generates a fingerprint for a given JVM [Type] that should be unique to the schema representation. @@ -57,147 +36,167 @@ class SerializerFingerPrinter : FingerPrinter { * The idea being that even for two classes that share the same name but differ in a minor way, the fingerprint will be * different. */ - override fun fingerprint(type: Type): String { - return fingerprintForType( - type, null, HashSet(), Hashing.murmur3_128().newHasher(), debugIndent = 1).hash().asBytes().toBase64() + override fun fingerprint(type: Type): String = FingerPrintingState(factory).fingerprint(type) +} + +// Representation of the current state of fingerprinting +internal class FingerPrintingState(private val factory: SerializerFactory) { + + companion object { + private const val ARRAY_HASH: String = "Array = true" + private const val ENUM_HASH: String = "Enum = true" + private const val ALREADY_SEEN_HASH: String = "Already seen = true" + private const val NULLABLE_HASH: String = "Nullable = true" + private const val NOT_NULLABLE_HASH: String = "Nullable = false" + private const val ANY_TYPE_HASH: String = "Any type = true" } - private fun isCollectionOrMap(type: Class<*>) = - (Collection::class.java.isAssignableFrom(type) || Map::class.java.isAssignableFrom(type)) - && !EnumSet::class.java.isAssignableFrom(type) + private val typesSeen: MutableSet<Type> = mutableSetOf() + private var currentContext: Type? = null + private var hasher: Hasher = newDefaultHasher() - internal fun fingerprintForDescriptors(vararg typeDescriptors: String): String { - val hasher = Hashing.murmur3_128().newHasher() - for (typeDescriptor in typeDescriptors) { - hasher.putUnencodedChars(typeDescriptor) - } - return hasher.hash().asBytes().toBase64() - } - - private fun Hasher.fingerprintWithCustomSerializerOrElse( - factory: SerializerFactory, - clazz: Class<*>, - declaredType: Type, - block: () -> Hasher): Hasher { - // Need to check if a custom serializer is applicable - val customSerializer = factory.findCustomSerializer(clazz, declaredType) - return if (customSerializer != null) { - putUnencodedChars(customSerializer.typeDescriptor) - } else { - block() - } - } + // Fingerprint the type recursively, and return the encoded fingerprint written into the hasher. + fun fingerprint(type: Type) = fingerprintType(type).hasher.fingerprint // This method concatenates various elements of the types recursively as unencoded strings into the hasher, // effectively creating a unique string for a type which we then hash in the calling function above. - private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: MutableSet<Type>, - hasher: Hasher, debugIndent: Int = 1): Hasher { - // We don't include Example<?> and Example<T> where type is ? or T in this otherwise we - // generate different fingerprints for class Outer<T>(val a: Inner<T>) when serialising - // and deserializing (assuming deserialization is occurring in a factory that didn't - // serialise the object in the first place (and thus the cache lookup fails). This is also - // true of Any, where we need Example<A, B> and Example<?, ?> to have the same fingerprint - return if ((type in alreadySeen) - && (type !== SerializerFactory.AnyType) - && (type !is TypeVariable<*>) - && (type !is WildcardType) - ) { - hasher.putUnencodedChars(ALREADY_SEEN_HASH) - } else { - alreadySeen += type - ifThrowsAppend<Hasher>({ type.typeName }) { - when (type) { - is ParameterizedType -> { - // Hash the rawType + params - val clazz = type.rawType as Class<*> + private fun fingerprintType(type: Type): FingerPrintingState = apply { + // Don't go round in circles. + if (hasSeen(type)) append(ALREADY_SEEN_HASH) + else ifThrowsAppend( + { type.typeName }, + { + typesSeen.add(type) + currentContext = type + fingerprintNewType(type) + }) + } - val startingHash = if (isCollectionOrMap(clazz)) { - hasher.putUnencodedChars(clazz.name) - } else { - hasher.fingerprintWithCustomSerializerOrElse(factory!!, clazz, type) { - fingerprintForObject(type, type, alreadySeen, hasher, factory!!, debugIndent + 1) - } - } + // For a type we haven't seen before, determine the correct path depending on the type of type it is. + private fun fingerprintNewType(type: Type) = when (type) { + is ParameterizedType -> fingerprintParameterizedType(type) + // Previously, we drew a distinction between TypeVariable, WildcardType, and AnyType, changing + // the signature of the fingerprinted object. This, however, doesn't work as it breaks bi- + // directional fingerprints. That is, fingerprinting a concrete instance of a generic + // type (Example<Int>), creates a different fingerprint from the generic type itself (Example<T>) + // + // On serialization Example<Int> is treated as Example<T>, a TypeVariable + // On deserialisation it is seen as Example<?>, A WildcardType *and* a TypeVariable + // Note: AnyType is a special case of WildcardType used in other parts of the + // serializer so both cases need to be dealt with here + // + // If we treat these types as fundamentally different and alter the fingerprint we will + // end up breaking into the evolver when we shouldn't or, worse, evoking the carpenter. + is SerializerFactory.AnyType, + is WildcardType, + is TypeVariable<*> -> append("?$ANY_TYPE_HASH") + is Class<*> -> fingerprintClass(type) + is GenericArrayType -> fingerprintType(type.genericComponentType).append(ARRAY_HASH) + else -> throw AMQPNotSerializableException(type, "Don't know how to hash") + } - // ... and concatenate the type data for each parameter type. - type.actualTypeArguments.fold(startingHash) { orig, paramType -> - fingerprintForType(paramType, type, alreadySeen, orig, debugIndent + 1) - } - } - // Previously, we drew a distinction between TypeVariable, WildcardType, and AnyType, changing - // the signature of the fingerprinted object. This, however, doesn't work as it breaks bi- - // directional fingerprints. That is, fingerprinting a concrete instance of a generic - // type (Example<Int>), creates a different fingerprint from the generic type itself (Example<T>) - // - // On serialization Example<Int> is treated as Example<T>, a TypeVariable - // On deserialisation it is seen as Example<?>, A WildcardType *and* a TypeVariable - // Note: AnyType is a special case of WildcardType used in other parts of the - // serializer so both cases need to be dealt with here - // - // If we treat these types as fundamentally different and alter the fingerprint we will - // end up breaking into the evolver when we shouldn't or, worse, evoking the carpenter. - is SerializerFactory.AnyType, - is WildcardType, - is TypeVariable<*> -> { - hasher.putUnencodedChars("?").putUnencodedChars(ANY_TYPE_HASH) - } - is Class<*> -> { - if (type.isArray) { - fingerprintForType(type.componentType, contextType, alreadySeen, hasher, debugIndent + 1) - .putUnencodedChars(ARRAY_HASH) - } else if (SerializerFactory.isPrimitive(type)) { - hasher.putUnencodedChars(type.name) - } else if (isCollectionOrMap(type)) { - hasher.putUnencodedChars(type.name) - } else if (type.isEnum) { - // ensures any change to the enum (adding constants) will trigger the need for evolution - hasher.apply { - type.enumConstants.forEach { - putUnencodedChars(it.toString()) - } - }.putUnencodedChars(type.name).putUnencodedChars(ENUM_HASH) - } else { - hasher.fingerprintWithCustomSerializerOrElse(factory!!, type, type) { - if (type.kotlinObjectInstance != null) { - // TODO: name collision is too likely for kotlin objects, we need to introduce some - // reference to the CorDapp but maybe reference to the JAR in the short term. - hasher.putUnencodedChars(type.name) - } else { - fingerprintForObject(type, type, alreadySeen, hasher, factory!!, debugIndent + 1) - } - } - } - } - // Hash the element type + some array hash - is GenericArrayType -> { - fingerprintForType(type.genericComponentType, contextType, alreadySeen, - hasher, debugIndent + 1).putUnencodedChars(ARRAY_HASH) - } - else -> throw AMQPNotSerializableException(type, "Don't know how to hash") - } - } + private fun fingerprintClass(type: Class<*>) = when { + type.isArray -> fingerprintType(type.componentType).append(ARRAY_HASH) + type.isPrimitiveOrCollection -> append(type.name) + type.isEnum -> fingerprintEnum(type) + else -> fingerprintWithCustomSerializerOrElse(type, type) { + if (type.kotlinObjectInstance != null) append(type.name) + else fingerprintObject(type) } } - private fun fingerprintForObject( - type: Type, - contextType: Type?, - alreadySeen: MutableSet<Type>, - hasher: Hasher, - factory: SerializerFactory, - debugIndent: Int = 0): Hasher { - // Hash the class + properties + interfaces - val name = type.asClass()?.name - ?: throw AMQPNotSerializableException(type, "Expected only Class or ParameterizedType but found $type") + private fun fingerprintParameterizedType(type: ParameterizedType) { + // Hash the rawType + params + type.asClass().let { clazz -> + if (clazz.isCollectionOrMap) append(clazz.name) + else fingerprintWithCustomSerializerOrElse(clazz, type) { + fingerprintObject(type) + } + } - propertiesForSerialization(constructorForDeserialization(type), contextType ?: type, factory) - .serializationOrder - .fold(hasher.putUnencodedChars(name)) { orig, prop -> - fingerprintForType(prop.serializer.resolvedType, type, alreadySeen, orig, debugIndent + 1) - .putUnencodedChars(prop.serializer.name) - .putUnencodedChars(if (prop.serializer.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH) - } - interfacesForSerialization(type, factory).map { fingerprintForType(it, type, alreadySeen, hasher, debugIndent + 1) } - return hasher + // ...and concatenate the type data for each parameter type. + type.actualTypeArguments.forEach { paramType -> + fingerprintType(paramType) + } } -} \ No newline at end of file + + private fun fingerprintObject(type: Type) { + // Hash the class + properties + interfaces + append(type.asClass().name) + + orderedPropertiesForSerialization(type).forEach { prop -> + fingerprintType(prop.serializer.resolvedType) + fingerprintPropSerialiser(prop) + } + + interfacesForSerialization(type, factory).forEach { iface -> + fingerprintType(iface) + } + } + + // ensures any change to the enum (adding constants) will trigger the need for evolution + private fun fingerprintEnum(type: Class<*>) { + append(type.enumConstants.joinToString()) + append(type.name) + append(ENUM_HASH) + } + + private fun fingerprintPropSerialiser(prop: PropertyAccessor) { + append(prop.serializer.name) + append(if (prop.serializer.mandatory) NOT_NULLABLE_HASH + else NULLABLE_HASH) + } + + // Write the given character sequence into the hasher. + private fun append(chars: CharSequence) { + hasher = hasher.putUnencodedChars(chars) + } + + // Give any custom serializers loaded into the factory the chance to supply their own type-descriptors + private fun fingerprintWithCustomSerializerOrElse( + clazz: Class<*>, + declaredType: Type, + defaultAction: () -> Unit) + : Unit = factory.findCustomSerializer(clazz, declaredType)?.let { + append(it.typeDescriptor) + } ?: defaultAction() + + // Test whether we are in a state in which we have already seen the given type. + // + // We don't include Example<?> and Example<T> where type is ? or T in this otherwise we + // generate different fingerprints for class Outer<T>(val a: Inner<T>) when serialising + // and deserializing (assuming deserialization is occurring in a factory that didn't + // serialise the object in the first place (and thus the cache lookup fails). This is also + // true of Any, where we need Example<A, B> and Example<?, ?> to have the same fingerprint + private fun hasSeen(type: Type) = (type in typesSeen) + && (type !== SerializerFactory.AnyType) + && (type !is TypeVariable<*>) + && (type !is WildcardType) + + private fun orderedPropertiesForSerialization(type: Type): List<PropertyAccessor> { + return propertiesForSerialization( + if (type.asClass().isConcreteClass) constructorForDeserialization(type) else null, + currentContext ?: type, + factory).serializationOrder + } + +} + +// region Utility functions + +// Create a new instance of the [Hasher] used for fingerprinting by the default [SerializerFingerPrinter] +private fun newDefaultHasher() = Hashing.murmur3_128().newHasher() + +// We obtain a fingerprint from a [Hasher] by taking the Base 64 encoding of its hash bytes +private val Hasher.fingerprint get() = hash().asBytes().toBase64() + +internal fun fingerprintForDescriptors(vararg typeDescriptors: String): String = + newDefaultHasher().putUnencodedChars(typeDescriptors.joinToString()).fingerprint + +private val Class<*>.isCollectionOrMap get() = + (Collection::class.java.isAssignableFrom(this) || Map::class.java.isAssignableFrom(this)) + && !EnumSet::class.java.isAssignableFrom(this) + +private val Class<*>.isPrimitiveOrCollection get() = + isPrimitive(this) || isCollectionOrMap +// endregion diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt index 7a8f0d2334..dc142fce16 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt @@ -1,5 +1,6 @@ package net.corda.serialization.internal.amqp +import net.corda.core.internal.isConcreteClass import net.corda.core.serialization.SerializationContext import net.corda.core.utilities.contextLogger import net.corda.core.utilities.trace @@ -18,7 +19,7 @@ import kotlin.reflect.jvm.javaConstructor */ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerializer<Any> { override val type: Type get() = clazz - open val kotlinConstructor = constructorForDeserialization(clazz) + open val kotlinConstructor = if (clazz.asClass().isConcreteClass) constructorForDeserialization(clazz) else null val javaConstructor by lazy { kotlinConstructor?.javaConstructor } companion object { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertyDescriptor.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertyDescriptor.kt new file mode 100644 index 0000000000..882f067eba --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertyDescriptor.kt @@ -0,0 +1,205 @@ +package net.corda.serialization.internal.amqp + +import com.google.common.reflect.TypeToken +import net.corda.core.KeepForDJVM +import net.corda.core.internal.isPublic +import net.corda.serialization.internal.amqp.MethodClassifier.* +import java.lang.reflect.Field +import java.lang.reflect.Method +import java.lang.reflect.Type +import java.util.* + +/** + * Encapsulates the property of a class and its potential getter and setter methods. + * + * @property field a property of a class. + * @property setter the method of a class that sets the field. Determined by locating + * a function called setXyz on the class for the property named in field as xyz. + * @property getter the method of a class that returns a fields value. Determined by + * locating a function named getXyz for the property named in field as xyz. + */ +@KeepForDJVM +data class PropertyDescriptor(val field: Field?, val setter: Method?, val getter: Method?) { + override fun toString() = StringBuilder("").apply { + appendln("Property - ${field?.name ?: "null field"}\n") + appendln(" getter - ${getter?.name ?: "no getter"}") + appendln(" setter - ${setter?.name ?: "no setter"}") + }.toString() + + /** + * Check the types of the field, getter and setter methods against each other. + */ + fun validate() { + getter?.apply { + val getterType = genericReturnType + field?.apply { + if (!getterType.isSupertypeOf(genericReturnType)) + throw AMQPNotSerializableException( + declaringClass, + "Defined getter for parameter $name returns type $getterType " + + "yet underlying type is $genericType") + } + } + + setter?.apply { + val setterType = genericParameterTypes[0]!! + + field?.apply { + if (!genericType.isSupertypeOf(setterType)) + throw AMQPNotSerializableException( + declaringClass, + "Defined setter for parameter $name takes parameter of type $setterType " + + "yet underlying type is $genericType") + } + + getter?.apply { + if (!genericReturnType.isSupertypeOf(setterType)) + throw AMQPNotSerializableException( + declaringClass, + "Defined setter for parameter $name takes parameter of type $setterType, " + + "but getter returns $genericReturnType") + } + } + } +} + +private fun Type.isSupertypeOf(that: Type) = TypeToken.of(this).isSupertypeOf(that) + +// match an uppercase letter that also has a corresponding lower case equivalent +private val propertyMethodRegex = Regex("(?<type>get|set|is)(?<var>\\p{Lu}.*)") + +/** + * Collate the properties of a class and match them with their getter and setter + * methods as per a JavaBean. + * + * for a property + * exampleProperty + * + * We look for methods + * setExampleProperty + * getExampleProperty + * isExampleProperty + * + * Where getExampleProperty must return a type compatible with exampleProperty, setExampleProperty must + * take a single parameter of a type compatible with exampleProperty and isExampleProperty must + * return a boolean + */ +fun Class<out Any?>.propertyDescriptors(): Map<String, PropertyDescriptor> { + val fieldProperties = superclassChain().declaredFields().byFieldName() + + return superclassChain().declaredMethods() + .thatArePublic() + .thatArePropertyMethods() + .withValidSignature() + .byNameAndClassifier(fieldProperties.keys) + .toClassProperties(fieldProperties) + .validated() +} + +// Generate the sequence of classes starting with this class and ascending through it superclasses. +private fun Class<*>.superclassChain() = generateSequence(this, Class<*>::getSuperclass) + +// Obtain the fields declared by all classes in this sequence of classes. +private fun Sequence<Class<*>>.declaredFields() = flatMap { it.declaredFields.asSequence() } + +// Obtain the methods declared by all classes in this sequence of classes. +private fun Sequence<Class<*>>.declaredMethods() = flatMap { it.declaredMethods.asSequence() } + +// Map a sequence of fields by field name. +private fun Sequence<Field>.byFieldName() = map { it.name to it }.toMap() + +// Select only those methods that are public (and are not the "getClass" method) +private fun Sequence<Method>.thatArePublic() = filter { it.isPublic && it.name != "getClass" } + +// Select only those methods that are isX/getX/setX methods +private fun Sequence<Method>.thatArePropertyMethods() = map { method -> + propertyMethodRegex.find(method.name)?.let { result -> + PropertyNamedMethod( + result.groups[2]!!.value, + MethodClassifier.valueOf(result.groups[1]!!.value.toUpperCase()), + method) + } +}.filterNotNull() + +// Pick only those methods whose signatures are valid, discarding the remainder without warning. +private fun Sequence<PropertyNamedMethod>.withValidSignature() = filter { it.hasValidSignature() } + +// Group methods by name and classifier, picking the method with the least generic signature if there is more than one +// of a given name and type. +private fun Sequence<PropertyNamedMethod>.byNameAndClassifier(fieldNames: Set<String>): Map<String, Map<MethodClassifier, Method>> { + val result = mutableMapOf<String, EnumMap<MethodClassifier, Method>>() + + forEach { (fieldName, classifier, method) -> + result.compute(getPropertyName(fieldName, fieldNames)) { _, byClassifier -> + (byClassifier ?: EnumMap(MethodClassifier::class.java)).merge(classifier, method) + } + } + + return result +} + +// Merge the given method into a map of methods by method classifier, picking the least generic method for each classifier. +private fun EnumMap<MethodClassifier, Method>.merge(classifier: MethodClassifier, method: Method): EnumMap<MethodClassifier, Method> { + compute(classifier) { _, existingMethod -> + if (existingMethod == null) method + else when (classifier) { + IS -> existingMethod + GET -> leastGenericBy({ genericReturnType }, existingMethod, method) + SET -> leastGenericBy({ genericParameterTypes[0] }, existingMethod, method) + } + } + return this +} + +// Make the property name conform to the underlying field name, if there is one. +private fun getPropertyName(propertyName: String, fieldNames: Set<String>) = + if (propertyName.decapitalize() in fieldNames) propertyName.decapitalize() + else propertyName + + +// Which of the three types of property method the method is. +private enum class MethodClassifier { GET, SET, IS } + +private data class PropertyNamedMethod(val fieldName: String, val classifier: MethodClassifier, val method: Method) { + // Validate the method's signature against its classifier + fun hasValidSignature(): Boolean = method.run { + when (classifier) { + GET -> parameterCount == 0 && returnType != Void.TYPE + SET -> parameterCount == 1 && returnType == Void.TYPE + IS -> parameterCount == 0 && + (returnType == Boolean::class.java || + returnType == Boolean::class.javaObjectType) + } + } +} + +// Construct a map of PropertyDescriptors by name, by merging the raw field map with the map of classified property methods +private fun Map<String, Map<MethodClassifier, Method>>.toClassProperties(fieldMap: Map<String, Field>): Map<String, PropertyDescriptor> { + val result = mutableMapOf<String, PropertyDescriptor>() + + // Fields for which we have no property methods + for ((name, field) in fieldMap) { + if (name !in keys) { + result[name] = PropertyDescriptor(field, null, null) + } + } + + for ((name, methodMap) in this) { + result[name] = PropertyDescriptor( + fieldMap[name], + methodMap[SET], + methodMap[GET] ?: methodMap[IS] + ) + } + + return result +} + +// Select the least generic of two methods by a type associated with each. +private fun leastGenericBy(feature: Method.() -> Type, first: Method, second: Method) = + if (first.feature().isSupertypeOf(second.feature())) second else first + +// Throw an exception if any property descriptor is inconsistent, e.g. the types don't match +private fun Map<String, PropertyDescriptor>.validated() = apply { + forEach { _, value -> value.validate() } +} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializer.kt index e83c5c4119..3b3ee33478 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializer.kt @@ -19,8 +19,8 @@ sealed class PropertySerializer(val name: String, val propertyReader: PropertyRe val default: String? = generateDefault() val mandatory: Boolean = generateMandatory() - private val isInterface: Boolean get() = resolvedType.asClass()?.isInterface == true - private val isJVMPrimitive: Boolean get() = resolvedType.asClass()?.isPrimitive == true + private val isInterface: Boolean get() = resolvedType.asClass().isInterface + private val isJVMPrimitive: Boolean get() = resolvedType.asClass().isPrimitive private fun generateType(): String { return if (isInterface || resolvedType == Any::class.java) "*" else SerializerFactory.nameForType(resolvedType) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt index f393fb7aad..f2820cc505 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt @@ -2,15 +2,12 @@ package net.corda.serialization.internal.amqp import com.google.common.primitives.Primitives import com.google.common.reflect.TypeToken -import net.corda.core.KeepForDJVM import net.corda.core.internal.isConcreteClass -import net.corda.core.internal.isPublic import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.ConstructorForDeserialization import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializationContext import org.apache.qpid.proton.codec.Data -import java.io.NotSerializableException import java.lang.reflect.* import java.lang.reflect.Field import java.util.* @@ -26,42 +23,37 @@ import kotlin.reflect.jvm.javaType /** * Code for finding the constructor we will use for deserialization. * - * If there's only one constructor, it selects that. If there are two and one is the default, it selects the other. - * Otherwise it starts with the primary constructor in kotlin, if there is one, and then will override this with any that is - * annotated with [@ConstructorForDeserialization]. It will report an error if more than one constructor is annotated. + * If any constructor is uniquely annotated with [@ConstructorForDeserialization], then that constructor is chosen. + * An error is reported if more than one constructor is annotated. + * + * Otherwise, if there is a Kotlin primary constructor, it selects that, and if not it selects either the unique + * constructor or, if there are two and one is the default no-argument constructor, the non-default constructor. */ -fun constructorForDeserialization(type: Type): KFunction<Any>? { - val clazz: Class<*> = type.asClass()!! - if (clazz.isConcreteClass) { - var preferredCandidate: KFunction<Any>? = clazz.kotlin.primaryConstructor - var annotatedCount = 0 - val kotlinConstructors = clazz.kotlin.constructors - val hasDefault = kotlinConstructors.any { it.parameters.isEmpty() } - - for (kotlinConstructor in kotlinConstructors) { - if (preferredCandidate == null && kotlinConstructors.size == 1) { - preferredCandidate = kotlinConstructor - } else if (preferredCandidate == null && - kotlinConstructors.size == 2 && - hasDefault && - kotlinConstructor.parameters.isNotEmpty() - ) { - preferredCandidate = kotlinConstructor - } else if (kotlinConstructor.findAnnotation<ConstructorForDeserialization>() != null) { - if (annotatedCount++ > 0) { - throw AMQPNotSerializableException( - type, - "More than one constructor for $clazz is annotated with @ConstructorForDeserialization.") - } - preferredCandidate = kotlinConstructor - } - } - - return preferredCandidate?.apply { isAccessible = true } - ?: throw AMQPNotSerializableException(type, "No constructor for deserialization found for $clazz.") - } else { - return null +fun constructorForDeserialization(type: Type): KFunction<Any> { + val clazz = type.asClass().apply { + if (!isConcreteClass) throw AMQPNotSerializableException(type, + "Cannot find deserialisation constructor for non-concrete class $this") } + + val kotlinCtors = clazz.kotlin.constructors + + val annotatedCtors = kotlinCtors.filter { it.findAnnotation<ConstructorForDeserialization>() != null } + if (annotatedCtors.size > 1) throw AMQPNotSerializableException( + type, + "More than one constructor for $clazz is annotated with @ConstructorForDeserialization.") + + val defaultCtor = kotlinCtors.firstOrNull { it.parameters.isEmpty() } + val nonDefaultCtors = kotlinCtors.filter { it != defaultCtor } + + val preferredCandidate = annotatedCtors.firstOrNull() ?: + clazz.kotlin.primaryConstructor ?: + when(nonDefaultCtors.size) { + 1 -> nonDefaultCtors.first() + 0 -> defaultCtor ?: throw AMQPNotSerializableException(type, "No constructor found for $clazz.") + else -> throw AMQPNotSerializableException(type, "No unique non-default constructor found for $clazz.") + } + + return preferredCandidate.apply { isAccessible = true } } /** @@ -75,145 +67,13 @@ fun constructorForDeserialization(type: Type): KFunction<Any>? { fun <T : Any> propertiesForSerialization( kotlinConstructor: KFunction<T>?, type: Type, - factory: SerializerFactory): PropertySerializers { - return PropertySerializers.make( + factory: SerializerFactory): PropertySerializers = PropertySerializers.make( if (kotlinConstructor != null) { propertiesForSerializationFromConstructor(kotlinConstructor, type, factory) } else { - propertiesForSerializationFromAbstract(type.asClass()!!, type, factory) + propertiesForSerializationFromAbstract(type.asClass(), type, factory) }.sortedWith(PropertyAccessor) ) -} - -/** - * Encapsulates the property of a class and its potential getter and setter methods. - * - * @property field a property of a class. - * @property setter the method of a class that sets the field. Determined by locating - * a function called setXyz on the class for the property named in field as xyz. - * @property getter the method of a class that returns a fields value. Determined by - * locating a function named getXyz for the property named in field as xyz. - */ -@KeepForDJVM -data class PropertyDescriptor(var field: Field?, var setter: Method?, var getter: Method?, var iser: Method?) { - override fun toString() = StringBuilder("").apply { - appendln("Property - ${field?.name ?: "null field"}\n") - appendln(" getter - ${getter?.name ?: "no getter"}") - appendln(" setter - ${setter?.name ?: "no setter"}") - appendln(" iser - ${iser?.name ?: "no isXYZ defined"}") - }.toString() - - constructor() : this(null, null, null, null) - - fun preferredGetter(): Method? = getter ?: iser -} - -object PropertyDescriptorsRegex { - // match an uppercase letter that also has a corresponding lower case equivalent - val re = Regex("(?<type>get|set|is)(?<var>\\p{Lu}.*)") -} - -/** - * Collate the properties of a class and match them with their getter and setter - * methods as per a JavaBean. - * - * for a property - * exampleProperty - * - * We look for methods - * setExampleProperty - * getExampleProperty - * isExampleProperty - * - * Where setExampleProperty must return a type compatible with exampleProperty, getExampleProperty must - * take a single parameter of a type compatible with exampleProperty and isExampleProperty must - * return a boolean - */ -fun Class<out Any?>.propertyDescriptors(): Map<String, PropertyDescriptor> { - val classProperties = mutableMapOf<String, PropertyDescriptor>() - - var clazz: Class<out Any?>? = this - - do { - clazz!!.declaredFields.forEach { property -> - classProperties.computeIfAbsent(property.name) { - PropertyDescriptor() - }.apply { - this.field = property - } - } - clazz = clazz.superclass - } while (clazz != null) - - // - // Running as two loops rather than one as we need to ensure we have captured all of the properties - // before looking for interacting methods and need to cope with the class hierarchy introducing - // new properties / methods - // - clazz = this - do { - // Note: It is possible for a class to have multiple instances of a function where the types - // differ. For example: - // interface I<out T> { val a: T } - // class D(override val a: String) : I<String> - // instances of D will have both - // getA - returning a String (java.lang.String) and - // getA - returning an Object (java.lang.Object) - // In this instance we take the most derived object - // - // In addition, only getters that take zero parameters and setters that take a single - // parameter will be considered - clazz!!.declaredMethods?.map { func -> - if (!func.isPublic) return@map - if (func.name == "getClass") return@map - - PropertyDescriptorsRegex.re.find(func.name)?.apply { - // matching means we have an func getX where the property could be x or X - // so having pre-loaded all of the properties we try to match to either case. If that - // fails the getter doesn't refer to a property directly, but may refer to a constructor - // parameter that shadows a property - val properties = - classProperties[groups[2]!!.value] ?: classProperties[groups[2]!!.value.decapitalize()] ?: - // take into account those constructor properties that don't directly map to a named - // property which are, by default, already added to the map - classProperties.computeIfAbsent(groups[2]!!.value) { PropertyDescriptor() } - - properties.apply { - when (groups[1]!!.value) { - "set" -> { - if (func.parameterCount == 1) { - if (setter == null) setter = func - else if (TypeToken.of(setter!!.genericReturnType).isSupertypeOf(func.genericReturnType)) { - setter = func - } - } - } - "get" -> { - if (func.parameterCount == 0) { - if (getter == null) getter = func - else if (TypeToken.of(getter!!.genericReturnType).isSupertypeOf(func.genericReturnType)) { - getter = func - } - } - } - "is" -> { - if (func.parameterCount == 0) { - val rtnType = TypeToken.of(func.genericReturnType) - if ((rtnType == TypeToken.of(Boolean::class.java)) - || (rtnType == TypeToken.of(Boolean::class.javaObjectType))) { - if (iser == null) iser = func - } - } - } - } - } - } - } - clazz = clazz.superclass - } while (clazz != null) - - return classProperties -} /** * From a constructor, determine which properties of a class are to be serialized. @@ -235,66 +95,48 @@ internal fun <T : Any> propertiesForSerializationFromConstructor( // think you could inspect the parameter and check the isSynthetic flag but that is always // false so given the naming convention is specified by the standard we can just check for // this - if (kotlinConstructor.javaConstructor?.parameterCount ?: 0 > 0 && - kotlinConstructor.javaConstructor?.parameters?.get(0)?.name == "this$0" - ) { - throw SyntheticParameterException(type) + kotlinConstructor.javaConstructor?.apply { + if (parameterCount > 0 && parameters[0].name == "this$0") throw SyntheticParameterException(type) } if (classProperties.isNotEmpty() && kotlinConstructor.parameters.isEmpty()) { return propertiesForSerializationFromSetters(classProperties, type, factory) } - return mutableListOf<PropertyAccessor>().apply { - kotlinConstructor.parameters.withIndex().forEach { param -> - // name cannot be null, if it is then this is a synthetic field and we will have bailed - // out prior to this - val name = param.value.name!! - - // We will already have disambiguated getA for property A or a but we still need to cope - // with the case we don't know the case of A when the parameter doesn't match a property - // but has a getter - val matchingProperty = classProperties[name] ?: classProperties[name.capitalize()] - ?: throw AMQPNotSerializableException(type, - "Constructor parameter - \"$name\" - doesn't refer to a property of \"$clazz\"") - - // If the property has a getter we'll use that to retrieve it's value from the instance, if it doesn't - // *for *know* we switch to a reflection based method - val propertyReader = if (matchingProperty.getter != null) { - val getter = matchingProperty.getter ?: throw AMQPNotSerializableException( - type, - "Property has no getter method for - \"$name\" - of \"$clazz\". If using Java and the parameter name" - + "looks anonymous, check that you have the -parameters option specified in the " - + "Java compiler. Alternately, provide a proxy serializer " - + "(SerializationCustomSerializer) if recompiling isn't an option.") - - val returnType = resolveTypeVariables(getter.genericReturnType, type) - if (!constructorParamTakesReturnTypeOfGetter(returnType, getter.genericReturnType, param.value)) { - throw AMQPNotSerializableException( - type, - "Property - \"$name\" - has type \"$returnType\" on \"$clazz\" but differs from constructor " + - "parameter type \"${param.value.type.javaType}\"") - } - - Pair(PublicPropertyReader(getter), returnType) - } else { - val field = classProperties[name]!!.field - ?: throw AMQPNotSerializableException(type, - "No property matching constructor parameter named - \"$name\" - " + - "of \"$clazz\". If using Java, check that you have the -parameters option specified " + - "in the Java compiler. Alternately, provide a proxy serializer " + - "(SerializationCustomSerializer) if recompiling isn't an option") - - Pair(PrivatePropertyReader(field, type), resolveTypeVariables(field.genericType, type)) - } - - this += PropertyAccessorConstructor( - param.index, - PropertySerializer.make(name, propertyReader.first, propertyReader.second, factory)) - } + return kotlinConstructor.parameters.withIndex().map { param -> + toPropertyAccessorConstructor(param.index, param.value, classProperties, type, clazz, factory) } } +private fun toPropertyAccessorConstructor(index: Int, param: KParameter, classProperties: Map<String, PropertyDescriptor>, type: Type, clazz: Class<out Any>, factory: SerializerFactory): PropertyAccessorConstructor { + // name cannot be null, if it is then this is a synthetic field and we will have bailed + // out prior to this + val name = param.name!! + + // We will already have disambiguated getA for property A or a but we still need to cope + // with the case we don't know the case of A when the parameter doesn't match a property + // but has a getter + val matchingProperty = classProperties[name] ?: classProperties[name.capitalize()] + ?: throw AMQPNotSerializableException(type, + "Constructor parameter - \"$name\" - doesn't refer to a property of \"$clazz\"") + + // If the property has a getter we'll use that to retrieve it's value from the instance, if it doesn't + // *for *now* we switch to a reflection based method + val propertyReader = matchingProperty.getter?.let { getter -> + getPublicPropertyReader(getter, type, param, name, clazz) + } ?: matchingProperty.field?.let { field -> + getPrivatePropertyReader(field, type) + } ?: throw AMQPNotSerializableException(type, + "No property matching constructor parameter named - \"$name\" - " + + "of \"${param}\". If using Java, check that you have the -parameters option specified " + + "in the Java compiler. Alternately, provide a proxy serializer " + + "(SerializationCustomSerializer) if recompiling isn't an option") + + return PropertyAccessorConstructor( + index, + PropertySerializer.make(name, propertyReader.first, propertyReader.second, factory)) +} + /** * If we determine a class has a constructor that takes no parameters then check for pairs of getters / setters * and use those @@ -302,107 +144,83 @@ internal fun <T : Any> propertiesForSerializationFromConstructor( fun propertiesForSerializationFromSetters( properties: Map<String, PropertyDescriptor>, type: Type, - factory: SerializerFactory): List<PropertyAccessor> { - return mutableListOf<PropertyAccessorGetterSetter>().apply { - var idx = 0 + factory: SerializerFactory): List<PropertyAccessor> = + properties.asSequence().withIndex().map { (index, entry) -> + val (name, property) = entry - properties.forEach { property -> - val getter: Method? = property.value.preferredGetter() - val setter: Method? = property.value.setter + val getter = property.getter + val setter = property.setter - if (getter == null || setter == null) return@forEach + if (getter == null || setter == null) return@map null - if (setter.parameterCount != 1) { - throw AMQPNotSerializableException( - type, - "Defined setter for parameter ${property.value.field?.name} takes too many arguments") - } - - val setterType = setter.genericParameterTypes[0]!! - - if ((property.value.field != null) && - (!(TypeToken.of(property.value.field?.genericType!!).isSupertypeOf(setterType))) - ) { - throw AMQPNotSerializableException( - type, - "Defined setter for parameter ${property.value.field?.name} " + - "takes parameter of type $setterType yet underlying type is " + - "${property.value.field?.genericType!!}") - } - - // Make sure the getter returns the same type (within inheritance bounds) the setter accepts. - if (!(TypeToken.of(getter.genericReturnType).isSupertypeOf(setterType))) { - throw AMQPNotSerializableException( - type, - "Defined setter for parameter ${property.value.field?.name} " + - "takes parameter of type $setterType yet the defined getter returns a value of type " + - "${getter.returnType} [${getter.genericReturnType}]") - } - this += PropertyAccessorGetterSetter( - idx++, - PropertySerializer.make(property.key, PublicPropertyReader(getter), - resolveTypeVariables(getter.genericReturnType, type), factory), + PropertyAccessorGetterSetter( + index, + PropertySerializer.make( + name, + PublicPropertyReader(getter), + resolveTypeVariables(getter.genericReturnType, type), + factory), setter) - } - } -} + }.filterNotNull().toList() -private fun constructorParamTakesReturnTypeOfGetter( - getterReturnType: Type, - rawGetterReturnType: Type, - param: KParameter): Boolean { +private fun getPrivatePropertyReader(field: Field, type: Type) = + PrivatePropertyReader(field, type) to resolveTypeVariables(field.genericType, type) + +private fun getPublicPropertyReader(getter: Method, type: Type, param: KParameter, name: String, clazz: Class<out Any>): Pair<PublicPropertyReader, Type> { + val returnType = resolveTypeVariables(getter.genericReturnType, type) val paramToken = TypeToken.of(param.type.javaType) val rawParamType = TypeToken.of(paramToken.rawType) - return paramToken.isSupertypeOf(getterReturnType) - || paramToken.isSupertypeOf(rawGetterReturnType) - // cope with the case where the constructor parameter is a generic type (T etc) but we - // can discover it's raw type. When bounded this wil be the bounding type, unbounded - // generics this will be object - || rawParamType.isSupertypeOf(getterReturnType) - || rawParamType.isSupertypeOf(rawGetterReturnType) + if (!(paramToken.isSupertypeOf(returnType) + || paramToken.isSupertypeOf(getter.genericReturnType) + // cope with the case where the constructor parameter is a generic type (T etc) but we + // can discover it's raw type. When bounded this wil be the bounding type, unbounded + // generics this will be object + || rawParamType.isSupertypeOf(returnType) + || rawParamType.isSupertypeOf(getter.genericReturnType))) { + throw AMQPNotSerializableException( + type, + "Property - \"$name\" - has type \"$returnType\" on \"$clazz\" " + + "but differs from constructor parameter type \"${param.type.javaType}\"") + } + + return PublicPropertyReader(getter) to returnType } private fun propertiesForSerializationFromAbstract( clazz: Class<*>, type: Type, - factory: SerializerFactory): List<PropertyAccessor> { - val properties = clazz.propertyDescriptors() - - return mutableListOf<PropertyAccessorConstructor>().apply { - properties.toList().withIndex().forEach { - val getter = it.value.second.getter ?: return@forEach - if (it.value.second.field == null) return@forEach + factory: SerializerFactory): List<PropertyAccessor> = + clazz.propertyDescriptors().asSequence().withIndex().map { (index, entry) -> + val (name, property) = entry + if (property.getter == null || property.field == null) return@map null + val getter = property.getter val returnType = resolveTypeVariables(getter.genericReturnType, type) - this += PropertyAccessorConstructor( - it.index, - PropertySerializer.make(it.value.first, PublicPropertyReader(getter), returnType, factory)) - } - } -} -internal fun interfacesForSerialization(type: Type, serializerFactory: SerializerFactory): List<Type> { - val interfaces = LinkedHashSet<Type>() - exploreType(type, interfaces, serializerFactory) - return interfaces.toList() -} + PropertyAccessorConstructor( + index, + PropertySerializer.make(name, PublicPropertyReader(getter), returnType, factory)) + }.filterNotNull().toList() -private fun exploreType(type: Type?, interfaces: MutableSet<Type>, serializerFactory: SerializerFactory) { - val clazz = type?.asClass() - if (clazz != null) { - if (clazz.isInterface) { - if (serializerFactory.whitelist.isNotWhitelisted(clazz)) return // We stop exploring once we reach a branch that has no `CordaSerializable` annotation or whitelisting. - else interfaces += type - } - for (newInterface in clazz.genericInterfaces) { - if (newInterface !in interfaces) { - exploreType(resolveTypeVariables(newInterface, type), interfaces, serializerFactory) - } - } - val superClass = clazz.genericSuperclass ?: return - exploreType(resolveTypeVariables(superClass, type), interfaces, serializerFactory) +internal fun interfacesForSerialization(type: Type, serializerFactory: SerializerFactory): List<Type> = + exploreType(type, serializerFactory).toList() + +private fun exploreType(type: Type, serializerFactory: SerializerFactory, interfaces: MutableSet<Type> = LinkedHashSet()): MutableSet<Type> { + val clazz = type.asClass() + + if (clazz.isInterface) { + // Ignore classes we've already seen, and stop exploring once we reach a branch that has no `CordaSerializable` + // annotation or whitelisting. + if (clazz in interfaces || serializerFactory.whitelist.isNotWhitelisted(clazz)) return interfaces + else interfaces += type } + + (clazz.genericInterfaces.asSequence() + clazz.genericSuperclass) + .filterNotNull() + .forEach { exploreType(resolveTypeVariables(it, type), serializerFactory, interfaces) } + + return interfaces } /** @@ -459,21 +277,23 @@ fun resolveTypeVariables(actualType: Type, contextType: Type?): Type { } } -internal fun Type.asClass(): Class<*>? { - return when { - this is Class<*> -> this - this is ParameterizedType -> this.rawType.asClass() - this is GenericArrayType -> this.genericComponentType.asClass()?.arrayClass() - this is TypeVariable<*> -> this.bounds.first().asClass() - this is WildcardType -> this.upperBounds.first().asClass() - else -> null +internal fun Type.asClass(): Class<*> { + return when(this) { + is Class<*> -> this + is ParameterizedType -> this.rawType.asClass() + is GenericArrayType -> this.genericComponentType.asClass().arrayClass() + is TypeVariable<*> -> this.bounds.first().asClass() + is WildcardType -> this.upperBounds.first().asClass() + // Per https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Type.html, + // there is nothing else that it can be, so this can never happen. + else -> throw UnsupportedOperationException("Cannot convert $this to class") } } internal fun Type.asArray(): Type? { - return when { - this is Class<*> -> this.arrayClass() - this is ParameterizedType -> DeserializedGenericArrayType(this) + return when(this) { + is Class<*> -> this.arrayClass() + is ParameterizedType -> DeserializedGenericArrayType(this) else -> null } } @@ -506,7 +326,7 @@ internal fun Type.isSubClassOf(type: Type): Boolean { // ByteArrays, primitives and boxed primitives are not stored in the object history internal fun suitableForObjectReference(type: Type): Boolean { val clazz = type.asClass() - return type != ByteArray::class.java && (clazz != null && !clazz.isPrimitive && !Primitives.unwrap(clazz).isPrimitive) + return type != ByteArray::class.java && (!clazz.isPrimitive && !Primitives.unwrap(clazz).isPrimitive) } /** @@ -519,7 +339,7 @@ internal enum class CommonPropertyNames { fun ClassWhitelist.requireWhitelisted(type: Type) { - if (!this.isWhitelisted(type.asClass()!!)) { + if (!this.isWhitelisted(type.asClass())) { throw AMQPNotSerializableException( type, "Class \"$type\" is not on the whitelist or annotated with @CordaSerializable.") 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 657c72d99b..8c869e2eda 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 @@ -1,7 +1,6 @@ package net.corda.serialization.internal.amqp import com.google.common.primitives.Primitives -import com.google.common.reflect.TypeResolver import net.corda.core.DeleteForDJVM import net.corda.core.KeepForDJVM import net.corda.core.StubOutForDJVM @@ -54,7 +53,7 @@ open class SerializerFactory( val whitelist: ClassWhitelist, val classCarpenter: ClassCarpenter, private val evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(), - val fingerPrinter: FingerPrinter = SerializerFingerPrinter(), + val fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter, private val serializersByType: MutableMap<Type, AMQPSerializer<Any>>, val serializersByDescriptor: MutableMap<Any, AMQPSerializer<Any>>, private val customSerializers: MutableList<SerializerFor>, @@ -66,13 +65,13 @@ open class SerializerFactory( constructor(whitelist: ClassWhitelist, classCarpenter: ClassCarpenter, evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(), - fingerPrinter: FingerPrinter = SerializerFingerPrinter(), + fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter, onlyCustomSerializers: Boolean = false ) : this( whitelist, classCarpenter, evolutionSerializerGetter, - fingerPrinter, + fingerPrinterConstructor, ConcurrentHashMap(), ConcurrentHashMap(), CopyOnWriteArrayList(), @@ -86,18 +85,16 @@ open class SerializerFactory( carpenterClassLoader: ClassLoader, lenientCarpenter: Boolean = false, evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(), - fingerPrinter: FingerPrinter = SerializerFingerPrinter(), + fingerPrinterConstructor: (SerializerFactory) -> FingerPrinter = ::SerializerFingerPrinter, onlyCustomSerializers: Boolean = false ) : this( whitelist, ClassCarpenterImpl(whitelist, carpenterClassLoader, lenientCarpenter), evolutionSerializerGetter, - fingerPrinter, + fingerPrinterConstructor, onlyCustomSerializers) - init { - fingerPrinter.setOwner(this) - } + val fingerPrinter by lazy { fingerPrinterConstructor(this) } val classloader: ClassLoader get() = classCarpenter.classloader @@ -118,11 +115,9 @@ open class SerializerFactory( // can be useful to enable but will be *extremely* chatty if you do logger.trace { "Get Serializer for $actualClass ${declaredType.typeName}" } - val declaredClass = declaredType.asClass() ?: throw AMQPNotSerializableException( - declaredType, - "Declared types of $declaredType are not supported.") - - val actualType: Type = inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType + val declaredClass = declaredType.asClass() + val actualType: Type = if (actualClass == null) declaredType + else inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType val serializer = when { // Declared class may not be set to Collection, but actual class could be a collection. @@ -166,78 +161,6 @@ open class SerializerFactory( return serializer } - /** - * Try and infer concrete types for any generics type variables for the actual class encountered, - * based on the declared type. - */ - // TODO: test GenericArrayType - private fun inferTypeVariables(actualClass: Class<*>?, declaredClass: Class<*>, - declaredType: Type): Type? = when (declaredType) { - is ParameterizedType -> inferTypeVariables(actualClass, declaredClass, declaredType) - // Nothing to infer, otherwise we'd have ParameterizedType - is Class<*> -> actualClass - is GenericArrayType -> { - val declaredComponent = declaredType.genericComponentType - inferTypeVariables(actualClass?.componentType, declaredComponent.asClass()!!, declaredComponent)?.asArray() - } - is TypeVariable<*> -> actualClass - is WildcardType -> actualClass - else -> null - } - - /** - * Try and infer concrete types for any generics type variables for the actual class encountered, based on the declared - * type, which must be a [ParameterizedType]. - */ - private fun inferTypeVariables(actualClass: Class<*>?, declaredClass: Class<*>, declaredType: ParameterizedType): Type? { - if (actualClass == null || declaredClass == actualClass) { - return null - } else if (declaredClass.isAssignableFrom(actualClass)) { - return if (actualClass.typeParameters.isNotEmpty()) { - // The actual class can never have type variables resolved, due to the JVM's use of type erasure, so let's try and resolve them - // Search for declared type in the inheritance hierarchy and then see if that fills in all the variables - val implementationChain: List<Type>? = findPathToDeclared(actualClass, declaredType, mutableListOf()) - if (implementationChain != null) { - val start = implementationChain.last() - val rest = implementationChain.dropLast(1).drop(1) - val resolver = rest.reversed().fold(TypeResolver().where(start, declaredType)) { resolved, chainEntry -> - val newResolved = resolved.resolveType(chainEntry) - TypeResolver().where(chainEntry, newResolved) - } - // The end type is a special case as it is a Class, so we need to fake up a ParameterizedType for it to get the TypeResolver to do anything. - val endType = DeserializedParameterizedType(actualClass, actualClass.typeParameters) - val resolvedType = resolver.resolveType(endType) - resolvedType - } else throw AMQPNotSerializableException(declaredType, - "No inheritance path between actual $actualClass and declared $declaredType.") - } else actualClass - } else throw AMQPNotSerializableException( - declaredType, - "Found object of type $actualClass in a property expecting $declaredType") - } - - // Stop when reach declared type or return null if we don't find it. - private fun findPathToDeclared(startingType: Type, declaredType: Type, chain: MutableList<Type>): List<Type>? { - chain.add(startingType) - val startingClass = startingType.asClass() - if (startingClass == declaredType.asClass()) { - // We're done... - return chain - } - // Now explore potential options of superclass and all interfaces - val superClass = startingClass?.genericSuperclass - val superClassChain = if (superClass != null) { - val resolved = TypeResolver().where(startingClass.asParameterizedType(), startingType.asParameterizedType()).resolveType(superClass) - findPathToDeclared(resolved, declaredType, ArrayList(chain)) - } else null - if (superClassChain != null) return superClassChain - for (iface in startingClass?.genericInterfaces ?: emptyArray()) { - val resolved = TypeResolver().where(startingClass!!.asParameterizedType(), startingType.asParameterizedType()).resolveType(iface) - return findPathToDeclared(resolved, declaredType, ArrayList(chain)) ?: continue - } - return null - } - /** * Lookup and manufacture a serializer for the given AMQP type descriptor, assuming we also have the necessary types * contained in the [Schema]. @@ -349,7 +272,7 @@ open class SerializerFactory( // TODO: class loader logic, and compare the schema. val type = typeForName(typeNotation.name, classloader) return get( - type.asClass() ?: throw AMQPNotSerializableException(type, "Unable to build composite type for $type"), + type.asClass(), type) } @@ -402,7 +325,7 @@ open class SerializerFactory( // super type. Could be done, but do we need it? for (customSerializer in customSerializers) { if (customSerializer.isSerializerFor(clazz)) { - val declaredSuperClass = declaredType.asClass()?.superclass + val declaredSuperClass = declaredType.asClass().superclass return if (declaredSuperClass == null diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TypeParameterUtils.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TypeParameterUtils.kt new file mode 100644 index 0000000000..72720add79 --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TypeParameterUtils.kt @@ -0,0 +1,94 @@ +package net.corda.serialization.internal.amqp + +import com.google.common.reflect.TypeResolver +import java.lang.reflect.* + +/** + * Try and infer concrete types for any generics type variables for the actual class encountered, + * based on the declared type. + */ +// TODO: test GenericArrayType +fun inferTypeVariables(actualClass: Class<*>, + declaredClass: Class<*>, + declaredType: Type): Type? = when (declaredType) { + is ParameterizedType -> inferTypeVariables(actualClass, declaredClass, declaredType) + is GenericArrayType -> { + val declaredComponent = declaredType.genericComponentType + inferTypeVariables(actualClass.componentType, declaredComponent.asClass(), declaredComponent)?.asArray() + } + // Nothing to infer, otherwise we'd have ParameterizedType + is Class<*> -> actualClass + is TypeVariable<*> -> actualClass + is WildcardType -> actualClass + else -> throw UnsupportedOperationException("Cannot infer type variables for type $declaredType") +} + +/** + * Try and infer concrete types for any generics type variables for the actual class encountered, based on the declared + * type, which must be a [ParameterizedType]. + */ +private fun inferTypeVariables(actualClass: Class<*>, declaredClass: Class<*>, declaredType: ParameterizedType): Type? { + if (declaredClass == actualClass) { + return null + } + + if (!declaredClass.isAssignableFrom(actualClass)) { + throw AMQPNotSerializableException( + declaredType, + "Found object of type $actualClass in a property expecting $declaredType") + } + + if (actualClass.typeParameters.isEmpty()) { + return actualClass + } + // The actual class can never have type variables resolved, due to the JVM's use of type erasure, so let's try and resolve them + // Search for declared type in the inheritance hierarchy and then see if that fills in all the variables + val implementationChain: List<Type> = findPathToDeclared(actualClass, declaredType)?.toList() + ?: throw AMQPNotSerializableException( + declaredType, + "No inheritance path between actual $actualClass and declared $declaredType.") + + val start = implementationChain.last() + val rest = implementationChain.dropLast(1).drop(1) + val resolver = rest.reversed().fold(TypeResolver().where(start, declaredType)) { resolved, chainEntry -> + val newResolved = resolved.resolveType(chainEntry) + TypeResolver().where(chainEntry, newResolved) + } + // The end type is a special case as it is a Class, so we need to fake up a ParameterizedType for it to get the TypeResolver to do anything. + val endType = DeserializedParameterizedType(actualClass, actualClass.typeParameters) + return resolver.resolveType(endType) +} + +// Stop when reach declared type or return null if we don't find it. +private fun findPathToDeclared(startingType: Type, declaredType: Type, chain: Sequence<Type> = emptySequence()): Sequence<Type>? { + val extendedChain = chain + startingType + val startingClass = startingType.asClass() + + if (startingClass == declaredType.asClass()) { + // We're done... + return extendedChain + } + + val resolver = { type: Type -> + TypeResolver().where( + startingClass.asParameterizedType(), + startingType.asParameterizedType()) + .resolveType(type) + } + + // Now explore potential options of superclass and all interfaces + return findPathViaGenericSuperclass(startingClass, resolver, declaredType, extendedChain) + ?: findPathViaInterfaces(startingClass, resolver, declaredType, extendedChain) +} + +private fun findPathViaInterfaces(startingClass: Class<*>, resolver: (Type) -> Type, declaredType: Type, extendedChain: Sequence<Type>): Sequence<Type>? = + startingClass.genericInterfaces.asSequence().map { + findPathToDeclared(resolver(it), declaredType, extendedChain) + }.filterNotNull().firstOrNull() + + +private fun findPathViaGenericSuperclass(startingClass: Class<*>, resolver: (Type) -> Type, declaredType: Type, extendedChain: Sequence<Type>): Sequence<Type>? { + val superClass = startingClass.genericSuperclass ?: return null + return findPathToDeclared(resolver(superClass), declaredType, extendedChain) +} + diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PrivateKeySerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PrivateKeySerializer.kt index e87c679570..7bf9bbf344 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PrivateKeySerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PrivateKeySerializer.kt @@ -2,7 +2,6 @@ package net.corda.serialization.internal.amqp.custom import net.corda.core.crypto.Crypto import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationContext.UseCase.Checkpoint import net.corda.core.serialization.SerializationContext.UseCase.Storage import net.corda.serialization.internal.amqp.* import net.corda.serialization.internal.checkUseCase @@ -13,14 +12,12 @@ import java.util.* object PrivateKeySerializer : CustomSerializer.Implements<PrivateKey>(PrivateKey::class.java) { - private val allowedUseCases = EnumSet.of(Storage, Checkpoint) - override val schemaForDocumentation = Schema(listOf(RestrictedType(type.toString(), "", listOf(type.toString()), SerializerFactory.primitiveTypeName(ByteArray::class.java)!!, descriptor, emptyList()))) override fun writeDescribedObject(obj: PrivateKey, data: Data, type: Type, output: SerializationOutput, context: SerializationContext ) { - checkUseCase(allowedUseCases) + checkUseCase(Storage) output.writeObject(obj.encoded, data, clazz, context) } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt index 135e93710c..3b4b03800e 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt @@ -25,7 +25,7 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<T // Try and find a constructor try { val constructor = constructorForDeserialization(obj.javaClass) - propertiesForSerializationFromConstructor(constructor!!, obj.javaClass, factory).forEach { property -> + propertiesForSerializationFromConstructor(constructor, obj.javaClass, factory).forEach { property -> extraProperties[property.serializer.name] = property.serializer.propertyReader.read(obj) } } catch (e: NotSerializableException) { @@ -52,7 +52,7 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<T // If it is CordaException or CordaRuntimeException, we can seek any constructor and then set the properties // Otherwise we just make a CordaRuntimeException if (CordaThrowable::class.java.isAssignableFrom(clazz) && Throwable::class.java.isAssignableFrom(clazz)) { - val constructor = constructorForDeserialization(clazz)!! + val constructor = constructorForDeserialization(clazz) val throwable = constructor.callBy(constructor.parameters.map { it to proxy.additionalProperties[it.name] }.toMap()) (throwable as CordaThrowable).apply { if (this.javaClass.name != proxy.exceptionClass) this.originalExceptionClassName = proxy.exceptionClass diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt index 315ad35a50..f1a422f22e 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt @@ -130,8 +130,8 @@ class ClassCarpenterImpl @JvmOverloads constructor (override val whitelist: Clas */ override fun build(schema: Schema): Class<*> { validateSchema(schema) - // Walk up the inheritance hierarchy and then start walking back down once we either hit the top, or - // find a class we haven't generated yet. + // Walk up the inheritance hierarchy until we hit either the top or a class we've already generated, + // then walk back down it generating classes. val hierarchy = ArrayList<Schema>() hierarchy += schema var cursor = schema.superclass @@ -306,16 +306,16 @@ class ClassCarpenterImpl @JvmOverloads constructor (override val whitelist: Clas visitInsn(DUP) var idx = 0 - schema.fields.forEach { + schema.fields.keys.forEach { key -> visitInsn(DUP) visitIntInsn(BIPUSH, idx) visitTypeInsn(NEW, schema.jvmName) visitInsn(DUP) - visitLdcInsn(it.key) + visitLdcInsn(key) visitIntInsn(BIPUSH, idx++) visitMethodInsn(INVOKESPECIAL, schema.jvmName, "<init>", "(L$jlString;I)V", false) visitInsn(DUP) - visitFieldInsn(PUTSTATIC, schema.jvmName, it.key, "L${schema.jvmName};") + visitFieldInsn(PUTSTATIC, schema.jvmName, key, "L${schema.jvmName};") visitInsn(AASTORE) } @@ -381,20 +381,18 @@ class ClassCarpenterImpl @JvmOverloads constructor (override val whitelist: Clas visitCode() // Calculate the super call. - val superclassFields = schema.superclass?.fieldsIncludingSuperclasses() ?: emptyMap() visitVarInsn(ALOAD, 0) val sc = schema.superclass + var slot = 1 if (sc == null) { visitMethodInsn(INVOKESPECIAL, jlObject, "<init>", "()V", false) } else { - var slot = 1 - superclassFields.values.forEach { slot += load(slot, it) } + slot = sc.fieldsIncludingSuperclasses().values.fold(slot) { acc, field -> acc + load(acc, field) } val superDesc = sc.descriptorsIncludingSuperclasses().values.joinToString("") visitMethodInsn(INVOKESPECIAL, sc.jvmName, "<init>", "($superDesc)V", false) } // Assign the fields from parameters. - var slot = 1 + superclassFields.size for ((name, field) in schema.fields) { (field as ClassField).nullTest(this, slot) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/SchemaFields.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/SchemaFields.kt index c9babf9348..ba40dc20f8 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/SchemaFields.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/SchemaFields.kt @@ -1,9 +1,9 @@ package net.corda.serialization.internal.carpenter -import jdk.internal.org.objectweb.asm.Opcodes.* import net.corda.core.DeleteForDJVM import org.objectweb.asm.ClassWriter import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes.* import org.objectweb.asm.Type abstract class Field(val field: Class<out Any?>) { diff --git a/serialization/src/test/java/net/corda/serialization/internal/ForbiddenLambdaSerializationTests.java b/serialization/src/test/java/net/corda/serialization/internal/ForbiddenLambdaSerializationTests.java index 130870d544..03b0117c07 100644 --- a/serialization/src/test/java/net/corda/serialization/internal/ForbiddenLambdaSerializationTests.java +++ b/serialization/src/test/java/net/corda/serialization/internal/ForbiddenLambdaSerializationTests.java @@ -4,7 +4,6 @@ import com.google.common.collect.Maps; import net.corda.core.serialization.SerializationContext; import net.corda.core.serialization.SerializationFactory; import net.corda.core.serialization.SerializedBytes; -import net.corda.serialization.internal.amqp.AMQPNotSerializableException; import net.corda.serialization.internal.amqp.SchemaKt; import net.corda.testing.core.SerializationEnvironmentRule; import org.junit.Before; @@ -20,8 +19,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.ThrowableAssert.catchThrowable; public final class ForbiddenLambdaSerializationTests { + private EnumSet<SerializationContext.UseCase> contexts = EnumSet.complementOf( - EnumSet.of(SerializationContext.UseCase.Checkpoint, SerializationContext.UseCase.Testing)); + EnumSet.of(SerializationContext.UseCase.Testing)); + @Rule public final SerializationEnvironmentRule testSerialization = new SerializationEnvironmentRule(); private SerializationFactory factory; diff --git a/serialization/src/test/java/net/corda/serialization/internal/LambdaCheckpointSerializationTest.java b/serialization/src/test/java/net/corda/serialization/internal/LambdaCheckpointSerializationTest.java index 1cae8762bb..feab89ad92 100644 --- a/serialization/src/test/java/net/corda/serialization/internal/LambdaCheckpointSerializationTest.java +++ b/serialization/src/test/java/net/corda/serialization/internal/LambdaCheckpointSerializationTest.java @@ -1,11 +1,11 @@ package net.corda.serialization.internal; -import net.corda.core.serialization.SerializationContext; -import net.corda.core.serialization.SerializationFactory; -import net.corda.core.serialization.SerializedBytes; +import net.corda.core.serialization.*; +import net.corda.core.serialization.internal.CheckpointSerializationContext; +import net.corda.core.serialization.internal.CheckpointSerializationFactory; import net.corda.node.serialization.kryo.CordaClosureSerializer; -import net.corda.node.serialization.kryo.KryoSerializationSchemeKt; import net.corda.testing.core.SerializationEnvironmentRule; +import net.corda.testing.core.internal.CheckpointSerializationEnvironmentRule; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -18,21 +18,22 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.ThrowableAssert.catchThrowable; public final class LambdaCheckpointSerializationTest { + @Rule - public final SerializationEnvironmentRule testSerialization = new SerializationEnvironmentRule(); - private SerializationFactory factory; - private SerializationContext context; + public final CheckpointSerializationEnvironmentRule testCheckpointSerialization = + new CheckpointSerializationEnvironmentRule(); + + private CheckpointSerializationFactory factory; + private CheckpointSerializationContext context; @Before public void setup() { - factory = testSerialization.getSerializationFactory(); - context = new SerializationContextImpl( - KryoSerializationSchemeKt.getKryoMagic(), + factory = testCheckpointSerialization.getCheckpointSerializationFactory(); + context = new CheckpointSerializationContextImpl( getClass().getClassLoader(), AllWhitelist.INSTANCE, Collections.emptyMap(), true, - SerializationContext.UseCase.Checkpoint, null ); } diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/ContractAttachmentSerializerTest.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/ContractAttachmentSerializerTest.kt index a4d17cc52e..73b799217d 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/ContractAttachmentSerializerTest.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/ContractAttachmentSerializerTest.kt @@ -3,8 +3,13 @@ package net.corda.serialization.internal import net.corda.core.contracts.ContractAttachment import net.corda.core.identity.CordaX500Name import net.corda.core.serialization.* +import net.corda.core.serialization.internal.CheckpointSerializationContext +import net.corda.core.serialization.internal.CheckpointSerializationFactory +import net.corda.core.serialization.internal.checkpointDeserialize +import net.corda.core.serialization.internal.checkpointSerialize import net.corda.testing.contracts.DummyContract import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.core.internal.CheckpointSerializationEnvironmentRule import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices import org.apache.commons.lang.ArrayUtils.EMPTY_BYTE_ARRAY @@ -17,28 +22,29 @@ import org.junit.Test import kotlin.test.assertEquals class ContractAttachmentSerializerTest { + @Rule @JvmField - val testSerialization = SerializationEnvironmentRule() + val testCheckpointSerialization = CheckpointSerializationEnvironmentRule() - private lateinit var factory: SerializationFactory - private lateinit var context: SerializationContext - private lateinit var contextWithToken: SerializationContext + private lateinit var factory: CheckpointSerializationFactory + private lateinit var context: CheckpointSerializationContext + private lateinit var contextWithToken: CheckpointSerializationContext private val mockServices = MockServices(emptyList(), CordaX500Name("MegaCorp", "London", "GB"), rigorousMock()) @Before fun setup() { - factory = testSerialization.serializationFactory - context = testSerialization.checkpointContext - contextWithToken = context.withTokenContext(SerializeAsTokenContextImpl(Any(), factory, context, mockServices)) + factory = testCheckpointSerialization.checkpointSerializationFactory + context = testCheckpointSerialization.checkpointSerializationContext + contextWithToken = context.withTokenContext(CheckpointSerializeAsTokenContextImpl(Any(), factory, context, mockServices)) } @Test fun `write contract attachment and read it back`() { val contractAttachment = ContractAttachment(GeneratedAttachment(EMPTY_BYTE_ARRAY), DummyContract.PROGRAM_ID) // no token context so will serialize the whole attachment - val serialized = contractAttachment.serialize(factory, context) - val deserialized = serialized.deserialize(factory, context) + val serialized = contractAttachment.checkpointSerialize(factory, context) + val deserialized = serialized.checkpointDeserialize(factory, context) assertEquals(contractAttachment.id, deserialized.attachment.id) assertEquals(contractAttachment.contract, deserialized.contract) @@ -53,8 +59,8 @@ class ContractAttachmentSerializerTest { mockServices.attachments.importAttachment(attachment.open(), "test", null) val contractAttachment = ContractAttachment(attachment, DummyContract.PROGRAM_ID) - val serialized = contractAttachment.serialize(factory, contextWithToken) - val deserialized = serialized.deserialize(factory, contextWithToken) + val serialized = contractAttachment.checkpointSerialize(factory, contextWithToken) + val deserialized = serialized.checkpointDeserialize(factory, contextWithToken) assertEquals(contractAttachment.id, deserialized.attachment.id) assertEquals(contractAttachment.contract, deserialized.contract) @@ -70,7 +76,7 @@ class ContractAttachmentSerializerTest { mockServices.attachments.importAttachment(attachment.open(), "test", null) val contractAttachment = ContractAttachment(attachment, DummyContract.PROGRAM_ID) - val serialized = contractAttachment.serialize(factory, contextWithToken) + val serialized = contractAttachment.checkpointSerialize(factory, contextWithToken) assertThat(serialized.size).isLessThan(largeAttachmentSize) } @@ -82,8 +88,8 @@ class ContractAttachmentSerializerTest { // don't importAttachment in mockService val contractAttachment = ContractAttachment(attachment, DummyContract.PROGRAM_ID) - val serialized = contractAttachment.serialize(factory, contextWithToken) - val deserialized = serialized.deserialize(factory, contextWithToken) + val serialized = contractAttachment.checkpointSerialize(factory, contextWithToken) + val deserialized = serialized.checkpointDeserialize(factory, contextWithToken) assertThatThrownBy { deserialized.attachment.open() }.isInstanceOf(MissingAttachmentsException::class.java) } @@ -94,8 +100,8 @@ class ContractAttachmentSerializerTest { // don't importAttachment in mockService val contractAttachment = ContractAttachment(attachment, DummyContract.PROGRAM_ID) - val serialized = contractAttachment.serialize(factory, contextWithToken) - serialized.deserialize(factory, contextWithToken) + val serialized = contractAttachment.checkpointSerialize(factory, contextWithToken) + serialized.checkpointDeserialize(factory, contextWithToken) // MissingAttachmentsException thrown if we try to open attachment } diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/CordaClassResolverTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/CordaClassResolverTests.kt index f78752577a..860a04a81c 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/CordaClassResolverTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/CordaClassResolverTests.kt @@ -11,12 +11,11 @@ import com.nhaarman.mockito_kotlin.verify import com.nhaarman.mockito_kotlin.whenever import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER import net.corda.core.node.services.AttachmentStorage +import net.corda.core.serialization.internal.CheckpointSerializationContext import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.SerializationContext import net.corda.node.serialization.kryo.CordaClassResolver import net.corda.node.serialization.kryo.CordaKryo -import net.corda.node.serialization.kryo.kryoMagic import net.corda.testing.internal.rigorousMock import net.corda.testing.services.MockAttachmentStorage import org.junit.Rule @@ -115,8 +114,8 @@ class CordaClassResolverTests { val emptyMapClass = mapOf<Any, Any>().javaClass } - private val emptyWhitelistContext: SerializationContext = SerializationContextImpl(kryoMagic, this.javaClass.classLoader, EmptyWhitelist, emptyMap(), true, SerializationContext.UseCase.P2P, null) - private val allButBlacklistedContext: SerializationContext = SerializationContextImpl(kryoMagic, this.javaClass.classLoader, AllButBlacklisted, emptyMap(), true, SerializationContext.UseCase.P2P, null) + private val emptyWhitelistContext: CheckpointSerializationContext = CheckpointSerializationContextImpl(this.javaClass.classLoader, EmptyWhitelist, emptyMap(), true, null) + private val allButBlacklistedContext: CheckpointSerializationContext = CheckpointSerializationContextImpl(this.javaClass.classLoader, AllButBlacklisted, emptyMap(), true, null) @Test fun `Annotation on enum works for specialised entries`() { CordaClassResolver(emptyWhitelistContext).getRegistration(Foo.Bar::class.java) diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/PrivateKeySerializationTest.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/PrivateKeySerializationTest.kt index 3b1d46f342..bdd5b672ef 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/PrivateKeySerializationTest.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/PrivateKeySerializationTest.kt @@ -3,6 +3,8 @@ package net.corda.serialization.internal import net.corda.core.crypto.Crypto import net.corda.core.serialization.SerializationContext.UseCase.* import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.internal.CheckpointSerializationDefaults +import net.corda.core.serialization.internal.checkpointSerialize import net.corda.core.serialization.serialize import net.corda.testing.core.SerializationEnvironmentRule import org.assertj.core.api.Assertions.assertThatThrownBy @@ -33,13 +35,13 @@ class PrivateKeySerializationTest(private val privateKey: PrivateKey, private va @Test fun `passed with expected UseCases`() { assertTrue { privateKey.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes.isNotEmpty() } - assertTrue { privateKey.serialize(context = SerializationDefaults.CHECKPOINT_CONTEXT).bytes.isNotEmpty() } + assertTrue { privateKey.checkpointSerialize(context = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT).bytes.isNotEmpty() } } @Test fun `failed with wrong UseCase`() { assertThatThrownBy { privateKey.serialize(context = SerializationDefaults.P2P_CONTEXT) } .isInstanceOf(IllegalStateException::class.java) - .hasMessageContaining("UseCase '$P2P' is not within") + .hasMessageContaining("UseCase '$P2P' is not 'Storage") } } \ No newline at end of file diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/SerializationTokenTest.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/SerializationTokenTest.kt index b18e1d725b..7f2bad6854 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/SerializationTokenTest.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/SerializationTokenTest.kt @@ -4,6 +4,10 @@ import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.KryoException import com.esotericsoftware.kryo.io.Output import net.corda.core.serialization.* +import net.corda.core.serialization.internal.CheckpointSerializationContext +import net.corda.core.serialization.internal.CheckpointSerializationFactory +import net.corda.core.serialization.internal.checkpointDeserialize +import net.corda.core.serialization.internal.checkpointSerialize import net.corda.core.utilities.OpaqueBytes import net.corda.node.serialization.kryo.CordaClassResolver import net.corda.node.serialization.kryo.CordaKryo @@ -11,6 +15,7 @@ import net.corda.node.serialization.kryo.DefaultKryoCustomizer import net.corda.node.serialization.kryo.kryoMagic import net.corda.testing.internal.rigorousMock import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.core.internal.CheckpointSerializationEnvironmentRule import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Rule @@ -18,16 +23,18 @@ import org.junit.Test import java.io.ByteArrayOutputStream class SerializationTokenTest { + @Rule @JvmField - val testSerialization = SerializationEnvironmentRule() - private lateinit var factory: SerializationFactory - private lateinit var context: SerializationContext + val testCheckpointSerialization = CheckpointSerializationEnvironmentRule() + + private lateinit var factory: CheckpointSerializationFactory + private lateinit var context: CheckpointSerializationContext @Before fun setup() { - factory = testSerialization.serializationFactory - context = testSerialization.checkpointContext.withWhitelisted(SingletonSerializationToken::class.java) + factory = testCheckpointSerialization.checkpointSerializationFactory + context = testCheckpointSerialization.checkpointSerializationContext.withWhitelisted(SingletonSerializationToken::class.java) } // Large tokenizable object so we can tell from the smaller number of serialized bytes it was actually tokenized @@ -42,16 +49,16 @@ class SerializationTokenTest { override fun equals(other: Any?) = other is LargeTokenizable && other.bytes.size == this.bytes.size } - private fun serializeAsTokenContext(toBeTokenized: Any) = SerializeAsTokenContextImpl(toBeTokenized, factory, context, rigorousMock()) + private fun serializeAsTokenContext(toBeTokenized: Any) = CheckpointSerializeAsTokenContextImpl(toBeTokenized, factory, context, rigorousMock()) @Test fun `write token and read tokenizable`() { val tokenizableBefore = LargeTokenizable() val context = serializeAsTokenContext(tokenizableBefore) val testContext = this.context.withTokenContext(context) - val serializedBytes = tokenizableBefore.serialize(factory, testContext) + val serializedBytes = tokenizableBefore.checkpointSerialize(factory, testContext) assertThat(serializedBytes.size).isLessThan(tokenizableBefore.numBytes) - val tokenizableAfter = serializedBytes.deserialize(factory, testContext) + val tokenizableAfter = serializedBytes.checkpointDeserialize(factory, testContext) assertThat(tokenizableAfter).isSameAs(tokenizableBefore) } @@ -62,8 +69,8 @@ class SerializationTokenTest { val tokenizableBefore = UnitSerializeAsToken() val context = serializeAsTokenContext(tokenizableBefore) val testContext = this.context.withTokenContext(context) - val serializedBytes = tokenizableBefore.serialize(factory, testContext) - val tokenizableAfter = serializedBytes.deserialize(factory, testContext) + val serializedBytes = tokenizableBefore.checkpointSerialize(factory, testContext) + val tokenizableAfter = serializedBytes.checkpointDeserialize(factory, testContext) assertThat(tokenizableAfter).isSameAs(tokenizableBefore) } @@ -72,7 +79,7 @@ class SerializationTokenTest { val tokenizableBefore = UnitSerializeAsToken() val context = serializeAsTokenContext(emptyList<Any>()) val testContext = this.context.withTokenContext(context) - tokenizableBefore.serialize(factory, testContext) + tokenizableBefore.checkpointSerialize(factory, testContext) } @Test(expected = UnsupportedOperationException::class) @@ -80,14 +87,14 @@ class SerializationTokenTest { val tokenizableBefore = UnitSerializeAsToken() val context = serializeAsTokenContext(emptyList<Any>()) val testContext = this.context.withTokenContext(context) - val serializedBytes = tokenizableBefore.toToken(serializeAsTokenContext(emptyList<Any>())).serialize(factory, testContext) - serializedBytes.deserialize(factory, testContext) + val serializedBytes = tokenizableBefore.toToken(serializeAsTokenContext(emptyList<Any>())).checkpointSerialize(factory, testContext) + serializedBytes.checkpointDeserialize(factory, testContext) } @Test(expected = KryoException::class) fun `no context set`() { val tokenizableBefore = UnitSerializeAsToken() - tokenizableBefore.serialize(factory, context) + tokenizableBefore.checkpointSerialize(factory, context) } @Test(expected = KryoException::class) @@ -105,7 +112,7 @@ class SerializationTokenTest { kryo.writeObject(it, emptyList<Any>()) } val serializedBytes = SerializedBytes<Any>(stream.toByteArray()) - serializedBytes.deserialize(factory, testContext) + serializedBytes.checkpointDeserialize(factory, testContext) } private class WrongTypeSerializeAsToken : SerializeAsToken { @@ -121,7 +128,7 @@ class SerializationTokenTest { val tokenizableBefore = WrongTypeSerializeAsToken() val context = serializeAsTokenContext(tokenizableBefore) val testContext = this.context.withTokenContext(context) - val serializedBytes = tokenizableBefore.serialize(factory, testContext) - serializedBytes.deserialize(factory, testContext) + val serializedBytes = tokenizableBefore.checkpointSerialize(factory, testContext) + serializedBytes.checkpointDeserialize(factory, testContext) } } diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolvabilityTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolvabilityTests.kt index 5e4b7e343f..e0109515d6 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolvabilityTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolvabilityTests.kt @@ -544,11 +544,11 @@ class EvolvabilityTests { } // - // This test uses a NetworkParameters signed set of bytes generated by R3 Corda and + // This test uses a NetworkParameters signed set of bytes generated by Corda Enterprise and // is here to ensure we can still read them. This test exists because of the break in // being able to deserialize an object serialized prior to some fixes to the fingerprinter. // - // The file itself was generated from R3 Corda at commit + // The file itself was generated from Corda Enterprise at commit // 6a6b6f256 Skip cache invalidation during init() - caches are still null. // // To regenerate the file un-ignore the test below this one (regenerate broken network parameters), @@ -565,7 +565,7 @@ class EvolvabilityTests { // // filename breakdown // networkParams - because this is a serialised set of network parameters - // r3corda - generated by R3 Corda instead of Corda + // r3corda - generated by Corda Enterprise instead of Corda // 6a6b6f256 - Commit sha of the build that generated the file we're testing against // val resource = "networkParams.r3corda.6a6b6f256" diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/FingerPrinterTesting.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/FingerPrinterTesting.kt index cc180e52c2..023f291394 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/FingerPrinterTesting.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/FingerPrinterTesting.kt @@ -15,10 +15,6 @@ class FingerPrinterTesting : FingerPrinter { return cache.computeIfAbsent(type) { index++.toString() } } - override fun setOwner(factory: SerializerFactory) { - return - } - @Suppress("UNUSED") fun changeFingerprint(type: Type) { cache.computeIfAbsent(type) { "" }.apply { index++.toString() } @@ -47,7 +43,7 @@ class FingerPrinterTestingTests { AllWhitelist, ClassLoader.getSystemClassLoader(), evolutionSerializerGetter = EvolutionSerializerGetterTesting(), - fingerPrinter = FingerPrinterTesting()) + fingerPrinterConstructor = { _ -> FingerPrinterTesting() }) val blob = TestSerializationOutput(VERBOSE, factory).serializeAndReturnSchema(C(1, 2L)) diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTest.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTest.kt index e74206e5b0..ac510642d7 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTest.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTest.kt @@ -129,6 +129,27 @@ class ClassCarpenterTest { assertEquals("B{a=xa, b=xb}", i.toString()) } + /** + * Tests the fix for [Corda-1945](https://r3-cev.atlassian.net/secure/RapidBoard.jspa?rapidView=83&modal=detail&selectedIssue=CORDA-1945) + */ + @Test + fun `superclasses with double-size primitive constructor parameters`() { + val schema1 = ClassSchema( + "gen.A", + mapOf("a" to NonNullableField(Long::class.javaPrimitiveType!!))) + + val schema2 = ClassSchema( + "gen.B", + mapOf("b" to NonNullableField(String::class.java)), + schema1) + + val clazz = cc.build(schema2) + val i = clazz.constructors[0].newInstance(1L, "xb") as SimpleFieldAccess + assertEquals(1L, i["a"]) + assertEquals("xb", i["b"]) + assertEquals("B{a=1, b=xb}", i.toString()) + } + @Test fun interfaces() { val schema1 = ClassSchema( diff --git a/settings.gradle b/settings.gradle index af4fa37e68..026a092f27 100644 --- a/settings.gradle +++ b/settings.gradle @@ -60,10 +60,12 @@ include 'samples:cordapp-configuration' include 'samples:network-verifier' include 'serialization' +apply from: 'buildCacheSettings.gradle' + if (JavaVersion.current() == JavaVersion.VERSION_1_8) { include 'core-deterministic' include 'core-deterministic:testing' - include 'core-deterministic:testing:common' include 'core-deterministic:testing:data' + include 'core-deterministic:testing:verifier' include 'serialization-deterministic' } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index 6e085b6c70..be91c42c1f 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -18,6 +18,7 @@ import net.corda.core.node.services.* import net.corda.core.serialization.SerializeAsToken import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort +import net.corda.node.VersionInfo import net.corda.node.cordapp.CordappLoader import net.corda.node.internal.ServicesForResolutionImpl import net.corda.node.internal.configureDatabase @@ -27,6 +28,7 @@ import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.services.vault.NodeVaultService +import net.corda.nodeapi.internal.PLATFORM_VERSION import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.HibernateConfiguration @@ -69,10 +71,10 @@ open class MockServices private constructor( companion object { - private fun cordappLoaderForPackages(packages: Iterable<String>): CordappLoader { + private fun cordappLoaderForPackages(packages: Iterable<String>, versionInfo: VersionInfo = VersionInfo.UNKNOWN): CordappLoader { val cordappPaths = TestCordappDirectories.forPackages(packages) - return JarScanningCordappLoader.fromDirectories(cordappPaths) + return JarScanningCordappLoader.fromDirectories(cordappPaths, versionInfo) } /** diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index 10f6392bd1..850cf8f8d5 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -5,7 +5,7 @@ import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigValueFactory -import net.corda.client.rpc.internal.createCordaRPCClientWithInternalSslAndClassLoader +import net.corda.client.rpc.internal.createCordaRPCClientWithSslAndClassLoader import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.firstOf import net.corda.core.identity.CordaX500Name @@ -23,10 +23,12 @@ import net.corda.node.NodeRegistrationOption import net.corda.node.VersionInfo import net.corda.node.internal.Node import net.corda.node.internal.NodeWithInfo +import net.corda.node.internal.clientSslOptionsCompatibleWith import net.corda.node.services.Permissions import net.corda.node.services.config.* import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NodeRegistrationHelper +import net.corda.nodeapi.internal.PLATFORM_VERSION import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.addShutdownHook @@ -45,14 +47,17 @@ import net.corda.testing.driver.internal.InProcessImpl import net.corda.testing.driver.internal.NodeHandleInternal import net.corda.testing.driver.internal.OutOfProcessImpl import net.corda.testing.internal.setGlobalSerialization +import net.corda.testing.internal.stubs.CertificateStoreStubs import net.corda.testing.node.ClusterSpec import net.corda.testing.node.NotarySpec import net.corda.testing.node.User import net.corda.testing.node.internal.DriverDSLImpl.Companion.cordappsInCurrentAndAdditionalPackages import okhttp3.OkHttpClient import okhttp3.Request +import rx.Observable import rx.Subscription import rx.schedulers.Schedulers +import rx.subjects.AsyncSubject import java.lang.management.ManagementFactory import java.net.ConnectException import java.net.URL @@ -160,7 +165,8 @@ class DriverDSLImpl( private fun establishRpc(config: NodeConfig, processDeathFuture: CordaFuture<out Process>): CordaFuture<CordaRPCOps> { val rpcAddress = config.corda.rpcOptions.address - val client = createCordaRPCClientWithInternalSslAndClassLoader(config.corda.rpcOptions.adminAddress, sslConfiguration = config.corda) + val clientRpcSslOptions = clientSslOptionsCompatibleWith(config.corda.rpcOptions) + val client = createCordaRPCClientWithSslAndClassLoader(rpcAddress, sslConfiguration = clientRpcSslOptions) val connectionFuture = poll(executorService, "RPC connection") { try { config.corda.rpcUsers[0].run { client.start(username, password) } @@ -195,7 +201,8 @@ class DriverDSLImpl( ): CordaFuture<NodeHandle> { val p2pAddress = portAllocation.nextHostAndPort() // TODO: Derive name from the full picked name, don't just wrap the common name - val name = providedName ?: CordaX500Name("${oneOf(names).organisation}-${p2pAddress.port}", "London", "GB") + val name = providedName + ?: CordaX500Name("${oneOf(names).organisation}-${p2pAddress.port}", "London", "GB") val registrationFuture = if (compatibilityZone?.rootCert != null) { // We don't need the network map to be available to be able to register the node @@ -268,7 +275,7 @@ class DriverDSLImpl( "devMode" to false) )).checkAndOverrideForInMemoryDB() - val versionInfo = VersionInfo(1, "1", "1", "1") + val versionInfo = VersionInfo(PLATFORM_VERSION, "1", "1", "1") config.corda.certificatesDirectory.createDirectories() // Create network root truststore. val rootTruststorePath = config.corda.certificatesDirectory / "network-root-truststore.jks" @@ -602,7 +609,8 @@ class DriverDSLImpl( } } } - val p2pReadyFuture = addressMustBeBoundFuture(executorService, config.corda.p2pAddress, process) + val effectiveP2PAddress = config.corda.messagingServerAddress ?: config.corda.p2pAddress + val p2pReadyFuture = addressMustBeBoundFuture(executorService, effectiveP2PAddress, process) return p2pReadyFuture.flatMap { val processDeathFuture = poll(executorService, "process death while waiting for RPC (${config.corda.myLegalName})") { if (process.isAlive) null else process @@ -612,7 +620,7 @@ class DriverDSLImpl( val networkMapFuture = executorService.fork { visibilityHandle.listen(rpc) }.flatMap { it } firstOf(processDeathFuture, networkMapFuture) { if (it == processDeathFuture) { - throw ListenProcessDeathException(config.corda.p2pAddress, process) + throw ListenProcessDeathException(effectiveP2PAddress, process) } // Will interrupt polling for process death as this is no longer relevant since the process been // successfully started and reflected itself in the NetworkMap. @@ -687,6 +695,7 @@ class DriverDSLImpl( executorService: ScheduledExecutorService, config: NodeConfig ): CordaFuture<Pair<NodeWithInfo, Thread>> { + val effectiveP2PAddress = config.corda.messagingServerAddress ?: config.corda.p2pAddress return executorService.fork { log.info("Starting in-process Node ${config.corda.myLegalName.organisation}") if (!(ManagementFactory.getRuntimeMXBean().inputArguments.any { it.contains("quasar") })) { @@ -703,7 +712,7 @@ class DriverDSLImpl( } nodeWithInfo to nodeThread }.flatMap { nodeAndThread -> - addressMustBeBoundFuture(executorService, config.corda.p2pAddress).map { nodeAndThread } + addressMustBeBoundFuture(executorService, effectiveP2PAddress).map { nodeAndThread } } } @@ -726,9 +735,11 @@ class DriverDSLImpl( val systemProperties = mutableMapOf( "name" to config.corda.myLegalName, - "visualvm.display.name" to "corda-${config.corda.myLegalName}", - "log4j2.debug" to if (debugPort != null) "true" else "false" + "visualvm.display.name" to "corda-${config.corda.myLegalName}" ) + debugPort?.let { + systemProperties += "log4j2.debug" to "true" + } systemProperties += inheritFromParentProcess() systemProperties += overriddenSystemProperties @@ -798,8 +809,13 @@ class DriverDSLImpl( config += "rpcUsers" to configuration.toConfig().getValue("rpcUsers") config += "useHTTPS" to useHTTPS config += "baseDirectory" to configuration.baseDirectory.toAbsolutePath().toString() - config += "keyStorePassword" to configuration.keyStorePassword - config += "trustStorePassword" to configuration.trustStorePassword + + config += "keyStorePath" to configuration.p2pSslOptions.keyStore.path.toString() + config += "keyStorePassword" to configuration.p2pSslOptions.keyStore.password + + config += "trustStorePath" to configuration.p2pSslOptions.trustStore.path.toString() + config += "trustStorePassword" to configuration.p2pSslOptions.trustStore.password + return config } @@ -868,7 +884,7 @@ private class NetworkVisibilityController { val (snapshot, updates) = rpc.networkMapFeed() visibleNodeCount = snapshot.size checkIfAllVisible() - subscription = updates.subscribe { + subscription = updates.subscribe({ when (it) { is NetworkMapCache.MapChange.Added -> { visibleNodeCount++ @@ -882,7 +898,9 @@ private class NetworkVisibilityController { // Nothing to do here but better being exhaustive. } } - } + }, { _ -> + // Nothing to do on errors here. + }) return future } @@ -1109,4 +1127,17 @@ private fun Config.toNodeOnly(): Config { } internal fun DriverParameters.cordappsForAllNodes(): Set<TestCorDapp> = cordappsForAllNodes - ?: cordappsInCurrentAndAdditionalPackages(extraCordappPackagesToScan) \ No newline at end of file + ?: cordappsInCurrentAndAdditionalPackages(extraCordappPackagesToScan) + +fun DriverDSL.startNode(providedName: CordaX500Name, devMode: Boolean, parameters: NodeParameters = NodeParameters()): CordaFuture<NodeHandle> { + var customOverrides = emptyMap<String, String>() + if (!devMode) { + val nodeDir = baseDirectory(providedName) + val certificatesDirectory = nodeDir / "certificates" + val signingCertStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) + val p2pSslConfig = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) + p2pSslConfig.configureDevKeyAndTrustStores(providedName, signingCertStore, certificatesDirectory) + customOverrides = mapOf("devMode" to "false") + } + return startNode(parameters, providedName = providedName, customOverrides = customOverrides) +} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index 96050e777f..cebb7f6d30 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -46,6 +46,7 @@ import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.transactions.BFTNonValidatingNotaryService import net.corda.node.services.transactions.BFTSMaRt import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor +import net.corda.node.utilities.DefaultNamedCacheFactory import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.network.NetworkParametersCopier @@ -55,6 +56,7 @@ import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.driver.TestCorDapp import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.setGlobalSerialization +import net.corda.testing.internal.stubs.CertificateStoreStubs import net.corda.testing.internal.testThreadFactory import net.corda.testing.node.* import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties @@ -72,7 +74,7 @@ import java.time.Clock import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger -val MOCK_VERSION_INFO = VersionInfo(1, "Mock release", "Mock revision", "Mock Vendor") +val MOCK_VERSION_INFO = VersionInfo(4, "Mock release", "Mock revision", "Mock Vendor") data class MockNodeArgs( val config: NodeConfiguration, @@ -209,15 +211,6 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe return defaultNotaryNode.info.legalIdentities.singleOrNull() ?: throw IllegalStateException("Default notary has multiple identities") } - /** - * Return the identity of the default notary node. - * @see defaultNotaryNode - */ - val defaultNotaryIdentityAndCert: PartyAndCertificate - get() { - return defaultNotaryNode.info.legalIdentitiesAndCerts.singleOrNull() ?: throw IllegalStateException("Default notary has multiple identities") - } - /** * Because this executor is shared, we need to be careful about nodes shutting it down. */ @@ -283,9 +276,10 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe } } - open class MockNode(args: MockNodeArgs, cordappLoader: CordappLoader = JarScanningCordappLoader.fromDirectories(args.config.cordappDirectories)) : AbstractNode<TestStartedNode>( + open class MockNode(args: MockNodeArgs, cordappLoader: CordappLoader = JarScanningCordappLoader.fromDirectories(args.config.cordappDirectories, args.version)) : AbstractNode<TestStartedNode>( args.config, TestClock(Clock.systemUTC()), + DefaultNamedCacheFactory(), args.version, cordappLoader, args.network.getServerThread(args.id), @@ -465,8 +459,11 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe private fun createNodeImpl(parameters: InternalMockNodeParameters, nodeFactory: (MockNodeArgs, CordappLoader?) -> MockNode, start: Boolean): MockNode { val id = parameters.forcedID ?: nextNodeId++ - val config = mockNodeConfiguration().also { - doReturn(baseDirectory(id).createDirectories()).whenever(it).baseDirectory + val baseDirectory = baseDirectory(id) + val certificatesDirectory = baseDirectory / "certificates" + certificatesDirectory.createDirectories() + val config = mockNodeConfiguration(certificatesDirectory).also { + doReturn(baseDirectory).whenever(it).baseDirectory doReturn(parameters.legalName ?: CordaX500Name("Mock Company $id", "London", "GB")).whenever(it).myLegalName doReturn(makeTestDataSourceProperties("node_${id}_net_$networkId")).whenever(it).dataSourceProperties doReturn(emptyList<SecureHash>()).whenever(it).extraNetworkMapKeys @@ -477,7 +474,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe val cordappDirectories = sharedCorDappsDirectories + TestCordappDirectories.cached(cordapps) doReturn(cordappDirectories).whenever(config).cordappDirectories - val node = nodeFactory(MockNodeArgs(config, this, id, parameters.entropyRoot, parameters.version), JarScanningCordappLoader.fromDirectories(cordappDirectories)) + val node = nodeFactory(MockNodeArgs(config, this, id, parameters.entropyRoot, parameters.version), JarScanningCordappLoader.fromDirectories(cordappDirectories, parameters.version)) _nodes += node if (start) { node.start() @@ -570,12 +567,17 @@ abstract class MessagingServiceSpy { abstract fun send(message: Message, target: MessageRecipients, sequenceKey: Any) } -private fun mockNodeConfiguration(): NodeConfiguration { +private fun mockNodeConfiguration(certificatesDirectory: Path): NodeConfiguration { @DoNotImplement abstract class AbstractNodeConfiguration : NodeConfiguration + + val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) + val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) + return rigorousMock<AbstractNodeConfiguration>().also { - doReturn("cordacadevpass").whenever(it).keyStorePassword - doReturn("trustpass").whenever(it).trustStorePassword + doReturn(certificatesDirectory.createDirectories()).whenever(it).certificatesDirectory + doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions + doReturn(signingCertificateStore).whenever(it).signingCertificateStore doReturn(emptyList<User>()).whenever(it).rpcUsers doReturn(null).whenever(it).notary doReturn(DatabaseConfig()).whenever(it).database diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt index 6c2094a2e7..1346e22276 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt @@ -1,5 +1,6 @@ package net.corda.testing.node.internal +import net.corda.client.rpc.ConnectionFailureException import net.corda.client.rpc.CordaRPCClient import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture @@ -8,17 +9,21 @@ import net.corda.core.flows.FlowLogic import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.times +import net.corda.core.messaging.CordaRPCOps import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.millis import net.corda.core.utilities.seconds import net.corda.node.services.api.StartedNodeServices import net.corda.node.services.messaging.Message +import net.corda.testing.driver.NodeHandle import net.corda.testing.internal.chooseIdentity import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.User import net.corda.testing.node.testContext import org.slf4j.LoggerFactory +import rx.Observable +import rx.subjects.AsyncSubject import java.net.Socket import java.net.SocketException import java.time.Duration @@ -108,4 +113,22 @@ fun StartedNodeServices.newContext(): InvocationContext = testContext(myInfo.cho fun InMemoryMessagingNetwork.MessageTransfer.getMessage(): Message = message -fun CordaRPCClient.start(user: User) = start(user.username, user.password) \ No newline at end of file +fun CordaRPCClient.start(user: User) = start(user.username, user.password) + +fun NodeHandle.waitForShutdown(): Observable<Unit> { + + return rpc.waitForShutdown().doAfterTerminate(::stop) +} + +fun CordaRPCOps.waitForShutdown(): Observable<Unit> { + + val completable = AsyncSubject.create<Unit>() + stateMachinesFeed().updates.subscribe({ _ -> }, { error -> + if (error is ConnectionFailureException) { + completable.onCompleted() + } else { + completable.onError(error) + } + }) + return completable +} \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt index bce128a107..b21df970db 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt @@ -10,9 +10,8 @@ import net.corda.core.node.NodeInfo import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.loggerFor import net.corda.node.VersionInfo -import net.corda.node.internal.NodeWithInfo import net.corda.node.internal.Node - +import net.corda.node.internal.NodeWithInfo import net.corda.node.services.config.* import net.corda.nodeapi.internal.config.toConfig import net.corda.nodeapi.internal.network.NetworkParametersCopier @@ -21,6 +20,7 @@ import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.driver.PortAllocation import net.corda.testing.internal.testThreadFactory import net.corda.testing.node.User +import org.apache.commons.lang.SystemUtils import org.apache.logging.log4j.Level import org.junit.After import org.junit.Before @@ -85,7 +85,7 @@ abstract class NodeBasedTest(private val cordappPackages: List<String> = emptyLi @JvmOverloads fun startNode(legalName: CordaX500Name, - platformVersion: Int = 1, + platformVersion: Int = 4, rpcUsers: List<User> = emptyList(), configOverrides: Map<String, Any> = emptyMap()): NodeWithInfo { val baseDirectory = baseDirectory(legalName).createDirectories() @@ -147,5 +147,10 @@ abstract class NodeBasedTest(private val cordappPackages: List<String> = emptyLi class InProcessNode(configuration: NodeConfiguration, versionInfo: VersionInfo) : Node(configuration, versionInfo, false) { + override fun start() : NodeInfo { + check(isValidJavaVersion()) { "You are using a version of Java that is not supported (${SystemUtils.JAVA_VERSION}). Please upgrade to the latest version of Java 8." } + return super.start() + } + override val rxIoScheduler get() = CachedThreadScheduler(testThreadFactory()).also { runOnStop += it::shutdown } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt index 1ce8494c99..05b5662a83 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt @@ -21,8 +21,8 @@ import net.corda.core.utilities.seconds import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.services.messaging.RPCServer import net.corda.node.services.messaging.RPCServerConfiguration -import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.RPCApi +import net.corda.nodeapi.internal.ArtemisTcpTransport import net.corda.serialization.internal.AMQP_RPC_CLIENT_CONTEXT import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.MAX_MESSAGE_SIZE diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/performance/Reporter.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/performance/Reporter.kt index a0a9e48a0f..cbd6cd5a00 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/performance/Reporter.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/performance/Reporter.kt @@ -12,12 +12,14 @@ fun startReporter(shutdownManager: ShutdownManager, metricRegistry: MetricRegist val jmxReporter = thread { JmxReporter.forRegistry(metricRegistry).inDomain("net.corda").createsObjectNamesWith { _, domain, name -> // Make the JMX hierarchy a bit better organised. - val category = name.substringBefore('.') + val category = name.substringBefore('.').substringBeforeLast('/') + val component = name.substringBefore('.').substringAfterLast('/', "") val subName = name.substringAfter('.', "") if (subName == "") - ObjectName("$domain:name=$category") + ObjectName("$domain:name=$category${if (component.isNotEmpty()) ",component=$component," else ""}") else - ObjectName("$domain:type=$category,name=$subName") + ObjectName("$domain:type=$category,${if (component.isNotEmpty()) "component=$component," else ""}name=$subName") + }.build().start() } shutdownManager.registerShutdown { jmxReporter.interrupt() } diff --git a/testing/test-cli/src/main/kotlin/net/corda/testing/CliBackwardsCompatibleTest.kt b/testing/test-cli/src/main/kotlin/net/corda/testing/CliBackwardsCompatibleTest.kt index 53b4b1b032..4390070dc9 100644 --- a/testing/test-cli/src/main/kotlin/net/corda/testing/CliBackwardsCompatibleTest.kt +++ b/testing/test-cli/src/main/kotlin/net/corda/testing/CliBackwardsCompatibleTest.kt @@ -1,16 +1,21 @@ package net.corda.testing import junit.framework.AssertionFailedError +import org.junit.Test -open class CliBackwardsCompatibleTest { +open class CliBackwardsCompatibleTest(val clazz: Class<*>) { + @Test + fun `should always be backwards compatible`() { + checkBackwardsCompatibility(clazz) + } fun checkBackwardsCompatibility(clazz: Class<*>) { val checker = CommandLineCompatibilityChecker() val checkResults = checker.checkCommandLineIsBackwardsCompatible(clazz) if (checkResults.isNotEmpty()) { - val exceptionMessage= checkResults.map { it.message }.joinToString(separator = "\n") + val exceptionMessage = checkResults.map { it.message }.joinToString(separator = "\n") throw AssertionFailedError("Command line is not backwards compatible:\n$exceptionMessage") } } diff --git a/testing/test-cli/src/main/kotlin/net/corda/testing/GenerateCommandLineCompat.kt b/testing/test-cli/src/main/kotlin/net/corda/testing/CommandLineCompatibilityUtils.kt similarity index 94% rename from testing/test-cli/src/main/kotlin/net/corda/testing/GenerateCommandLineCompat.kt rename to testing/test-cli/src/main/kotlin/net/corda/testing/CommandLineCompatibilityUtils.kt index e0141767c5..6ff161a885 100644 --- a/testing/test-cli/src/main/kotlin/net/corda/testing/GenerateCommandLineCompat.kt +++ b/testing/test-cli/src/main/kotlin/net/corda/testing/CommandLineCompatibilityUtils.kt @@ -11,6 +11,11 @@ import kotlin.collections.ArrayList class CommandLineCompatibilityChecker { + companion object { + fun printCommandLineYAML(clazz: Class<*>) { + CommandLineCompatibilityChecker().printCommandDescription(CommandLine(clazz.newInstance())) + } + } fun topoSort(commandLine: CommandLine): List<CommandDescription> { val toVisit = Stack<CommandLine>() @@ -62,6 +67,7 @@ class CommandLineCompatibilityChecker { return Iterable::class.java.isAssignableFrom(clazz) || Array<Any>::class.java.isAssignableFrom(clazz) } + fun printCommandDescription(commandLine: CommandLine) { val objectMapper = ObjectMapper(YAMLFactory()).registerKotlinModule() val results = topoSort(commandLine) @@ -109,7 +115,7 @@ class CommandLineCompatibilityChecker { throw IllegalArgumentException("Commands must match (${old.commandName} != ${new.commandName})") } val oldSet = old.positionalParams.sortedBy { it.parameterName }.toSet() - val newSet = new.positionalParams.sortedBy { it.parameterName}.toSet() + val newSet = new.positionalParams.sortedBy { it.parameterName }.toSet() val newIsSuperSetOfOld = newSet.containsAll(oldSet) return if (!newIsSuperSetOfOld) { oldSet.filterNot { newSet.contains(it) }.map { @@ -150,7 +156,9 @@ class CommandLineCompatibilityChecker { val commandLineToCheckName = commandLineToCheck.canonicalName val instance = commandLineToCheck.newInstance() val resourceAsStream = this.javaClass.classLoader.getResourceAsStream("$commandLineToCheckName.yml") - ?: throw IllegalStateException("no Descriptor for $commandLineToCheckName found on classpath") + ?: throw IllegalStateException("$commandLineToCheckName.yml not found on classpath").also { + printCommandLineYAML(commandLineToCheck) + } val old = readCommandDescription(resourceAsStream) val new = topoSort(CommandLine(instance)) return checkCommandLineIsBackwardsCompatible(old, new) @@ -158,8 +166,8 @@ class CommandLineCompatibilityChecker { fun checkBackwardsCompatibility(old: CommandLine, new: CommandLine): List<CliBackwardsCompatibilityValidationCheck> { - val topoSortOld= topoSort(old) - val topoSortNew= topoSort(new) + val topoSortOld = topoSort(old) + val topoSortNew = topoSort(new) return checkCommandLineIsBackwardsCompatible(topoSortOld, topoSortNew) } diff --git a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/UnsafeCertificatesFactory.kt b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/UnsafeCertificatesFactory.kt index 4ce15b78af..bce2152d57 100644 --- a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/UnsafeCertificatesFactory.kt +++ b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/UnsafeCertificatesFactory.kt @@ -4,7 +4,9 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.internal.createFile import net.corda.core.internal.deleteIfExists import net.corda.core.internal.div -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier +import net.corda.nodeapi.internal.config.SslConfiguration +import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.crypto.* import org.apache.commons.io.FileUtils import sun.security.tools.keytool.CertAndKeyGen @@ -65,7 +67,7 @@ class KeyStores(val keyStore: UnsafeKeyStore, val trustStore: UnsafeKeyStore) { val keyStoreFile = keyStore.toTemporaryFile("sslkeystore", directory = directory) val trustStoreFile = trustStore.toTemporaryFile("truststore", directory = directory) - val sslConfiguration = sslConfiguration(directory) + val sslConfiguration = sslConfiguration(keyStoreFile, trustStoreFile) return object : AutoClosableSSLConfiguration { override val value = sslConfiguration @@ -77,16 +79,16 @@ class KeyStores(val keyStore: UnsafeKeyStore, val trustStore: UnsafeKeyStore) { } } - data class TestSslOptions(override val certificatesDirectory: Path, - override val keyStorePassword: String, - override val trustStorePassword: String, - override val crlCheckSoftFail: Boolean) : SSLConfiguration + private fun sslConfiguration(keyStoreFile: TemporaryFile, trustStoreFile: TemporaryFile): MutualSslConfiguration { - private fun sslConfiguration(directory: Path) = TestSslOptions(directory, keyStore.password, trustStore.password, true) + val keyStore = FileBasedCertificateStoreSupplier(keyStoreFile.file, keyStore.password) + val trustStore = FileBasedCertificateStoreSupplier(trustStoreFile.file, trustStore.password) + return SslConfiguration.mutual(keyStore, trustStore) + } } interface AutoClosableSSLConfiguration : AutoCloseable { - val value: SSLConfiguration + val value: MutualSslConfiguration } typealias KeyStoreEntry = Pair<String, UnsafeCertificate> @@ -189,7 +191,7 @@ private fun newKeyStore(type: String, password: String): KeyStore { return keyStore } -fun withKeyStores(server: KeyStores, client: KeyStores, action: (brokerSslOptions: SSLConfiguration, clientSslOptions: SSLConfiguration) -> Unit) { +fun withKeyStores(server: KeyStores, client: KeyStores, action: (brokerSslOptions: MutualSslConfiguration, clientSslOptions: MutualSslConfiguration) -> Unit) { val serverDir = Files.createTempDirectory(null) FileUtils.forceDeleteOnExit(serverDir.toFile()) diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/core/SerializationTestHelpers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/SerializationTestHelpers.kt index d896710d26..514b23a855 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/core/SerializationTestHelpers.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/SerializationTestHelpers.kt @@ -5,6 +5,7 @@ import com.nhaarman.mockito_kotlin.doAnswer import com.nhaarman.mockito_kotlin.whenever import net.corda.core.DoNotImplement import net.corda.core.internal.staticField +import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.internal.SerializationEnvironment import net.corda.core.serialization.internal.effectiveSerializationEnv import net.corda.testing.common.internal.asContextEnv @@ -45,7 +46,6 @@ class SerializationEnvironmentRule(private val inheritable: Boolean = false) : T private lateinit var env: SerializationEnvironment val serializationFactory get() = env.serializationFactory - val checkpointContext get() = env.checkpointContext override fun apply(base: Statement, description: Description): Statement { init(description.toString()) diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestUtils.kt index 3de6c0a16b..485e7671c2 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestUtils.kt @@ -5,10 +5,7 @@ package net.corda.testing.core import net.corda.core.contracts.PartyAndReference import net.corda.core.contracts.StateRef -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.entropyToKeyPair -import net.corda.core.crypto.generateKeyPair -import net.corda.core.crypto.toStringShort +import net.corda.core.crypto.* import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate @@ -25,7 +22,6 @@ import java.math.BigInteger import java.security.KeyPair import java.security.PublicKey import java.security.cert.X509Certificate -import java.util.* import java.util.concurrent.atomic.AtomicInteger /** @@ -46,7 +42,7 @@ import java.util.concurrent.atomic.AtomicInteger * - The Int.DOLLARS syntax doesn't work from Java. Use the DOLLARS(int) function instead. */ -/** Returns a fake state reference for testing purposes **/ +/** Returns a fake state reference for testing purposes. **/ fun generateStateRef(): StateRef = StateRef(SecureHash.randomSHA256(), 0) private val freePortCounter = AtomicInteger(30000) @@ -112,7 +108,7 @@ fun getTestPartyAndCertificate(name: CordaX500Name, publicKey: PublicKey): Party private val count = AtomicInteger(0) /** - * Randomise a party name to avoid clashes with other tests + * Randomise a party name to avoid clashes with other tests. */ fun makeUnique(name: CordaX500Name) = name.copy(commonName = if (name.commonName == null) { @@ -131,24 +127,28 @@ class TestIdentity(val name: CordaX500Name, val keyPair: KeyPair) { * Creates an identity that won't equal any other. This is mostly useful as a throwaway for test helpers. * @param organisation the organisation part of the new identity's name. */ - fun fresh(organisation: String): TestIdentity { - val keyPair = generateKeyPair() + @JvmStatic + @JvmOverloads + fun fresh(organisation: String, signatureScheme: SignatureScheme = Crypto.DEFAULT_SIGNATURE_SCHEME): TestIdentity { + val keyPair = Crypto.generateKeyPair(signatureScheme) val name = CordaX500Name(organisation, keyPair.public.toStringShort(), CordaX500Name.unspecifiedCountry) return TestIdentity(name, keyPair) } } - /** Creates an identity with a deterministic [keyPair] i.e. same [entropy] same keyPair .*/ - constructor(name: CordaX500Name, entropy: Long) : this(name, entropyToKeyPair(BigInteger.valueOf(entropy))) + /** Creates an identity with a deterministic [keyPair] i.e. same [entropy] same keyPair. */ + @JvmOverloads constructor(name: CordaX500Name, entropy: Long, signatureScheme: SignatureScheme = Crypto.DEFAULT_SIGNATURE_SCHEME) + : this(name, Crypto.deriveKeyPairFromEntropy(signatureScheme, BigInteger.valueOf(entropy))) /** Creates an identity with the given name and a fresh keyPair. */ - constructor(name: CordaX500Name) : this(name, generateKeyPair()) + @JvmOverloads constructor(name: CordaX500Name, signatureScheme: SignatureScheme = Crypto.DEFAULT_SIGNATURE_SCHEME) + : this(name, Crypto.generateKeyPair(signatureScheme)) val publicKey: PublicKey get() = keyPair.public val party: Party = Party(name, publicKey) val identity: PartyAndCertificate by lazy { getTestPartyAndCertificate(party) } // Often not needed. - /** Returns a [PartyAndReference] for this identity and the given reference */ + /** Returns a [PartyAndReference] for this identity and the given reference. */ fun ref(vararg bytes: Byte): PartyAndReference = party.ref(*bytes) } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/CheckpointSerializationTestHelpers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/CheckpointSerializationTestHelpers.kt new file mode 100644 index 0000000000..eb92d12cf6 --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/CheckpointSerializationTestHelpers.kt @@ -0,0 +1,71 @@ +package net.corda.testing.core.internal + +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.doAnswer +import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.internal.staticField +import net.corda.core.serialization.internal.SerializationEnvironment +import net.corda.core.serialization.internal.effectiveSerializationEnv +import net.corda.testing.common.internal.asContextEnv +import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.internal.createTestSerializationEnv +import net.corda.testing.internal.inVMExecutors +import net.corda.testing.internal.rigorousMock +import net.corda.testing.internal.testThreadFactory +import org.apache.activemq.artemis.core.remoting.impl.invm.InVMConnector +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +/** + * A test checkpoint serialization rule implementation for use in tests. + * + * @param inheritable whether new threads inherit the environment, use sparingly. + */ +class CheckpointSerializationEnvironmentRule(private val inheritable: Boolean = false) : TestRule { + companion object { + init { + // Can't turn it off, and it creates threads that do serialization, so hack it: + InVMConnector::class.staticField<ExecutorService>("threadPoolExecutor").value = rigorousMock<ExecutorService>().also { + doAnswer { + inVMExecutors.computeIfAbsent(effectiveSerializationEnv) { + Executors.newCachedThreadPool(testThreadFactory(true)) // Close enough to what InVMConnector makes normally. + }.execute(it.arguments[0] as Runnable) + }.whenever(it).execute(any()) + } + } + + /** Do not call, instead use [SerializationEnvironmentRule] as a [org.junit.Rule]. */ + fun <T> run(taskLabel: String, task: (SerializationEnvironment) -> T): T { + return CheckpointSerializationEnvironmentRule().apply { init(taskLabel) }.runTask(task) + } + } + + + private lateinit var env: SerializationEnvironment + + override fun apply(base: Statement, description: Description): Statement { + init(description.toString()) + return object : Statement() { + override fun evaluate() = runTask { base.evaluate() } + } + } + + private fun init(envLabel: String) { + env = createTestSerializationEnv(envLabel) + } + + private fun <T> runTask(task: (SerializationEnvironment) -> T): T { + try { + return env.asContextEnv(inheritable, task) + } finally { + inVMExecutors.remove(env) + } + } + + val checkpointSerializationFactory get() = env.checkpointSerializationFactory + val checkpointSerializationContext get() = env.checkpointContext + +} diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt index 5a8e4c83a1..53bee6f798 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt @@ -4,10 +4,11 @@ import com.nhaarman.mockito_kotlin.doNothing import com.nhaarman.mockito_kotlin.whenever import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme import net.corda.core.DoNotImplement +import net.corda.core.serialization.internal.CheckpointSerializationFactory import net.corda.core.serialization.internal.* import net.corda.node.serialization.amqp.AMQPServerSerializationScheme import net.corda.node.serialization.kryo.KRYO_CHECKPOINT_CONTEXT -import net.corda.node.serialization.kryo.KryoServerSerializationScheme +import net.corda.node.serialization.kryo.KryoSerializationScheme import net.corda.serialization.internal.* import net.corda.testing.core.SerializationEnvironmentRule import java.util.concurrent.ConcurrentHashMap @@ -33,8 +34,6 @@ internal fun createTestSerializationEnv(label: String): SerializationEnvironment val factory = SerializationFactoryImpl().apply { registerScheme(AMQPClientSerializationScheme(emptyList())) registerScheme(AMQPServerSerializationScheme(emptyList())) - // needed for checkpointing - registerScheme(KryoServerSerializationScheme()) } return object : SerializationEnvironmentImpl( factory, @@ -42,7 +41,8 @@ internal fun createTestSerializationEnv(label: String): SerializationEnvironment AMQP_RPC_SERVER_CONTEXT, AMQP_RPC_CLIENT_CONTEXT, AMQP_STORAGE_CONTEXT, - KRYO_CHECKPOINT_CONTEXT + KRYO_CHECKPOINT_CONTEXT, + CheckpointSerializationFactory(KryoSerializationScheme) ) { override fun toString() = "testSerializationEnv($label)" } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt index 5b4555e547..4ddca172af 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt @@ -9,15 +9,15 @@ import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.NodeInfo import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor -import net.corda.node.services.config.configureDevKeyAndTrustStores import net.corda.nodeapi.BrokerRpcSslOptions -import net.corda.nodeapi.internal.config.SSLConfiguration -import net.corda.nodeapi.internal.createDevKeyStores +import net.corda.nodeapi.internal.config.MutualSslConfiguration +import net.corda.nodeapi.internal.registerDevP2pCertificates import net.corda.nodeapi.internal.createDevNodeCa import net.corda.nodeapi.internal.crypto.* +import net.corda.nodeapi.internal.loadDevCaTrustStore import net.corda.serialization.internal.amqp.AMQP_ENABLED +import net.corda.testing.internal.stubs.CertificateStoreStubs import java.nio.file.Files import java.nio.file.Path import java.security.KeyPair @@ -37,17 +37,17 @@ inline fun <reified T : Any> T.amqpSpecific(reason: String, function: () -> Unit loggerFor<T>().info("Ignoring AMQP specific test, reason: $reason") } -fun configureTestSSL(legalName: CordaX500Name): SSLConfiguration { - return object : SSLConfiguration { - override val certificatesDirectory = Files.createTempDirectory("certs") - override val keyStorePassword: String get() = "cordacadevpass" - override val trustStorePassword: String get() = "trustpass" - override val crlCheckSoftFail: Boolean = true +fun configureTestSSL(legalName: CordaX500Name): MutualSslConfiguration { - init { - configureDevKeyAndTrustStores(legalName) - } + val certificatesDirectory = Files.createTempDirectory("certs") + val config = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) + if (config.trustStore.getOptional() == null) { + loadDevCaTrustStore().copyTo(config.trustStore.get(true)) } + if (config.keyStore.getOptional() == null) { + config.keyStore.get(true).registerDevP2pCertificates(legalName) + } + return config } private val defaultRootCaName = X500Principal("CN=Corda Root CA,O=R3 Ltd,L=London,C=GB") @@ -103,17 +103,6 @@ fun BrokerRpcSslOptions.useSslRpcOverrides(): Map<String, String> { ) } -fun SSLConfiguration.noSslRpcOverrides(rpcAdminAddress: NetworkHostAndPort): Map<String, Any> { - return mapOf( - "rpcSettings.adminAddress" to rpcAdminAddress.toString(), - "rpcSettings.useSsl" to "false", - "rpcSettings.ssl.certificatesDirectory" to certificatesDirectory.toString(), - "rpcSettings.ssl.keyStorePassword" to keyStorePassword, - "rpcSettings.ssl.trustStorePassword" to trustStorePassword, - "rpcSettings.ssl.crlCheckSoftFail" to true - ) -} - /** * Until we have proper handling of multiple identities per node, for tests we use the first identity as special one. * TODO: Should be removed after multiple identities are introduced. @@ -127,19 +116,12 @@ fun NodeInfo.chooseIdentityAndCert(): PartyAndCertificate = legalIdentitiesAndCe */ fun NodeInfo.chooseIdentity(): Party = chooseIdentityAndCert().party -fun createNodeSslConfig(path: Path, name: CordaX500Name = CordaX500Name("MegaCorp", "London", "GB")): SSLConfiguration { - val sslConfig = object : SSLConfiguration { - override val crlCheckSoftFail = true - override val certificatesDirectory = path - override val keyStorePassword = "serverstorepass" - override val trustStorePassword = "trustpass" - } +fun p2pSslOptions(path: Path, name: CordaX500Name = CordaX500Name("MegaCorp", "London", "GB")): MutualSslConfiguration { + val sslConfig = CertificateStoreStubs.P2P.withCertificatesDirectory(path, keyStorePassword = "serverstorepass") val (rootCa, intermediateCa) = createDevIntermediateCaCertPath() - sslConfig.createDevKeyStores(name, rootCa.certificate, intermediateCa) - val trustStore = loadOrCreateKeyStore(sslConfig.trustStoreFile, sslConfig.trustStorePassword) - trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCa.certificate) - trustStore.save(sslConfig.trustStoreFile, sslConfig.trustStorePassword) - + sslConfig.keyStore.get(true).registerDevP2pCertificates(name, rootCa.certificate, intermediateCa) + val trustStore = sslConfig.trustStore.get(true) + trustStore[X509Utilities.CORDA_ROOT_CA] = rootCa.certificate return sslConfig } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt index 508ace7fb4..9874799f6d 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt @@ -33,6 +33,7 @@ class MockCordappProvider( serializationCustomSerializers = emptyList(), customSchemas = emptySet(), jarPath = Paths.get("").toUri().toURL(), + info = CordappImpl.Info.UNKNOWN, allFlows = emptyList(), jarHash = SecureHash.allOnesHash) if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) }) { @@ -40,7 +41,9 @@ class MockCordappProvider( } } - override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? = cordappRegistry.find { it.first.contractClassNames.contains(contractClassName) }?.second ?: super.getContractAttachmentID(contractClassName) + override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? { + return cordappRegistry.find { it.first.contractClassNames.contains(contractClassName) }?.second ?: super.getContractAttachmentID(contractClassName) + } private fun findOrImportAttachment(contractClassNames: List<ContractClassName>, data: ByteArray, attachments: MockAttachmentStorage): AttachmentId { val existingAttachment = attachments.files.filter { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/stubs/CertificateStoreStubs.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/stubs/CertificateStoreStubs.kt new file mode 100644 index 0000000000..62f871ca8c --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/stubs/CertificateStoreStubs.kt @@ -0,0 +1,101 @@ +package net.corda.testing.internal.stubs + +import net.corda.core.internal.div +import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier +import net.corda.nodeapi.internal.config.SslConfiguration +import net.corda.nodeapi.internal.config.MutualSslConfiguration +import java.nio.file.Path + +class CertificateStoreStubs { + + companion object { + + const val DEFAULT_CERTIFICATES_DIRECTORY_NAME = "certificates" + + @JvmStatic + fun withStoreAt(certificateStorePath: Path, password: String): FileBasedCertificateStoreSupplier = FileBasedCertificateStoreSupplier(certificateStorePath, password) + } + + class Signing { + + companion object { + + const val DEFAULT_STORE_FILE_NAME = "nodekeystore.jks" + const val DEFAULT_STORE_PASSWORD = "cordacadevpass" + + @JvmStatic + fun withCertificatesDirectory(certificatesDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { + + return FileBasedCertificateStoreSupplier(certificatesDirectory / certificateStoreFileName, password) + } + + @JvmStatic + fun withBaseDirectory(baseDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, certificatesDirectoryName: String = DEFAULT_CERTIFICATES_DIRECTORY_NAME, certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { + + return FileBasedCertificateStoreSupplier(baseDirectory / certificatesDirectoryName / certificateStoreFileName, password) + } + } + } + + class P2P { + + companion object { + + @JvmStatic + fun withCertificatesDirectory(certificatesDirectory: Path, keyStoreFileName: String = KeyStore.DEFAULT_STORE_FILE_NAME, keyStorePassword: String = KeyStore.DEFAULT_STORE_PASSWORD, trustStoreFileName: String = TrustStore.DEFAULT_STORE_FILE_NAME, trustStorePassword: String = TrustStore.DEFAULT_STORE_PASSWORD): MutualSslConfiguration { + + val keyStore = FileBasedCertificateStoreSupplier(certificatesDirectory / keyStoreFileName, keyStorePassword) + val trustStore = FileBasedCertificateStoreSupplier(certificatesDirectory / trustStoreFileName, trustStorePassword) + return SslConfiguration.mutual(keyStore, trustStore) + } + + @JvmStatic + fun withBaseDirectory(baseDirectory: Path, certificatesDirectoryName: String = DEFAULT_CERTIFICATES_DIRECTORY_NAME, keyStoreFileName: String = KeyStore.DEFAULT_STORE_FILE_NAME, keyStorePassword: String = KeyStore.DEFAULT_STORE_PASSWORD, trustStoreFileName: String = TrustStore.DEFAULT_STORE_FILE_NAME, trustStorePassword: String = TrustStore.DEFAULT_STORE_PASSWORD): MutualSslConfiguration { + + return withCertificatesDirectory(baseDirectory / certificatesDirectoryName, keyStoreFileName, keyStorePassword, trustStoreFileName, trustStorePassword) + } + } + + class KeyStore { + + companion object { + + const val DEFAULT_STORE_FILE_NAME = "sslkeystore.jks" + const val DEFAULT_STORE_PASSWORD = "cordacadevpass" + + @JvmStatic + fun withCertificatesDirectory(certificatesDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { + + return FileBasedCertificateStoreSupplier(certificatesDirectory / certificateStoreFileName, password) + } + + @JvmStatic + fun withBaseDirectory(baseDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, certificatesDirectoryName: String = DEFAULT_CERTIFICATES_DIRECTORY_NAME, certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { + + return FileBasedCertificateStoreSupplier(baseDirectory / certificatesDirectoryName / certificateStoreFileName, password) + } + } + } + + class TrustStore { + + companion object { + + const val DEFAULT_STORE_FILE_NAME = "truststore.jks" + const val DEFAULT_STORE_PASSWORD = "trustpass" + + @JvmStatic + fun withCertificatesDirectory(certificatesDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { + + return FileBasedCertificateStoreSupplier(certificatesDirectory / certificateStoreFileName, password) + } + + @JvmStatic + fun withBaseDirectory(baseDirectory: Path, password: String = DEFAULT_STORE_PASSWORD, certificatesDirectoryName: String = DEFAULT_CERTIFICATES_DIRECTORY_NAME, certificateStoreFileName: String = DEFAULT_STORE_FILE_NAME): FileBasedCertificateStoreSupplier { + + return FileBasedCertificateStoreSupplier(baseDirectory / certificatesDirectoryName / certificateStoreFileName, password) + } + } + } + } +} \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/UniqueDummyFungibleContract.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/UniqueDummyFungibleContract.kt new file mode 100644 index 0000000000..88be337e35 --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/UniqueDummyFungibleContract.kt @@ -0,0 +1,45 @@ +package net.corda.testing.internal.vault + +import net.corda.core.contracts.* +import net.corda.core.identity.AbstractParty +import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.PersistentState +import net.corda.core.schemas.QueryableState +import net.corda.core.transactions.LedgerTransaction +import net.corda.testing.core.DummyCommandData +import java.util.* +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Table + +const val UNIQUE_DUMMY_FUNGIBLE_CONTRACT_PROGRAM_ID = "net.corda.testing.internal.vault.UniqueDummyFungibleContract" + +class UniqueDummyFungibleContract : Contract { + override fun verify(tx: LedgerTransaction) {} + + data class State(override val amount: Amount<Issued<Currency>>, + override val owner: AbstractParty) : FungibleAsset<Currency>, QueryableState { + + override val exitKeys = setOf(owner.owningKey, amount.token.issuer.party.owningKey) + override val participants = listOf(owner) + + override fun withNewOwnerAndAmount(newAmount: Amount<Issued<Currency>>, newOwner: AbstractParty): FungibleAsset<Currency> = copy(amount = amount.copy(newAmount.quantity), owner = newOwner) + + override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(DummyCommandData, copy(owner = newOwner)) + + override fun supportedSchemas(): Iterable<MappedSchema> = listOf(UniqueDummyFungibleStateSchema) + + override fun generateMappedObject(schema: MappedSchema): PersistentState { + return UniqueDummyFungibleStateSchema.UniquePersistentDummyFungibleState(currency = amount.token.product.currencyCode) + } + } +} + +object UniqueDummyFungibleStateSchema : MappedSchema(schemaFamily = UniqueDummyFungibleStateSchema::class.java, version = 1, mappedTypes = listOf(UniquePersistentDummyFungibleState::class.java)) { + @Entity + @Table(name = "unique_dummy_fungible_state") + class UniquePersistentDummyFungibleState( + @Column(unique = true) + val currency: String + ) : PersistentState() +} \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/UniqueDummyLinearContract.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/UniqueDummyLinearContract.kt new file mode 100644 index 0000000000..07e9673fe1 --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/UniqueDummyLinearContract.kt @@ -0,0 +1,41 @@ +package net.corda.testing.internal.vault + +import net.corda.core.contracts.Contract +import net.corda.core.contracts.LinearState +import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.identity.AbstractParty +import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.PersistentState +import net.corda.core.schemas.QueryableState +import net.corda.core.transactions.LedgerTransaction +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Table + +const val UNIQUE_DUMMY_LINEAR_CONTRACT_PROGRAM_ID = "net.corda.testing.internal.vault.UniqueDummyLinearContract" + +class UniqueDummyLinearContract : Contract { + override fun verify(tx: LedgerTransaction) {} + + data class State( + override val participants: List<AbstractParty>, + override val linearId: UniqueIdentifier) : LinearState, QueryableState { + constructor(participants: List<AbstractParty> = listOf(), + ref: String) : this(participants, UniqueIdentifier(ref)) + + override fun supportedSchemas(): Iterable<MappedSchema> = listOf(UniqueDummyLinearStateSchema) + + override fun generateMappedObject(schema: MappedSchema): PersistentState { + return UniqueDummyLinearStateSchema.UniquePersistentLinearDummyState(id = linearId.externalId!!) + } + } +} + +object UniqueDummyLinearStateSchema : MappedSchema(schemaFamily = UniqueDummyLinearStateSchema::class.java, version = 1, mappedTypes = listOf(UniquePersistentLinearDummyState::class.java)) { + @Entity + @Table(name = "unique_dummy_linear_state") + class UniquePersistentLinearDummyState( + @Column(unique = true) + val id: String + ) : PersistentState() +} \ No newline at end of file diff --git a/tools/blobinspector/build.gradle b/tools/blobinspector/build.gradle index 4cd7ef4386..a1f6f865ef 100644 --- a/tools/blobinspector/build.gradle +++ b/tools/blobinspector/build.gradle @@ -5,7 +5,7 @@ apply plugin: 'com.jfrog.artifactory' dependencies { compile project(':client:jackson') - compile "info.picocli:picocli:$picocli_version" + compile project(':tools:cliutils') compile "org.slf4j:jul-to-slf4j:$slf4j_version" compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version" @@ -23,10 +23,9 @@ jar { exclude "META-INF/*.DSA" exclude "META-INF/*.RSA" } - archiveName = "blob-inspector-${corda_release_version}.jar" + baseName = "blob-inspector" manifest { attributes( - 'Automatic-Module-Name': 'net.corda.blobinspector', 'Main-Class': 'net.corda.blobinspector.BlobInspectorKt' ) } diff --git a/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt b/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt index 655ddb830c..570a9cbe2e 100644 --- a/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt +++ b/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt @@ -4,6 +4,9 @@ import com.fasterxml.jackson.core.JsonFactory import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import com.jcabi.manifests.Manifests import net.corda.client.jackson.JacksonSupport +import net.corda.cliutils.CordaCliWrapper +import net.corda.cliutils.ExitCodes +import net.corda.cliutils.start import net.corda.core.internal.isRegularFile import net.corda.core.internal.rootMessage import net.corda.core.serialization.SerializationContext @@ -14,46 +17,27 @@ import net.corda.core.serialization.internal._contextSerializationEnv import net.corda.core.utilities.base64ToByteArray import net.corda.core.utilities.hexToByteArray import net.corda.core.utilities.sequence -import net.corda.serialization.internal.AMQP_P2P_CONTEXT -import net.corda.serialization.internal.AMQP_STORAGE_CONTEXT -import net.corda.serialization.internal.CordaSerializationMagic -import net.corda.serialization.internal.SerializationFactoryImpl +import net.corda.serialization.internal.* import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme import net.corda.serialization.internal.amqp.DeserializationInput import net.corda.serialization.internal.amqp.amqpMagic +import org.slf4j.event.Level import picocli.CommandLine import picocli.CommandLine.* import java.io.PrintStream import java.net.MalformedURLException import java.net.URL import java.nio.file.Paths +import java.util.* import kotlin.system.exitProcess fun main(args: Array<String>) { - val main = BlobInspector() - try { - CommandLine.run(main, *args) - } catch (e: ExecutionException) { - val throwable = e.cause ?: e - if (main.verbose) { - throwable.printStackTrace() - } else { - System.err.println("*ERROR*: ${throwable.rootMessage}. Use --verbose for more details") - } - exitProcess(1) - } + BlobInspector().start(args) } -@Command( - name = "blob-inspector", - versionProvider = CordaVersionProvider::class, - mixinStandardHelpOptions = true, // add --help and --version options, - showDefaultValues = true, - description = ["Convert AMQP serialised binary blobs to text"] -) -class BlobInspector : Runnable { - @Parameters(index = "0", paramLabel = "SOURCE", description = ["URL or file path to the blob"], converter = [SourceConverter::class]) - var source: URL? = null +class BlobInspector : CordaCliWrapper("blob-inspector", "Convert AMQP serialised binary blobs to text") { + @Parameters(index = "*..0", paramLabel = "SOURCE", description = ["URL or file path to the blob"], converter = [SourceConverter::class]) + var source: MutableList<URL> = mutableListOf() @Option(names = ["--format"], paramLabel = "type", description = ["Output format. Possible values: [YAML, JSON]"]) private var formatType: OutputFormatType = OutputFormatType.YAML @@ -68,17 +52,19 @@ class BlobInspector : Runnable { @Option(names = ["--schema"], description = ["Print the blob's schema first"]) private var schema: Boolean = false - @Option(names = ["--verbose"], description = ["Enable verbose output"]) - var verbose: Boolean = false + override fun runProgram() = run(System.out) - override fun run() = run(System.out) - - fun run(out: PrintStream) { + override fun initLogging() { if (verbose) { - System.setProperty("logLevel", "trace") + loggingLevel = Level.TRACE } + val loggingLevel = loggingLevel.name.toLowerCase(Locale.ENGLISH) + System.setProperty("logLevel", loggingLevel) // This property is referenced from the XML config file. + } - val inputBytes = source!!.readBytes() + fun run(out: PrintStream): Int { + require(source.count() == 1) { "You must specify URL or file path to the blob" } + val inputBytes = source.first().readBytes() val bytes = parseToBinaryRelaxed(inputFormatType, inputBytes) ?: throw IllegalArgumentException("Error: this input does not appear to be encoded in Corda's AMQP extended format, sorry.") @@ -102,6 +88,9 @@ class BlobInspector : Runnable { val deserialized = bytes.deserialize<Any>(context = SerializationDefaults.STORAGE_CONTEXT) out.println(deserialized.javaClass.name) mapper.writeValue(out, deserialized) + return ExitCodes.SUCCESS + } catch(e: Exception) { + return ExitCodes.FAILURE } finally { _contextSerializationEnv.set(null) } diff --git a/tools/blobinspector/src/test/kotlin/net/corda/blobinspector/BlobInspectorTest.kt b/tools/blobinspector/src/test/kotlin/net/corda/blobinspector/BlobInspectorTest.kt index 65e1223c4f..04cc72d621 100644 --- a/tools/blobinspector/src/test/kotlin/net/corda/blobinspector/BlobInspectorTest.kt +++ b/tools/blobinspector/src/test/kotlin/net/corda/blobinspector/BlobInspectorTest.kt @@ -53,7 +53,7 @@ class BlobInspectorTest { } private fun run(resourceName: String): String { - blobInspector.source = javaClass.getResource(resourceName) + blobInspector.source = mutableListOf(javaClass.getResource(resourceName)) val writer = StringWriter() blobInspector.run(PrintStream(WriterOutputStream(writer, UTF_8))) val output = writer.toString() diff --git a/tools/bootstrapper/build.gradle b/tools/bootstrapper/build.gradle index de04e80796..dc98459edd 100644 --- a/tools/bootstrapper/build.gradle +++ b/tools/bootstrapper/build.gradle @@ -1,4 +1,3 @@ -apply plugin: 'java' apply plugin: 'kotlin' apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'com.jfrog.artifactory' @@ -8,14 +7,21 @@ description 'Network bootstrapper' dependencies { compile project(':node-api') compile project(':tools:cliutils') - compile "info.picocli:picocli:$picocli_version" - compile "org.slf4j:jul-to-slf4j:$slf4j_version" compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" - compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version" + + testCompile(project(':test-utils')) { + exclude group: 'org.apache.logging.log4j', module: 'log4j-slf4j-impl' + } + + testCompile(project(':test-cli')) +} + +processResources { + from file("$rootDir/config/dev/log4j2.xml") } jar { - from(configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }) { + from(configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }) { exclude "META-INF/*.SF" exclude "META-INF/*.DSA" exclude "META-INF/*.RSA" @@ -23,10 +29,9 @@ jar { from(project(':node:capsule').tasks['buildCordaJAR']) { rename 'corda-(.*)', 'corda.jar' } - archiveName = "network-bootstrapper-${corda_release_version}.jar" + baseName = "network-bootstrapper" manifest { attributes( - 'Automatic-Module-Name': 'net.corda.bootstrapper', 'Main-Class': 'net.corda.bootstrapper.MainKt' ) } diff --git a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt index a75c92d53d..09e6ca9972 100644 --- a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt +++ b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt @@ -8,7 +8,7 @@ import java.nio.file.Path import java.nio.file.Paths fun main(args: Array<String>) { - NetworkBootstrapperRunner().start(*args) + NetworkBootstrapperRunner().start(args) } class NetworkBootstrapperRunner : CordaCliWrapper("bootstrapper", "Bootstrap a local test Corda network using a set of node configuration files and CorDapp JARs") { @@ -24,7 +24,8 @@ class NetworkBootstrapperRunner : CordaCliWrapper("bootstrapper", "Bootstrap a l @Option(names = ["--no-copy"], description = ["""Don't copy the CorDapp JARs into the nodes' "cordapps" directories."""]) private var noCopy: Boolean = false - override fun runProgram() { + override fun runProgram(): Int { NetworkBootstrapper().bootstrap(dir.toAbsolutePath().normalize(), copyCordapps = !noCopy) + return 0 //exit code } } \ No newline at end of file diff --git a/tools/bootstrapper/src/main/resources/log4j2.xml b/tools/bootstrapper/src/main/resources/log4j2.xml deleted file mode 100644 index 98b3648e6b..0000000000 --- a/tools/bootstrapper/src/main/resources/log4j2.xml +++ /dev/null @@ -1,16 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<Configuration status="info"> - <Properties> - <Property name="logLevel">off</Property> - </Properties> - <Appenders> - <Console name="STDOUT" target="SYSTEM_OUT" ignoreExceptions="false"> - <PatternLayout pattern="[%C{1}.%M] %m%n"/> - </Console> - </Appenders> - <Loggers> - <Root level="${sys:logLevel}"> - <AppenderRef ref="STDOUT"/> - </Root> - </Loggers> -</Configuration> \ No newline at end of file diff --git a/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/NetworkBootstrapperRunnerTest.kt b/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/NetworkBootstrapperRunnerTest.kt new file mode 100644 index 0000000000..8322c5862f --- /dev/null +++ b/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/NetworkBootstrapperRunnerTest.kt @@ -0,0 +1,5 @@ +package net.corda.bootstrapper + +import net.corda.testing.CliBackwardsCompatibleTest + +class NetworkBootstrapperRunnerTest : CliBackwardsCompatibleTest(NetworkBootstrapperRunner::class.java) \ No newline at end of file diff --git a/tools/bootstrapper/src/test/resources/net.corda.bootstrapper.NetworkBootstrapperRunner.yml b/tools/bootstrapper/src/test/resources/net.corda.bootstrapper.NetworkBootstrapperRunner.yml new file mode 100644 index 0000000000..de9379b953 --- /dev/null +++ b/tools/bootstrapper/src/test/resources/net.corda.bootstrapper.NetworkBootstrapperRunner.yml @@ -0,0 +1,43 @@ +- commandName: "<main class>" + positionalParams: [] + params: + - parameterName: "--dir" + parameterType: "java.nio.file.Path" + required: false + multiParam: true + acceptableValues: [] + - parameterName: "--install-shell-extensions" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--log-to-console" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--logging-level" + parameterType: "org.slf4j.event.Level" + required: false + multiParam: false + acceptableValues: + - "ERROR" + - "WARN" + - "INFO" + - "DEBUG" + - "TRACE" + - parameterName: "--no-copy" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--verbose" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "-v" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] \ No newline at end of file diff --git a/tools/cliutils/build.gradle b/tools/cliutils/build.gradle index 87c7575f0c..0f341432b7 100644 --- a/tools/cliutils/build.gradle +++ b/tools/cliutils/build.gradle @@ -1,5 +1,7 @@ apply plugin: 'java' apply plugin: 'kotlin' +apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' description 'CLI Utilities' @@ -8,7 +10,20 @@ dependencies { compile "info.picocli:picocli:$picocli_version" compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version" + compile "org.slf4j:slf4j-api:$slf4j_version" compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" + // JAnsi: for drawing things to the terminal in nicely coloured ways. + compile "org.fusesource.jansi:jansi:$jansi_version" + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" } + +jar { + baseName = "cliutils" +} + +publish { + name 'corda-tools-cliutils' +} + diff --git a/tools/cliutils/src/main/kotlin/net/corda/cliutils/ConfigFilePathArgsParser.kt b/tools/cliutils/src/main/kotlin/net/corda/cliutils/ConfigFilePathArgsParser.kt deleted file mode 100644 index 3d8c37829d..0000000000 --- a/tools/cliutils/src/main/kotlin/net/corda/cliutils/ConfigFilePathArgsParser.kt +++ /dev/null @@ -1,25 +0,0 @@ -package net.corda.cliutils - -import net.corda.core.internal.exists -import net.corda.core.internal.isReadable -import picocli.CommandLine -import java.nio.file.Path - -/** - * When a config file is required as part of setup, use this class to check that it exists and is formatted correctly. Add it as - * `@CommandLine.Mixin - * lateinit var configParser: ConfigFilePathArgsParser` - * in your command class and then call `validate()` - */ -@CommandLine.Command(description = ["Parse configuration file. Checks if given configuration file exists"]) -class ConfigFilePathArgsParser : Validated { - @CommandLine.Option(names = ["--config-file", "-f"], required = true, paramLabel = "FILE", description = ["The path to the config file"]) - lateinit var configFile: Path - - override fun validator(): List<String> { - val res = mutableListOf<String>() - if (!configFile.exists()) res += "Config file ${configFile.toAbsolutePath().normalize()} does not exist!" - if (!configFile.isReadable) res += "Config file ${configFile.toAbsolutePath().normalize()} is not readable" - return res - } -} \ No newline at end of file diff --git a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt index 317b1b4654..f30c3ed05d 100644 --- a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt +++ b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt @@ -2,12 +2,17 @@ package net.corda.cliutils import net.corda.core.internal.rootMessage import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.loggerFor -import org.apache.logging.log4j.Level +import org.fusesource.jansi.AnsiConsole +import org.slf4j.event.Level import picocli.CommandLine import picocli.CommandLine.* +import java.nio.file.Path +import java.nio.file.Paths import kotlin.system.exitProcess import java.util.* +import java.util.concurrent.Callable /** * When we have errors in command line flags that are not handled by picocli (e.g. non existing files), an error is thrown @@ -34,19 +39,48 @@ interface Validated { logger.error(RED + "Exceptions when parsing command line arguments:") logger.error(errors.joinToString("\n") + RESET) CommandLine(this).usage(System.err) - exitProcess(1) + exitProcess(ExitCodes.FAILURE) } } } -fun CordaCliWrapper.start(vararg args: String) { +/** This is generally covered by commons-lang. */ +object CordaSystemUtils { + private const val OS_NAME = "os.name" + private const val MAC_PREFIX = "Mac" + private const val WIN_PREFIX = "Windows" + + fun isOsMac(): Boolean = getOsName().startsWith(MAC_PREFIX) + fun isOsWindows(): Boolean = getOsName().startsWith(WIN_PREFIX) + fun getOsName(): String = System.getProperty(OS_NAME) +} + +fun CordaCliWrapper.start(args: Array<String>) { + this.args = args + + // This line makes sure ANSI escapes work on Windows, where they aren't supported out of the box. + AnsiConsole.systemInstall() + val cmd = CommandLine(this) + // Make sure any provided paths are absolute. Relative paths have caused issues and are less clear in logs. + cmd.registerConverter(Path::class.java) { Paths.get(it).toAbsolutePath().normalize() } cmd.commandSpec.name(alias) cmd.commandSpec.usageMessage().description(description) try { - cmd.parseWithHandlers(RunLast().useOut(System.out).useAnsi(Help.Ansi.AUTO), - DefaultExceptionHandler<List<Any>>().useErr(System.err).useAnsi(Help.Ansi.AUTO), + val defaultAnsiMode = if (CordaSystemUtils.isOsWindows()) { Help.Ansi.ON } else { Help.Ansi.AUTO } + val results = cmd.parseWithHandlers(RunLast().useOut(System.out).useAnsi(defaultAnsiMode), + DefaultExceptionHandler<List<Any>>().useErr(System.err).useAnsi(defaultAnsiMode), *args) + // If an error code has been returned, use this and exit + results?.firstOrNull()?.let { + if (it is Int) { + exitProcess(it) + } else { + exitProcess(ExitCodes.FAILURE) + } + } + // If no results returned, picocli ran something without invoking the main program, e.g. --help or --version, so exit successfully + exitProcess(ExitCodes.SUCCESS) } catch (e: ExecutionException) { val throwable = e.cause ?: e if (this.verbose) { @@ -54,7 +88,7 @@ fun CordaCliWrapper.start(vararg args: String) { } else { System.err.println("*ERROR*: ${throwable.rootMessage ?: "Use --verbose for more details"}") } - exitProcess(1) + exitProcess(ExitCodes.FAILURE) } } @@ -72,8 +106,16 @@ fun CordaCliWrapper.start(vararg args: String) { parameterListHeading = "%n@|bold,underline Parameters|@:%n%n", optionListHeading = "%n@|bold,underline Options|@:%n%n", commandListHeading = "%n@|bold,underline Commands|@:%n%n") -abstract class CordaCliWrapper(val alias: String, val description: String) : Runnable { - @Option(names = ["-v", "--verbose"], description = ["If set, prints logging to the console as well as to a file."]) +abstract class CordaCliWrapper(val alias: String, val description: String) : Callable<Int> { + companion object { + private val logger by lazy { loggerFor<CordaCliWrapper>() } + } + + // Raw args are provided for use in logging - this is a lateinit var rather than a constructor parameter as the class + // needs to be parameterless for autocomplete to work. + lateinit var args: Array<String> + + @Option(names = ["-v", "--verbose", "--log-to-console"], description = ["If set, prints logging to the console as well as to a file."]) var verbose: Boolean = false @Option(names = ["--logging-level"], @@ -88,30 +130,33 @@ abstract class CordaCliWrapper(val alias: String, val description: String) : Run // This needs to be called before loggers (See: NodeStartup.kt:51 logger called by lazy, initLogging happens before). // Node's logging is more rich. In corda configurations two properties, defaultLoggingLevel and consoleLogLevel, are usually used. - private fun initLogging() { - val loggingLevel = loggingLevel.name().toLowerCase(Locale.ENGLISH) + open fun initLogging() { + val loggingLevel = loggingLevel.name.toLowerCase(Locale.ENGLISH) System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file. if (verbose) { System.setProperty("consoleLogLevel", loggingLevel) } + System.setProperty("log-path", Paths.get(".").toString()) } - // Override this function with the actual method to be run once all the arguments have been parsed - abstract fun runProgram() + // Override this function with the actual method to be run once all the arguments have been parsed. The return number + // is the exit code to be returned + abstract fun runProgram(): Int - final override fun run() { - installShellExtensionsParser.installOrUpdateShellExtensions(alias, this.javaClass.name) + override fun call(): Int { initLogging() - runProgram() + logger.info("Application Args: ${args.joinToString(" ")}") + installShellExtensionsParser.installOrUpdateShellExtensions(alias, this.javaClass.name) + return runProgram() } } /** - * Converter from String to log4j logging Level. + * Converter from String to slf4j logging Level. */ class LoggingLevelConverter : ITypeConverter<Level> { override fun convert(value: String?): Level { - return value?.let { Level.getLevel(it) } + return value?.let { Level.valueOf(it.toUpperCase()) } ?: throw TypeConversionException("Unknown option for --logging-level: $value") } diff --git a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaLog4j2ConfigFactory.kt b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaLog4j2ConfigFactory.kt new file mode 100644 index 0000000000..1dc245b26a --- /dev/null +++ b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaLog4j2ConfigFactory.kt @@ -0,0 +1,35 @@ +package net.corda.cliutils + +import org.apache.logging.log4j.core.LoggerContext +import org.apache.logging.log4j.core.config.Configuration +import org.apache.logging.log4j.core.config.ConfigurationFactory +import org.apache.logging.log4j.core.config.ConfigurationSource +import org.apache.logging.log4j.core.config.Order +import org.apache.logging.log4j.core.config.plugins.Plugin +import org.apache.logging.log4j.core.config.xml.XmlConfiguration +import org.apache.logging.log4j.core.impl.LogEventFactory + +@Plugin(name = "CordaLog4j2ConfigFactory", category = "ConfigurationFactory") +@Order(10) +class CordaLog4j2ConfigFactory : ConfigurationFactory() { + + private companion object { + private val SUPPORTED_TYPES = arrayOf(".xml", "*") + } + + override fun getConfiguration(loggerContext: LoggerContext, source: ConfigurationSource): Configuration = ErrorCodeAppendingConfiguration(loggerContext, source) + + override fun getSupportedTypes() = SUPPORTED_TYPES + + private class ErrorCodeAppendingConfiguration(loggerContext: LoggerContext, source: ConfigurationSource) : XmlConfiguration(loggerContext, source) { + + override fun doConfigure() { + + super.doConfigure() + loggers.values.forEach { + val existingFactory = it.logEventFactory + it.logEventFactory = LogEventFactory { loggerName, marker, fqcn, level, message, properties, error -> existingFactory.createEvent(loggerName, marker, fqcn, level, message?.withErrorCodeFor(error, level), properties, error) } + } + } + } +} \ No newline at end of file diff --git a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaVersionProvider.kt b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaVersionProvider.kt index 7158fa722b..85f9e9100b 100644 --- a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaVersionProvider.kt +++ b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaVersionProvider.kt @@ -9,13 +9,17 @@ import picocli.CommandLine */ class CordaVersionProvider : CommandLine.IVersionProvider { companion object { - val releaseVersion: String by lazy { Manifests.read("Corda-Release-Version") } - val revision: String by lazy { Manifests.read("Corda-Revision") } + private fun manifestValue(name: String): String? = if (Manifests.exists(name)) Manifests.read(name) else null + + val releaseVersion: String by lazy { manifestValue("Corda-Release-Version") ?: "Unknown" } + val revision: String by lazy { manifestValue("Corda-Revision") ?: "Unknown" } + val vendor: String by lazy { manifestValue("Corda-Vendor") ?: "Unknown" } + val platformVersion: Int by lazy { manifestValue("Corda-Platform-Version")?.toInt() ?: 1 } } override fun getVersion(): Array<String> { return if (Manifests.exists("Corda-Release-Version") && Manifests.exists("Corda-Revision")) { - arrayOf("Version: $releaseVersion", "Revision: $revision") + arrayOf("Version: $releaseVersion", "Revision: $revision", "Platform Version: $platformVersion", "Vendor: $vendor") } else { arrayOf("No version data is available in the MANIFEST file.") } diff --git a/tools/cliutils/src/main/kotlin/net/corda/cliutils/ExceptionsErrorCodeFunctions.kt b/tools/cliutils/src/main/kotlin/net/corda/cliutils/ExceptionsErrorCodeFunctions.kt new file mode 100644 index 0000000000..08dbfcd157 --- /dev/null +++ b/tools/cliutils/src/main/kotlin/net/corda/cliutils/ExceptionsErrorCodeFunctions.kt @@ -0,0 +1,61 @@ +package net.corda.cliutils + +import org.apache.logging.log4j.Level +import org.apache.logging.log4j.message.Message +import org.apache.logging.log4j.message.SimpleMessage +import java.util.* + +internal fun Message.withErrorCodeFor(error: Throwable?, level: Level): Message { + + return when { + error != null && level.isInRange(Level.FATAL, Level.WARN) -> CompositeMessage("$formattedMessage [errorCode=${error.errorCode()}]", format, parameters, throwable) + else -> this + } +} + +private fun Throwable.errorCode(hashedFields: (Throwable) -> Array<out Any?> = Throwable::defaultHashedFields): String { + + val hash = staticLocationBasedHash(hashedFields) + return hash.toBase(36) +} + +private fun Throwable.staticLocationBasedHash(hashedFields: (Throwable) -> Array<out Any?>, visited: Set<Throwable> = setOf(this)): Int { + + val cause = this.cause + val fields = hashedFields.invoke(this) + return when { + cause != null && !visited.contains(cause) -> Objects.hash(*fields, cause.staticLocationBasedHash(hashedFields, visited + cause)) + else -> Objects.hash(*fields) + } +} + +private fun Int.toBase(base: Int): String = Integer.toUnsignedString(this, base) + +private fun Array<StackTraceElement?>.customHashCode(maxElementsToConsider: Int = this.size): Int { + + return Arrays.hashCode(take(maxElementsToConsider).map { it?.customHashCode() ?: 0 }.toIntArray()) +} + +private fun StackTraceElement.customHashCode(hashedFields: (StackTraceElement) -> Array<out Any?> = StackTraceElement::defaultHashedFields): Int { + + return Objects.hash(*hashedFields.invoke(this)) +} + +private fun Throwable.defaultHashedFields(): Array<out Any?> { + + return arrayOf(this::class.java.name, stackTrace?.customHashCode(3) ?: 0) +} + +private fun StackTraceElement.defaultHashedFields(): Array<out Any?> { + + return arrayOf(className, methodName) +} + +private class CompositeMessage(message: String?, private val formatArg: String?, private val parameters: Array<out Any?>?, private val error: Throwable?) : SimpleMessage(message) { + + override fun getThrowable(): Throwable? = error + + override fun getParameters(): Array<out Any?>? = parameters + + override fun getFormat(): String? = formatArg +} \ No newline at end of file diff --git a/tools/cliutils/src/main/kotlin/net/corda/cliutils/ExitCodes.kt b/tools/cliutils/src/main/kotlin/net/corda/cliutils/ExitCodes.kt new file mode 100644 index 0000000000..cad83f65b3 --- /dev/null +++ b/tools/cliutils/src/main/kotlin/net/corda/cliutils/ExitCodes.kt @@ -0,0 +1,8 @@ +package net.corda.cliutils + +open class ExitCodes { + companion object { + const val SUCCESS: Int = 0 + const val FAILURE: Int = 1 + } +} \ No newline at end of file diff --git a/tools/cliutils/src/main/kotlin/net/corda/cliutils/InstallShellExtensionsParser.kt b/tools/cliutils/src/main/kotlin/net/corda/cliutils/InstallShellExtensionsParser.kt index bfbf95ea44..cd271e1014 100644 --- a/tools/cliutils/src/main/kotlin/net/corda/cliutils/InstallShellExtensionsParser.kt +++ b/tools/cliutils/src/main/kotlin/net/corda/cliutils/InstallShellExtensionsParser.kt @@ -53,7 +53,14 @@ private class ShellExtensionsGenerator(val alias: String, val className: String) } private val userHome: Path by lazy { Paths.get(System.getProperty("user.home")) } - private val jarLocation: Path by lazy { this.javaClass.location.toPath() } + private val jarLocation: Path by lazy { + val capsuleJarProperty = System.getProperty("capsule.jar") + if (capsuleJarProperty != null) { + Paths.get(capsuleJarProperty) + } else { + this.javaClass.location.toPath() + } + } // If on Windows, Path.toString() returns a path with \ instead of /, but for bash Windows users we want to convert those back to /'s private fun Path.toStringWithDeWindowsfication(): String = this.toAbsolutePath().toString().replace("\\", "/") @@ -114,7 +121,6 @@ private class ShellExtensionsGenerator(val alias: String, val className: String) } } -@CommandLine.Command(description = [""]) class InstallShellExtensionsParser { @CommandLine.Option(names = ["--install-shell-extensions"], description = ["Install alias and autocompletion for bash and zsh"]) var installShellExtensions: Boolean = false diff --git a/tools/cliutils/src/main/resources/log4j2.component.properties b/tools/cliutils/src/main/resources/log4j2.component.properties new file mode 100644 index 0000000000..dab7dd2d32 --- /dev/null +++ b/tools/cliutils/src/main/resources/log4j2.component.properties @@ -0,0 +1 @@ +log4j.configurationFactory=net.corda.cliutils.CordaLog4j2ConfigFactory \ No newline at end of file diff --git a/tools/demobench/build.gradle b/tools/demobench/build.gradle index d62249294a..c22547dbea 100644 --- a/tools/demobench/build.gradle +++ b/tools/demobench/build.gradle @@ -1,22 +1,15 @@ -buildscript { - ext { - tornadofx_version = '1.7.15' - jna_version = '4.1.0' - purejavacomm_version = '0.0.18' - controlsfx_version = '8.40.12' +ext { + tornadofx_version = '1.7.15' + jna_version = '4.1.0' + purejavacomm_version = '0.0.18' + controlsfx_version = '8.40.12' - java_home = System.properties.'java.home' - pkg_source = "$buildDir/packagesrc" - pkg_outDir = "$buildDir/javapackage" - dist_source = "$pkg_source/demobench-$version" - pkg_version = version.indexOf('-') >= 0 ? version.substring(0, version.indexOf('-')) : version - pkg_macosxKeyUserName = 'R3CEV' - } - - repositories { - mavenLocal() - mavenCentral() - } + java_home = System.properties.'java.home' + pkg_source = "$buildDir/packagesrc" + pkg_outDir = "$buildDir/javapackage" + dist_source = "$pkg_source/demobench-$version" + pkg_version = version.indexOf('-') >= 0 ? version.substring(0, version.indexOf('-')) : version + pkg_macosxKeyUserName = 'R3CEV' } apply plugin: 'java' @@ -84,12 +77,16 @@ dependencies { testCompile "org.assertj:assertj-core:$assertj_version" } +tasks.withType(JavaCompile) { + // Resolves a Gradle warning about not scanning for pre-processors. + options.compilerArgs << '-proc:none' +} + jar { manifest { attributes( 'Main-Class': mainClassName, - 'Class-Path': configurations.runtime.collect { it.getName() }.join(' '), - 'Automatic-Module-Name': 'net.corda.tools.demobench' + 'Class-Path': configurations.runtimeClasspath.collect { it.name }.join(' '), ) } } diff --git a/tools/demobench/src/main/java/net/corda/demobench/config/LoggingConfig.java b/tools/demobench/src/main/java/net/corda/demobench/config/LoggingConfig.java index 55e8369df1..181ea09d7c 100644 --- a/tools/demobench/src/main/java/net/corda/demobench/config/LoggingConfig.java +++ b/tools/demobench/src/main/java/net/corda/demobench/config/LoggingConfig.java @@ -10,6 +10,7 @@ import java.util.logging.*; * to be added to the JVM's command line. */ public class LoggingConfig { + private static final String LOGGING_CONFIG = "logging.properties"; public LoggingConfig() throws IOException { try (InputStream input = getLoggingProperties()) { @@ -20,10 +21,11 @@ public class LoggingConfig { private static InputStream getLoggingProperties() throws IOException { ClassLoader classLoader = LoggingConfig.class.getClassLoader(); - InputStream input = classLoader.getResourceAsStream("logging.properties"); + InputStream input = classLoader.getResourceAsStream(LOGGING_CONFIG); if (input == null) { - Path javaHome = Paths.get(System.getProperty("java.home")); - input = Files.newInputStream(javaHome.resolve("lib").resolve("logging.properties")); + // Use the default JUL logging configuration properties instead. + Path logging = Paths.get(System.getProperty("java.home"), "lib", LOGGING_CONFIG); + input = Files.newInputStream(logging, StandardOpenOption.READ); } return input; } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt index aa25cc163c..05e1bdc3e3 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt @@ -146,7 +146,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { notaries = listOf(NotaryInfo(identity, config.nodeConfig.notary!!.validating)), modifiedTime = Instant.now(), maxMessageSize = 10485760, - maxTransactionSize = Int.MAX_VALUE, + maxTransactionSize = 10485760, epoch = 1, whitelistedContractImplementations = emptyMap() )) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/ui/PropertyLabel.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/ui/PropertyLabel.kt index c591ea9f77..87fa12d754 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/ui/PropertyLabel.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/ui/PropertyLabel.kt @@ -1,30 +1,41 @@ package net.corda.demobench.ui +import javafx.scene.control.ContentDisplay import javafx.scene.control.Label +import javafx.scene.control.Tooltip import javafx.scene.layout.HBox class PropertyLabel : HBox() { + private val nameLabel = Label() + private val myTooltip = Tooltip() - val nameLabel = Label() - val valueLabel = Label() + private var nameText = "" + private var valueText = "" var name: String - get() = nameLabel.text + get() = nameText set(value) { - nameLabel.text = value + nameText = value + updateText() } var value: String - get() = valueLabel.text + get() = valueText set(value) { - valueLabel.text = value + valueText = value + updateText() } + private fun updateText() { + nameLabel.text = "$nameText $valueText" + myTooltip.text = "$nameText $valueText" + } + init { nameLabel.styleClass.add("property-name") - valueLabel.styleClass.add("property-value") - - children.addAll(nameLabel, valueLabel) + myTooltip.contentDisplay = ContentDisplay.CENTER + Tooltip.install(nameLabel, myTooltip) + children.addAll(nameLabel) styleClass.add("property-label") } } diff --git a/tools/demobench/src/main/resources/net/corda/demobench/views/NodeTerminalView.fxml b/tools/demobench/src/main/resources/net/corda/demobench/views/NodeTerminalView.fxml index b83cd543a5..ef8a2907ac 100644 --- a/tools/demobench/src/main/resources/net/corda/demobench/views/NodeTerminalView.fxml +++ b/tools/demobench/src/main/resources/net/corda/demobench/views/NodeTerminalView.fxml @@ -5,10 +5,10 @@ <?import net.corda.demobench.ui.*?> <VBox visible="false" prefHeight="953.0" prefWidth="1363.0" xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1" styleClass="terminal-vbox"> <HBox fx:id="header" disable="true" prefHeight="95.0" prefWidth="800.0" styleClass="header"> - <VBox prefHeight="66.0" HBox.hgrow="ALWAYS"> + <VBox prefHeight="66.0" HBox.hgrow="SOMETIMES"> <Label fx:id="nodeName" style="-fx-font-size: 40; -fx-text-fill: red;"/> </VBox> - <VBox prefHeight="93.0" prefWidth="267.0"> + <VBox prefHeight="93.0" prefWidth="267.0" HBox.hgrow="SOMETIMES"> <PropertyLabel fx:id="states" name="States in vault: "/> <PropertyLabel fx:id="transactions" name="Known transactions: "/> <PropertyLabel fx:id="balance" name="Balance: "/> diff --git a/tools/explorer/build.gradle b/tools/explorer/build.gradle index 065625a789..fe52056591 100644 --- a/tools/explorer/build.gradle +++ b/tools/explorer/build.gradle @@ -1,7 +1,3 @@ -repositories { - mavenCentral() -} - apply plugin: 'java' apply plugin: 'kotlin' apply plugin: 'application' @@ -61,6 +57,11 @@ dependencies { compile 'com.yuvimasory:orange-extensions:1.3.0' } +tasks.withType(JavaCompile) { + // Resolves a Gradle warning about not scanning for pre-processors. + options.compilerArgs << '-proc:none' +} + task runDemoNodes(dependsOn: 'classes', type: JavaExec) { main = 'net.corda.explorer.MainKt' classpath = sourceSets.main.runtimeClasspath diff --git a/tools/explorer/capsule/build.gradle b/tools/explorer/capsule/build.gradle index aba8ca33ff..e85b9915de 100644 --- a/tools/explorer/capsule/build.gradle +++ b/tools/explorer/capsule/build.gradle @@ -8,16 +8,7 @@ apply plugin: 'com.jfrog.artifactory' description 'Node Explorer' configurations { - runtimeArtifacts.extendsFrom runtime -} - -repositories { - mavenLocal() - mavenCentral() - maven { - url 'http://oss.sonatype.org/content/repositories/snapshots' - } - jcenter() + runtimeArtifacts.extendsFrom runtimeClasspath } // Force the Caplet to target Java 6. This ensures that running 'java -jar explorer.jar' on any Java 6 VM upwards @@ -28,12 +19,16 @@ repositories { sourceCompatibility = 1.6 targetCompatibility = 1.6 -task buildExplorerJAR(type: FatCapsule, dependsOn: project(':tools:explorer').compileJava) { +capsule { + version capsule_version +} + +task buildExplorerJAR(type: FatCapsule, dependsOn: project(':tools:explorer').tasks.jar) { applicationClass 'net.corda.explorer.Main' archiveName "node-explorer-${corda_release_version}.jar" applicationSource = files( - project(':tools:explorer').configurations.runtime, - project(':tools:explorer').jar, + project(':tools:explorer').configurations.runtimeClasspath, + project(':tools:explorer').tasks.jar, project(':tools:explorer').sourceSets.main.java.outputDir.toString() + '/ExplorerCaplet.class' ) classifier 'fat' @@ -63,6 +58,7 @@ artifacts { jar { classifier "ignore" + enabled = false } publish { diff --git a/tools/network-bootstrapper/build.gradle b/tools/network-bootstrapper/build.gradle index ef2eab205d..674b97f229 100644 --- a/tools/network-bootstrapper/build.gradle +++ b/tools/network-bootstrapper/build.gradle @@ -1,26 +1,24 @@ -buildscript { - - ext.tornadofx_version = '1.7.15' - ext.controlsfx_version = '8.40.12' - - - repositories { - mavenLocal() - mavenCentral() - jcenter() - } - - dependencies { - classpath "com.github.jengelman.gradle.plugins:shadow:$shadow_version" - } +ext { + tornadofx_version = '1.7.15' + controlsfx_version = '8.40.12' } apply plugin: 'kotlin' apply plugin: 'idea' apply plugin: 'java' apply plugin: 'application' +// We need to set mainClassName before applying the shadow plugin. +mainClassName = 'net.corda.bootstrapper.Main' + apply plugin: 'com.github.johnrengelman.shadow' +configurations { + compile { + exclude group: "log4j", module: "log4j" + exclude group: "org.apache.logging.log4j" + } +} + dependencies { compile "com.microsoft.azure:azure:1.8.0" compile "com.github.docker-java:docker-java:3.0.6" @@ -31,11 +29,11 @@ dependencies { compile project(':node-api') compile project(':node') - compile group: "com.typesafe", name: "config", version: typesafe_config_version - compile group: "com.fasterxml.jackson.dataformat", name: "jackson-dataformat-yaml", version: "2.9.0" - compile group: "com.fasterxml.jackson.core", name: "jackson-databind", version: "2.9.0" - compile "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.+" - compile group: 'info.picocli', name: 'picocli', version: '3.0.1' + compile "com.typesafe:config:$typesafe_config_version" + compile "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jackson_version" + compile "com.fasterxml.jackson.core:jackson-databind:$jackson_version" + compile "com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version" + compile "info.picocli:picocli:$picocli_version" // TornadoFX: A lightweight Kotlin framework for working with JavaFX UI's. compile "no.tornado:tornadofx:$tornadofx_version" @@ -44,18 +42,19 @@ dependencies { compile "org.controlsfx:controlsfx:$controlsfx_version" } +tasks.withType(JavaCompile) { + // Resolves a Gradle warning about not scanning for pre-processors. + options.compilerArgs << '-proc:none' +} + +jar.enabled = false + shadowJar { baseName = 'network-bootstrapper' classifier = null version = null zip64 true - mainClassName = 'net.corda.bootstrapper.Main' } -task buildNetworkBootstrapper(dependsOn: shadowJar) { -} - -configurations { - compile.exclude group: "log4j", module: "log4j" - compile.exclude group: "org.apache.logging.log4j" -} \ No newline at end of file +task buildNetworkBootstrapper(dependsOn: shadowJar) +assemble.dependsOn buildNetworkBootstrapper diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/serialization/SerializationHelper.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/serialization/SerializationHelper.kt index 8e350c8c14..ce978e4131 100644 --- a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/serialization/SerializationHelper.kt +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/serialization/SerializationHelper.kt @@ -3,6 +3,7 @@ package net.corda.bootstrapper.serialization import net.corda.core.serialization.internal.SerializationEnvironmentImpl import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.node.serialization.amqp.AMQPServerSerializationScheme +import net.corda.node.serialization.kryo.KRYO_CHECKPOINT_CONTEXT import net.corda.serialization.internal.AMQP_P2P_CONTEXT import net.corda.serialization.internal.AMQP_STORAGE_CONTEXT import net.corda.serialization.internal.SerializationFactoryImpl @@ -20,7 +21,7 @@ class SerializationEngine { p2pContext = AMQP_P2P_CONTEXT.withClassLoader(classloader), rpcServerContext = AMQP_P2P_CONTEXT.withClassLoader(classloader), storageContext = AMQP_STORAGE_CONTEXT.withClassLoader(classloader), - checkpointContext = AMQP_P2P_CONTEXT.withClassLoader(classloader) + checkpointContext = KRYO_CHECKPOINT_CONTEXT.withClassLoader(classloader) ) } } diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/volumes/Volume.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/volumes/Volume.kt index 4ec5d47f02..c65fa79d27 100644 --- a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/volumes/Volume.kt +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/volumes/Volume.kt @@ -46,7 +46,7 @@ interface Volume { minimumPlatformVersion = 1, notaries = it, maxMessageSize = 10485760, - maxTransactionSize = Int.MAX_VALUE, + maxTransactionSize = 10485760, modifiedTime = Instant.now(), epoch = 10, whitelistedContractImplementations = emptyMap()) diff --git a/tools/shell-cli/README.md b/tools/shell-cli/README.md index c6a00181cd..131fa0ba66 100644 --- a/tools/shell-cli/README.md +++ b/tools/shell-cli/README.md @@ -1,7 +1,7 @@ Standalone Shell ---------------- -Documentation for shell CLI can be found [here](http://docs.corda.net/website/releases/docs_head/shell.html) +Documentation for the standalone shell can be found [here](https://docs.corda.net/head/shell.html#the-standalone-shell) To build this from the command line on Unix or MacOS: diff --git a/tools/shell-cli/build.gradle b/tools/shell-cli/build.gradle index 45513f5f89..bb1b7b6bb9 100644 --- a/tools/shell-cli/build.gradle +++ b/tools/shell-cli/build.gradle @@ -1,45 +1,40 @@ description 'Corda Shell CLI' -buildscript { - repositories { - maven { - url "https://plugins.gradle.org/m2/" - } - } - dependencies { - classpath "com.github.jengelman.gradle.plugins:shadow:$shadow_version" - } -} - apply plugin: 'application' +// We need to set mainClassName before applying the shadow plugin. +mainClassName = 'net.corda.tools.shell.StandaloneShellKt' + apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'com.jfrog.artifactory' -mainClassName = 'net.corda.tools.shell.StandaloneShellKt' - dependencies { compile project(':tools:shell') - compile group: 'org.slf4j', name: 'slf4j-simple', version: slf4j_version + compile project(':tools:cliutils') + compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" + compile "org.slf4j:jul-to-slf4j:$slf4j_version" testCompile(project(':test-utils')) { exclude group: 'org.apache.logging.log4j', module: 'log4j-slf4j-impl' } + + testCompile(project(':test-cli')) } processResources { from file("$rootDir/config/dev/log4j2.xml") } -shadowJar { +shadowJar { mergeServiceFiles() } task buildShellCli(dependsOn: shadowJar) +assemble.dependsOn buildShellCli artifacts { publish shadowJar { - classifier "" + classifier = "" } } diff --git a/tools/shell-cli/src/main/kotlin/net/corda/tools/shell/ShellCmdLineOptions.kt b/tools/shell-cli/src/main/kotlin/net/corda/tools/shell/ShellCmdLineOptions.kt new file mode 100644 index 0000000000..35da8b0cf0 --- /dev/null +++ b/tools/shell-cli/src/main/kotlin/net/corda/tools/shell/ShellCmdLineOptions.kt @@ -0,0 +1,195 @@ +package net.corda.tools.shell + +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import net.corda.core.internal.div +import net.corda.core.messaging.ClientRpcSslOptions +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.nodeapi.internal.config.parseAs +import net.corda.tools.shell.ShellConfiguration.Companion.COMMANDS_DIR +import picocli.CommandLine.Option +import java.nio.file.Path +import java.nio.file.Paths + +class ShellCmdLineOptions { + @Option( + names = ["-f", "--config-file"], + description = ["The path to the shell configuration file, used instead of providing the rest of command line options."] + ) + var configFile: Path? = null + + @Option( + names = ["-c", "--cordapp-directory"], + description = ["The path to the directory containing CorDapp JARs, CorDapps are required when starting flows."] + ) + var cordappDirectory: Path? = null + + @Option( + names = ["-o", "--commands-directory"], + description = ["The path to the directory containing additional CRaSH shell commands."] + ) + var commandsDirectory: Path? = null + + @Option( + names = ["-a", "--host"], + description = ["The host address of the Corda node."] + ) + var host: String? = null + + @Option( + names = ["-p", "--port"], + description = ["The RPC port of the Corda node."] + ) + var port: String? = null + + @Option( + names = ["--user"], + description = ["The RPC user name."] + ) + var user: String? = null + + @Option( + names = ["--password"], + description = ["The RPC user password."] + ) + var password: String? = null + + + @Option( + names = ["--sshd-port"], + description = ["Enables SSH server for shell."] + ) + var sshdPort: String? = null + + @Option( + names = ["--sshd-hostkey-directory"], + description = ["The directory with hostkey.pem file for SSH server."] + ) + var sshdHostKeyDirectory: Path? = null + + @Option( + names = ["--truststore-password"], + description = ["The password to unlock the TrustStore file."] + ) + var trustStorePassword: String? = null + + + @Option( + names = ["--truststore-file"], + description = ["The path to the TrustStore file."] + ) + var trustStoreFile: Path? = null + + + @Option( + names = ["--truststore-type"], + description = ["The type of the TrustStore (e.g. JKS)."] + ) + var trustStoreType: String? = null + + + private fun toConfigFile(): Config { + val cmdOpts = mutableMapOf<String, Any?>() + + commandsDirectory?.apply { cmdOpts["extensions.commands.path"] = this.toString() } + cordappDirectory?.apply { cmdOpts["extensions.cordapps.path"] = this.toString() } + user?.apply { cmdOpts["node.user"] = this } + password?.apply { cmdOpts["node.password"] = this } + host?.apply { cmdOpts["node.addresses.rpc.host"] = this } + port?.apply { cmdOpts["node.addresses.rpc.port"] = this } + trustStoreFile?.apply { cmdOpts["ssl.truststore.path"] = this.toString() } + trustStorePassword?.apply { cmdOpts["ssl.truststore.password"] = this } + trustStoreType?.apply { cmdOpts["ssl.truststore.type"] = this } + sshdPort?.apply { + cmdOpts["extensions.sshd.port"] = this + cmdOpts["extensions.sshd.enabled"] = true + } + sshdHostKeyDirectory?.apply { cmdOpts["extensions.sshd.hostkeypath"] = this.toString() } + + return ConfigFactory.parseMap(cmdOpts) + } + + /** Return configuration parsed from an optional config file (provided by the command line option) + * and then overridden by the command line options */ + fun toConfig(): ShellConfiguration { + val fileConfig = configFile?.let { ConfigFactory.parseFile(it.toFile()) } + ?: ConfigFactory.empty() + val typeSafeConfig = toConfigFile().withFallback(fileConfig).resolve() + val shellConfigFile = typeSafeConfig.parseAs<ShellConfigurationFile.ShellConfigFile>() + return shellConfigFile.toShellConfiguration() + } +} + +/** Object representation of Shell configuration file */ +private class ShellConfigurationFile { + data class Rpc( + val host: String, + val port: Int) + + data class Addresses( + val rpc: Rpc + ) + + data class Node( + val addresses: Addresses, + val user: String?, + val password: String? + ) + + data class Cordapps( + val path: String + ) + + data class Sshd( + val enabled: Boolean, + val port: Int, + val hostkeypath: String? + ) + + data class Commands( + val path: String + ) + + data class Extensions( + val cordapps: Cordapps?, + val sshd: Sshd?, + val commands: Commands? + ) + + data class KeyStore( + val path: String, + val type: String = "JKS", + val password: String + ) + + data class Ssl( + val truststore: KeyStore + ) + + data class ShellConfigFile( + val node: Node, + val extensions: Extensions?, + val ssl: Ssl? + ) { + fun toShellConfiguration(): ShellConfiguration { + + val sslOptions = + ssl?.let { + ClientRpcSslOptions( + trustStorePath = Paths.get(it.truststore.path), + trustStorePassword = it.truststore.password) + } + + return ShellConfiguration( + commandsDirectory = extensions?.commands?.let { Paths.get(it.path) } ?: Paths.get(".") + / COMMANDS_DIR, + cordappsDirectory = extensions?.cordapps?.let { Paths.get(it.path) }, + user = node.user ?: "", + password = node.password ?: "", + hostAndPort = NetworkHostAndPort(node.addresses.rpc.host, node.addresses.rpc.port), + ssl = sslOptions, + sshdPort = extensions?.sshd?.let { if (it.enabled) it.port else null }, + sshHostKeyDirectory = extensions?.sshd?.let { if (it.enabled && it.hostkeypath != null) Paths.get(it.hostkeypath) else null }) + } + } +} diff --git a/tools/shell-cli/src/main/kotlin/net/corda/tools/shell/StandaloneShell.kt b/tools/shell-cli/src/main/kotlin/net/corda/tools/shell/StandaloneShell.kt index 00f8928b81..8d40c858ea 100644 --- a/tools/shell-cli/src/main/kotlin/net/corda/tools/shell/StandaloneShell.kt +++ b/tools/shell-cli/src/main/kotlin/net/corda/tools/shell/StandaloneShell.kt @@ -1,45 +1,34 @@ package net.corda.tools.shell import com.jcabi.manifests.Manifests -import joptsimple.OptionException -import net.corda.core.internal.* +import net.corda.cliutils.CordaCliWrapper +import net.corda.cliutils.ExitCodes +import net.corda.cliutils.start +import net.corda.core.internal.exists +import net.corda.core.internal.isRegularFile +import net.corda.core.internal.list import org.fusesource.jansi.Ansi import org.fusesource.jansi.AnsiConsole +import org.slf4j.bridge.SLF4JBridgeHandler +import picocli.CommandLine.Mixin +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStreamReader import java.net.URL import java.net.URLClassLoader import java.nio.file.Path import java.util.concurrent.CountDownLatch import kotlin.streams.toList -import java.io.IOException -import java.io.BufferedReader -import java.io.InputStreamReader -import kotlin.system.exitProcess fun main(args: Array<String>) { - - val argsParser = CommandLineOptionParser() - val cmdlineOptions = try { - argsParser.parse(*args) - } catch (e: OptionException) { - println("Invalid command line arguments: ${e.message}") - argsParser.printHelp(System.out) - exitProcess(1) - } - - if (cmdlineOptions.help) { - argsParser.printHelp(System.out) - return - } - val config = try { - cmdlineOptions.toConfig() - } catch(e: Exception) { - println("Configuration exception: ${e.message}") - exitProcess(1) - } - StandaloneShell(config).run() + StandaloneShell().start(args) } -class StandaloneShell(private val configuration: ShellConfiguration) { +class StandaloneShell : CordaCliWrapper("corda-shell", "The Corda standalone shell.") { + @Mixin + var cmdLineOptions = ShellCmdLineOptions() + + lateinit var configuration: ShellConfiguration private fun getCordappsInDirectory(cordappsDir: Path?): List<URL> = if (cordappsDir == null || !cordappsDir.exists()) { @@ -67,7 +56,20 @@ class StandaloneShell(private val configuration: ShellConfiguration) { private fun getManifestEntry(key: String) = if (Manifests.exists(key)) Manifests.read(key) else "Unknown" - fun run() { + override fun initLogging() { + super.initLogging() + SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler. + SLF4JBridgeHandler.install() + } + + override fun runProgram(): Int { + configuration = try { + cmdLineOptions.toConfig() + } catch(e: Exception) { + println("Configuration exception: ${e.message}") + return ExitCodes.FAILURE + } + val cordappJarPaths = getCordappsInDirectory(configuration.cordappsDirectory) val classLoader: ClassLoader = URLClassLoader(cordappJarPaths.toTypedArray(), javaClass.classLoader) with(configuration) { @@ -84,7 +86,7 @@ class StandaloneShell(private val configuration: ShellConfiguration) { InteractiveShell.nodeInfo() } catch (e: Exception) { println("Cannot login to ${configuration.hostAndPort}, reason: \"${e.message}\"") - exitProcess(1) + return ExitCodes.FAILURE } val exit = CountDownLatch(1) @@ -106,6 +108,6 @@ class StandaloneShell(private val configuration: ShellConfiguration) { exit.await() // because we can't clean certain Crash Shell threads that block on read() - exitProcess(0) + return ExitCodes.SUCCESS } } diff --git a/tools/shell-cli/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt b/tools/shell-cli/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt deleted file mode 100644 index 9b628948e9..0000000000 --- a/tools/shell-cli/src/main/kotlin/net/corda/tools/shell/StandaloneShellArgsParser.kt +++ /dev/null @@ -1,207 +0,0 @@ -package net.corda.tools.shell - -import com.typesafe.config.Config -import com.typesafe.config.ConfigFactory -import joptsimple.OptionParser -import joptsimple.util.EnumConverter -import net.corda.core.internal.div -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.messaging.ClientRpcSslOptions -import net.corda.nodeapi.internal.config.parseAs -import net.corda.tools.shell.ShellConfiguration.Companion.COMMANDS_DIR -import org.slf4j.event.Level -import java.io.PrintStream -import java.nio.file.Path -import java.nio.file.Paths - -// NOTE: Do not use any logger in this class as args parsing is done before the logger is setup. -class CommandLineOptionParser { - private val optionParser = OptionParser() - - private val configFileArg = optionParser - .accepts("config-file", "The path to the shell configuration file, used instead of providing the rest of command line options.") - .withOptionalArg() - private val cordappsDirectoryArg = optionParser - .accepts("cordpass-directory", "The path to directory containing Cordapps jars, Cordapps are require when starting flows.") - .withOptionalArg() - private val commandsDirectoryArg = optionParser - .accepts("commands-directory", "The directory with additional CrAsH shell commands.") - .withOptionalArg() - private val hostArg = optionParser - .acceptsAll(listOf("h", "host"), "The host of the Corda node.") - .withRequiredArg() - private val portArg = optionParser - .acceptsAll(listOf("p", "port"), "The port of the Corda node.") - .withRequiredArg() - private val userArg = optionParser - .accepts("user", "The RPC user name.") - .withOptionalArg() - private val passwordArg = optionParser - .accepts("password", "The RPC user password.") - .withOptionalArg() - private val loggerLevel = optionParser - .accepts("logging-level", "Enable logging at this level and higher.") - .withRequiredArg() - .withValuesConvertedBy(object : EnumConverter<Level>(Level::class.java) {}) - .defaultsTo(Level.INFO) - private val sshdPortArg = optionParser - .accepts("sshd-port", "Enables SSH server for shell.") - .withOptionalArg() - private val sshdHostKeyDirectoryArg = optionParser - .accepts("sshd-hostkey-directory", "The directory with hostkey.pem file for SSH server.") - .withOptionalArg() - private val helpArg = optionParser - .accepts("help") - .forHelp() - private val trustStorePasswordArg = optionParser - .accepts("truststore-password", "The password to unlock the TrustStore file.") - .withOptionalArg() - private val trustStoreDirArg = optionParser - .accepts("truststore-file", "The path to the TrustStore file.") - .withOptionalArg() - private val trustStoreTypeArg = optionParser - .accepts("truststore-type", "The type of the TrustStore (e.g. JKS).") - .withOptionalArg() - - fun parse(vararg args: String): CommandLineOptions { - val optionSet = optionParser.parse(*args) - return CommandLineOptions( - configFile = optionSet.valueOf(configFileArg), - host = optionSet.valueOf(hostArg), - port = optionSet.valueOf(portArg), - user = optionSet.valueOf(userArg), - password = optionSet.valueOf(passwordArg), - commandsDirectory = (optionSet.valueOf(commandsDirectoryArg))?.let { Paths.get(it).normalize().toAbsolutePath() }, - cordappsDirectory = (optionSet.valueOf(cordappsDirectoryArg))?.let { Paths.get(it).normalize().toAbsolutePath() }, - help = optionSet.has(helpArg), - loggingLevel = optionSet.valueOf(loggerLevel), - sshdPort = optionSet.valueOf(sshdPortArg), - sshdHostKeyDirectory = (optionSet.valueOf(sshdHostKeyDirectoryArg))?.let { Paths.get(it).normalize().toAbsolutePath() }, - trustStorePassword = optionSet.valueOf(trustStorePasswordArg), - trustStoreFile = (optionSet.valueOf(trustStoreDirArg))?.let { Paths.get(it).normalize().toAbsolutePath() }, - trustStoreType = optionSet.valueOf(trustStoreTypeArg)) - } - - fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink) -} - -data class CommandLineOptions(val configFile: String?, - val commandsDirectory: Path?, - val cordappsDirectory: Path?, - val host: String?, - val port: String?, - val user: String?, - val password: String?, - val help: Boolean, - val loggingLevel: Level, - val sshdPort: String?, - val sshdHostKeyDirectory: Path?, - val trustStorePassword: String?, - val trustStoreFile: Path?, - val trustStoreType: String?) { - - private fun toConfigFile(): Config { - val cmdOpts = mutableMapOf<String, Any?>() - - commandsDirectory?.apply { cmdOpts["extensions.commands.path"] = this.toString() } - cordappsDirectory?.apply { cmdOpts["extensions.cordapps.path"] = this.toString() } - user?.apply { cmdOpts["node.user"] = this } - password?.apply { cmdOpts["node.password"] = this } - host?.apply { cmdOpts["node.addresses.rpc.host"] = this } - port?.apply { cmdOpts["node.addresses.rpc.port"] = this } - trustStoreFile?.apply { cmdOpts["ssl.truststore.path"] = this.toString() } - trustStorePassword?.apply { cmdOpts["ssl.truststore.password"] = this } - trustStoreType?.apply { cmdOpts["ssl.truststore.type"] = this } - sshdPort?.apply { - cmdOpts["extensions.sshd.port"] = this - cmdOpts["extensions.sshd.enabled"] = true - } - sshdHostKeyDirectory?.apply { cmdOpts["extensions.sshd.hostkeypath"] = this.toString() } - - return ConfigFactory.parseMap(cmdOpts) - } - - /** Return configuration parsed from an optional config file (provided by the command line option) - * and then overridden by the command line options */ - fun toConfig(): ShellConfiguration { - val fileConfig = configFile?.let { ConfigFactory.parseFile(Paths.get(configFile).toFile()) } - ?: ConfigFactory.empty() - val typeSafeConfig = toConfigFile().withFallback(fileConfig).resolve() - val shellConfigFile = typeSafeConfig.parseAs<ShellConfigurationFile.ShellConfigFile>() - return shellConfigFile.toShellConfiguration() - } -} - -/** Object representation of Shell configuration file */ -private class ShellConfigurationFile { - data class Rpc( - val host: String, - val port: Int) - - data class Addresses( - val rpc: Rpc - ) - - data class Node( - val addresses: Addresses, - val user: String?, - val password: String? - ) - - data class Cordapps( - val path: String - ) - - data class Sshd( - val enabled: Boolean, - val port: Int, - val hostkeypath: String? - ) - - data class Commands( - val path: String - ) - - data class Extensions( - val cordapps: Cordapps, - val sshd: Sshd, - val commands: Commands? - ) - - data class KeyStore( - val path: String, - val type: String = "JKS", - val password: String - ) - - data class Ssl( - val truststore: KeyStore - ) - - data class ShellConfigFile( - val node: Node, - val extensions: Extensions?, - val ssl: Ssl? - ) { - fun toShellConfiguration(): ShellConfiguration { - - val sslOptions = - ssl?.let { - ClientRpcSslOptions( - trustStorePath = Paths.get(it.truststore.path), - trustStorePassword = it.truststore.password) - } - - return ShellConfiguration( - commandsDirectory = extensions?.commands?.let { Paths.get(it.path) } ?: Paths.get(".") - / COMMANDS_DIR, - cordappsDirectory = extensions?.cordapps?.let { Paths.get(it.path) }, - user = node.user ?: "", - password = node.password ?: "", - hostAndPort = NetworkHostAndPort(node.addresses.rpc.host, node.addresses.rpc.port), - ssl = sslOptions, - sshdPort = extensions?.sshd?.let { if (it.enabled) it.port else null }, - sshHostKeyDirectory = extensions?.sshd?.let { if (it.enabled && it.hostkeypath != null) Paths.get(it.hostkeypath) else null }) - } - } -} diff --git a/tools/shell-cli/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt b/tools/shell-cli/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt index d1817a822a..8563e65f26 100644 --- a/tools/shell-cli/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt +++ b/tools/shell-cli/src/test/kotlin/net/corda/tools/shell/StandaloneShellArgsParserTest.kt @@ -1,97 +1,44 @@ package net.corda.tools.shell import net.corda.core.internal.toPath -import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.messaging.ClientRpcSslOptions -import org.assertj.core.api.Assertions.assertThat +import net.corda.core.utilities.NetworkHostAndPort import org.junit.Test -import org.slf4j.event.Level import java.nio.file.Paths import kotlin.test.assertEquals class StandaloneShellArgsParserTest { private val CONFIG_FILE = StandaloneShellArgsParserTest::class.java.getResource("/config.conf").toPath() - @Test - fun args_to_cmd_options() { - val args = arrayOf("--config-file", "/x/y/z/config.conf", - "--commands-directory", "/x/y/commands", - "--cordpass-directory", "/x/y/cordapps", - "--host", "alocalhost", - "--port", "1234", - "--user", "demo", - "--password", "abcd1234", - "--logging-level", "DEBUG", - "--sshd-port", "2223", - "--sshd-hostkey-directory", "/x/y/ssh", - "--help", - "--truststore-password", "pass2", - "--truststore-file", "/x/y/truststore.jks", - "--truststore-type", "dummy") - - val expectedOptions = CommandLineOptions( - configFile = "/x/y/z/config.conf", - commandsDirectory = Paths.get("/x/y/commands").normalize().toAbsolutePath(), - cordappsDirectory = Paths.get("/x/y/cordapps").normalize().toAbsolutePath(), - host = "alocalhost", - port = "1234", - user = "demo", - password = "abcd1234", - help = true, - loggingLevel = Level.DEBUG, - sshdPort = "2223", - sshdHostKeyDirectory = Paths.get("/x/y/ssh").normalize().toAbsolutePath(), - trustStorePassword = "pass2", - trustStoreFile = Paths.get("/x/y/truststore.jks").normalize().toAbsolutePath(), - trustStoreType = "dummy") - - val options = CommandLineOptionParser().parse(*args) - - assertThat(options).isEqualTo(expectedOptions) - } - @Test fun empty_args_to_cmd_options() { - val args = emptyArray<String>() + val expectedOptions = ShellCmdLineOptions() - val expectedOptions = CommandLineOptions(configFile = null, - commandsDirectory = null, - cordappsDirectory = null, - host = null, - port = null, - user = null, - password = null, - help = false, - loggingLevel = Level.INFO, - sshdPort = null, - sshdHostKeyDirectory = null, - trustStorePassword = null, - trustStoreFile = null, - trustStoreType = null) - - val options = CommandLineOptionParser().parse(*args) - - assertEquals(expectedOptions, options) + assertEquals(expectedOptions.configFile, null) + assertEquals(expectedOptions.cordappDirectory, null) + assertEquals(expectedOptions.commandsDirectory, null) + assertEquals(expectedOptions.host, null) + assertEquals(expectedOptions.port, null) + assertEquals(expectedOptions.user, null) + assertEquals(expectedOptions.password, null) + assertEquals(expectedOptions.sshdPort, null) } @Test fun args_to_config() { - - val options = CommandLineOptions(configFile = null, - commandsDirectory = Paths.get("/x/y/commands"), - cordappsDirectory = Paths.get("/x/y/cordapps"), - host = "alocalhost", - port = "1234", - user = "demo", - password = "abcd1234", - help = true, - loggingLevel = Level.DEBUG, - sshdPort = "2223", - sshdHostKeyDirectory = Paths.get("/x/y/ssh"), - trustStorePassword = "pass2", - trustStoreFile = Paths.get("/x/y/truststore.jks"), - trustStoreType = "dummy" - ) + val options = ShellCmdLineOptions() + options.configFile = null + options.commandsDirectory = Paths.get("/x/y/commands") + options.cordappDirectory = Paths.get("/x/y/cordapps") + options.host = "alocalhost" + options.port = "1234" + options.user = "demo" + options.password = "abcd1234" + options.sshdPort = "2223" + options.sshdHostKeyDirectory = Paths.get("/x/y/ssh") + options.trustStorePassword = "pass2" + options.trustStoreFile = Paths.get("/x/y/truststore.jks") + options.trustStoreType = "dummy" val expectedSsl = ClientRpcSslOptions( trustStorePath = Paths.get("/x/y/truststore.jks"), @@ -114,21 +61,19 @@ class StandaloneShellArgsParserTest { @Test fun cmd_options_to_config_from_file() { - - val options = CommandLineOptions(configFile = CONFIG_FILE.toString(), - commandsDirectory = null, - cordappsDirectory = null, - host = null, - port = null, - user = null, - password = null, - help = false, - loggingLevel = Level.DEBUG, - sshdPort = null, - sshdHostKeyDirectory = null, - trustStorePassword = null, - trustStoreFile = null, - trustStoreType = null) + val options = ShellCmdLineOptions() + options.configFile = CONFIG_FILE + options.commandsDirectory = null + options.cordappDirectory = null + options.host = null + options.port = null + options.user = null + options.password = null + options.sshdPort = null + options.sshdHostKeyDirectory = null + options.trustStorePassword = null + options.trustStoreFile = null + options.trustStoreType = null val expectedConfig = ShellConfiguration( commandsDirectory = Paths.get("/x/y/commands"), @@ -148,21 +93,18 @@ class StandaloneShellArgsParserTest { @Test fun cmd_options_override_config_from_file() { - - val options = CommandLineOptions(configFile = CONFIG_FILE.toString(), - commandsDirectory = null, - cordappsDirectory = null, - host = null, - port = null, - user = null, - password = "blabla", - help = false, - loggingLevel = Level.DEBUG, - sshdPort = null, - sshdHostKeyDirectory = null, - trustStorePassword = null, - trustStoreFile = null, - trustStoreType = null) + val options = ShellCmdLineOptions() + options.configFile = CONFIG_FILE + options.commandsDirectory = null + options.host = null + options.port = null + options.user = null + options.password = "blabla" + options.sshdPort = null + options.sshdHostKeyDirectory = null + options.trustStorePassword = null + options.trustStoreFile = null + options.trustStoreType = null val expectedSsl = ClientRpcSslOptions( trustStorePath = Paths.get("/x/y/truststore.jks"), diff --git a/tools/shell-cli/src/test/kotlin/net/corda/tools/shell/StandaloneShellCompatibilityTest.kt b/tools/shell-cli/src/test/kotlin/net/corda/tools/shell/StandaloneShellCompatibilityTest.kt new file mode 100644 index 0000000000..3f56ef218f --- /dev/null +++ b/tools/shell-cli/src/test/kotlin/net/corda/tools/shell/StandaloneShellCompatibilityTest.kt @@ -0,0 +1,6 @@ +package net.corda.tools.shell + +import net.corda.testing.CliBackwardsCompatibleTest + +class StandaloneShellCompatibilityTest : CliBackwardsCompatibleTest(StandaloneShell::class.java) + diff --git a/tools/shell-cli/src/test/resources/net.corda.tools.shell.StandaloneShell.yml b/tools/shell-cli/src/test/resources/net.corda.tools.shell.StandaloneShell.yml new file mode 100644 index 0000000000..ce5ec0bab1 --- /dev/null +++ b/tools/shell-cli/src/test/resources/net.corda.tools.shell.StandaloneShell.yml @@ -0,0 +1,119 @@ +- commandName: "<main class>" + positionalParams: [] + params: + - parameterName: "--commands-directory" + parameterType: "java.nio.file.Path" + required: false + multiParam: true + acceptableValues: [] + - parameterName: "--config-file" + parameterType: "java.nio.file.Path" + required: false + multiParam: true + acceptableValues: [] + - parameterName: "--cordapp-directory" + parameterType: "java.nio.file.Path" + required: false + multiParam: true + acceptableValues: [] + - parameterName: "--host" + parameterType: "java.lang.String" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--install-shell-extensions" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--log-to-console" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--logging-level" + parameterType: "org.slf4j.event.Level" + required: false + multiParam: false + acceptableValues: + - "ERROR" + - "WARN" + - "INFO" + - "DEBUG" + - "TRACE" + - parameterName: "--password" + parameterType: "java.lang.String" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--port" + parameterType: "java.lang.String" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--sshd-hostkey-directory" + parameterType: "java.nio.file.Path" + required: false + multiParam: true + acceptableValues: [] + - parameterName: "--sshd-port" + parameterType: "java.lang.String" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--truststore-file" + parameterType: "java.nio.file.Path" + required: false + multiParam: true + acceptableValues: [] + - parameterName: "--truststore-password" + parameterType: "java.lang.String" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--truststore-type" + parameterType: "java.lang.String" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--user" + parameterType: "java.lang.String" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--verbose" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "-a" + parameterType: "java.lang.String" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "-c" + parameterType: "java.nio.file.Path" + required: false + multiParam: true + acceptableValues: [] + - parameterName: "-f" + parameterType: "java.nio.file.Path" + required: false + multiParam: true + acceptableValues: [] + - parameterName: "-o" + parameterType: "java.nio.file.Path" + required: false + multiParam: true + acceptableValues: [] + - parameterName: "-p" + parameterType: "java.lang.String" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "-v" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + diff --git a/tools/shell/build.gradle b/tools/shell/build.gradle index 219fdaa055..13338150fc 100644 --- a/tools/shell/build.gradle +++ b/tools/shell/build.gradle @@ -61,24 +61,25 @@ dependencies { compile "com.jcabi:jcabi-manifests:1.1" // For logging, required for ANSIProgressRenderer. - compile "org.apache.logging.log4j:log4j-core:${log4j_version}" + compile "org.apache.logging.log4j:log4j-core:$log4j_version" // Unit testing helpers. testCompile "junit:junit:$junit_version" - testCompile "org.assertj:assertj-core:${assertj_version}" + testCompile "org.assertj:assertj-core:$assertj_version" testCompile project(':test-utils') testCompile project(':finance') - // Integration test helpers. - integrationTestCompile "junit:junit:$junit_version" - integrationTestCompile "org.assertj:assertj-core:${assertj_version}" - // Jsh: Testing SSH server. integrationTestCompile "com.jcraft:jsch:$jsch_version" integrationTestCompile project(':node-driver') } +tasks.withType(JavaCompile) { + // Resolves a Gradle warning about not scanning for pre-processors. + options.compilerArgs << '-proc:none' +} + task integrationTest(type: Test) { testClassesDirs = sourceSets.integrationTest.output.classesDirs classpath = sourceSets.integrationTest.runtimeClasspath diff --git a/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt b/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt index 8d382d2604..f596598b52 100644 --- a/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt +++ b/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt @@ -3,10 +3,16 @@ package net.corda.tools.shell import com.google.common.io.Files import com.jcraft.jsch.ChannelExec import com.jcraft.jsch.JSch +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.doAnswer +import com.nhaarman.mockito_kotlin.mock import net.corda.client.rpc.RPCException +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC import net.corda.core.internal.div import net.corda.core.messaging.ClientRpcSslOptions import net.corda.core.messaging.CordaRPCOps +import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.getOrThrow import net.corda.node.services.Permissions import net.corda.node.services.Permissions.Companion.all @@ -21,10 +27,12 @@ import net.corda.testing.driver.driver import net.corda.testing.driver.internal.NodeHandleInternal import net.corda.testing.internal.useSslRpcOverrides import net.corda.testing.node.User +import net.corda.tools.shell.utlities.ANSIProgressRenderer import org.apache.activemq.artemis.api.core.ActiveMQSecurityException import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.bouncycastle.util.io.Streams +import org.crsh.text.RenderPrintWriter import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -131,7 +139,7 @@ class InteractiveShellIntegrationTest { driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { startNode().getOrThrow().use { node -> val conf = (node as NodeHandleInternal).configuration.toShellConfig() - InteractiveShell.startShellInternal(conf) + InteractiveShell.startShell(conf) assertThatThrownBy { InteractiveShell.nodeInfo() }.isInstanceOf(ActiveMQSecurityException::class.java) } } @@ -236,4 +244,159 @@ class InteractiveShellIntegrationTest { } } + + @Suppress("UNUSED") + @StartableByRPC + class NoOpFlow : FlowLogic<Unit>() { + override val progressTracker = ProgressTracker() + override fun call() { + println("NO OP!") + } + } + + @Suppress("UNUSED") + @StartableByRPC + class NoOpFlowA : FlowLogic<Unit>() { + override val progressTracker = ProgressTracker() + override fun call() { + println("NO OP! (A)") + } + } + + @Suppress("UNUSED") + @StartableByRPC + class BurbleFlow : FlowLogic<Unit>() { + override val progressTracker = ProgressTracker() + override fun call() { + println("NO OP! (Burble)") + } + } + + @Test + fun `shell should start flow with fully qualified class name`() { + val user = User("u", "p", setOf(all())) + var successful = false + driver(DriverParameters(notarySpecs = emptyList())) { + val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true) + val node = nodeFuture.getOrThrow() + + val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(), + user = user.username, password = user.password, + hostAndPort = node.rpcAddress) + InteractiveShell.startShell(conf) + + // setup and configure some mocks required by InteractiveShell.runFlowByNameFragment() + val output = mock<RenderPrintWriter> { + on { println(any<String>()) } doAnswer { + val line = it.arguments[0] + println("$line") + if ((line is String) && (line.startsWith("Flow completed with result:"))) + successful = true + } + } + val ansiProgressRenderer = mock<ANSIProgressRenderer> { + on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() } + } + InteractiveShell.runFlowByNameFragment( + InteractiveShellIntegrationTest::class.qualifiedName + "\$NoOpFlow", + "", output, node.rpc, ansiProgressRenderer) + } + assertThat(successful).isTrue() + } + + @Test + fun `shell should start flow with unique un-qualified class name`() { + val user = User("u", "p", setOf(all())) + var successful = false + driver(DriverParameters(notarySpecs = emptyList())) { + val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true) + val node = nodeFuture.getOrThrow() + + val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(), + user = user.username, password = user.password, + hostAndPort = node.rpcAddress) + InteractiveShell.startShell(conf) + + // setup and configure some mocks required by InteractiveShell.runFlowByNameFragment() + val output = mock<RenderPrintWriter> { + on { println(any<String>()) } doAnswer { + val line = it.arguments[0] + println("$line") + if ((line is String) && (line.startsWith("Flow completed with result:"))) + successful = true + } + } + val ansiProgressRenderer = mock<ANSIProgressRenderer> { + on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() } + } + InteractiveShell.runFlowByNameFragment( + "InteractiveShellIntegrationTest\$NoOpFlowA", + "", output, node.rpc, ansiProgressRenderer) + } + assertThat(successful).isTrue() + } + + @Test + fun `shell should fail to start flow with ambiguous class name`() { + val user = User("u", "p", setOf(all())) + var successful = false + driver(DriverParameters(notarySpecs = emptyList())) { + val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true) + val node = nodeFuture.getOrThrow() + + val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(), + user = user.username, password = user.password, + hostAndPort = node.rpcAddress) + InteractiveShell.startShell(conf) + + // setup and configure some mocks required by InteractiveShell.runFlowByNameFragment() + val output = mock<RenderPrintWriter> { + on { println(any<String>()) } doAnswer { + val line = it.arguments[0] + println("$line") + if ((line is String) && (line.startsWith("Ambiguous name provided, please be more specific."))) + successful = true + } + } + val ansiProgressRenderer = mock<ANSIProgressRenderer> { + on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() } + } + InteractiveShell.runFlowByNameFragment( + InteractiveShellIntegrationTest::class.qualifiedName + "\$NoOpFlo", + "", output, node.rpc, ansiProgressRenderer) + } + assertThat(successful).isTrue() + } + + @Test + fun `shell should start flow with partially matching class name`() { + val user = User("u", "p", setOf(all())) + var successful = false + driver(DriverParameters(notarySpecs = emptyList())) { + val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true) + val node = nodeFuture.getOrThrow() + + val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(), + user = user.username, password = user.password, + hostAndPort = node.rpcAddress) + InteractiveShell.startShell(conf) + + // setup and configure some mocks required by InteractiveShell.runFlowByNameFragment() + val output = mock<RenderPrintWriter> { + on { println(any<String>()) } doAnswer { + val line = it.arguments[0] + println("$line") + if ((line is String) && (line.startsWith("Flow completed with result"))) + successful = true + } + } + val ansiProgressRenderer = mock<ANSIProgressRenderer> { + on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() } + } + InteractiveShell.runFlowByNameFragment( + "Burble", + "", output, node.rpc, ansiProgressRenderer) + } + assertThat(successful).isTrue() + } } \ No newline at end of file diff --git a/tools/shell/src/main/java/net/corda/tools/shell/RunShellCommand.java b/tools/shell/src/main/java/net/corda/tools/shell/RunShellCommand.java index 9ac5cff1b9..763d9181be 100644 --- a/tools/shell/src/main/java/net/corda/tools/shell/RunShellCommand.java +++ b/tools/shell/src/main/java/net/corda/tools/shell/RunShellCommand.java @@ -44,7 +44,7 @@ public class RunShellCommand extends InteractiveShellCommand { emitHelp(context, parser); return null; } - return InteractiveShell.runRPCFromString(command, out, context, ops(), objectMapper(), isSsh()); + return InteractiveShell.runRPCFromString(command, out, context, ops(), objectMapper()); } private void emitHelp(InvocationContext<Map> context, StringToMethodCallParser<CordaRPCOps> parser) { 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 aa8b9776ac..6729ab0ea2 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 @@ -9,7 +9,6 @@ import net.corda.client.jackson.StringToMethodCallParser import net.corda.client.rpc.CordaRPCClientConfiguration import net.corda.client.rpc.CordaRPCConnection import net.corda.client.rpc.PermissionException -import net.corda.client.rpc.internal.createCordaRPCClientWithInternalSslAndClassLoader import net.corda.client.rpc.internal.createCordaRPCClientWithSslAndClassLoader import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture @@ -19,6 +18,7 @@ import net.corda.core.internal.* import net.corda.core.internal.concurrent.doneFuture import net.corda.core.internal.concurrent.openFuture import net.corda.core.messaging.* +import net.corda.nodeapi.internal.pendingFlowsCount import net.corda.tools.shell.utlities.ANSIProgressRenderer import net.corda.tools.shell.utlities.StdoutANSIProgressRenderer import org.crsh.command.InvocationContext @@ -47,8 +47,7 @@ import java.io.FileDescriptor import java.io.FileInputStream import java.io.InputStream import java.io.PrintWriter -import java.lang.reflect.InvocationTargetException -import java.lang.reflect.UndeclaredThrowableException +import java.lang.reflect.* import java.nio.file.Path import java.util.* import java.util.concurrent.CountDownLatch @@ -94,24 +93,6 @@ object InteractiveShell { _startShell(configuration, classLoader) } - /** - * Starts an interactive shell connected to the local terminal. This shell gives administrator access to the node - * internals. - */ - fun startShellInternal(configuration: ShellConfiguration, classLoader: ClassLoader? = null) { - rpcOps = { username: String, credentials: String -> - val client = createCordaRPCClientWithInternalSslAndClassLoader(hostAndPort = configuration.hostAndPort, - configuration = CordaRPCClientConfiguration.DEFAULT.copy( - maxReconnectAttempts = 1 - ), - sslConfiguration = configuration.nodeSslConfig, - classLoader = classLoader) - this.connection = client.start(username, credentials) - connection.proxy - } - _startShell(configuration, classLoader) - } - private fun _startShell(configuration: ShellConfiguration, classLoader: ClassLoader? = null) { shellConfiguration = configuration InteractiveShell.classLoader = classLoader @@ -229,6 +210,10 @@ object InteractiveShell { // TODO: This should become the default renderer rather than something used specifically by commands. private val outputMapper by lazy { createOutputMapper() } + @VisibleForTesting + lateinit var latch: CountDownLatch + private set + /** * Called from the 'flow' shell command. Takes a name fragment and finds a matching flow, or prints out * the list of options if the request is ambiguous. Then parses [inputData] as constructor arguments using @@ -240,7 +225,7 @@ object InteractiveShell { output: RenderPrintWriter, rpcOps: CordaRPCOps, ansiProgressRenderer: ANSIProgressRenderer, - om: ObjectMapper) { + om: ObjectMapper = outputMapper) { val matches = try { rpcOps.registeredFlows().filter { nameFragment in it } } catch (e: PermissionException) { @@ -250,23 +235,24 @@ object InteractiveShell { if (matches.isEmpty()) { output.println("No matching flow found, run 'flow list' to see your options.", Color.red) return - } else if (matches.size > 1) { + } else if (matches.size > 1 && matches.find { it.endsWith(nameFragment)} == null) { output.println("Ambiguous name provided, please be more specific. Your options are:") matches.forEachIndexed { i, s -> output.println("${i + 1}. $s", Color.yellow) } return } + val flowName = matches.find { it.endsWith(nameFragment)} ?: matches.single() val flowClazz: Class<FlowLogic<*>> = if (classLoader != null) { - uncheckedCast(Class.forName(matches.single(), true, classLoader)) + uncheckedCast(Class.forName(flowName, true, classLoader)) } else { - uncheckedCast(Class.forName(matches.single())) + uncheckedCast(Class.forName(flowName)) } try { // Show the progress tracker on the console until the flow completes or is interrupted with a // Ctrl-C keypress. val stateObservable = runFlowFromString({ clazz, args -> rpcOps.startTrackedFlowDynamic(clazz, *args) }, inputData, flowClazz, om) - val latch = CountDownLatch(1) + latch = CountDownLatch(1) 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. @@ -300,6 +286,38 @@ object InteractiveShell { override fun toString() = (listOf("No applicable constructor for flow. Problems were:") + errors).joinToString(System.lineSeparator()) } + /** + * Tidies up a possibly generic type name by chopping off the package names of classes in a hard-coded set of + * hierarchies that are known to be widely used and recognised, and also not have (m)any ambiguous names in them. + * + * This is used for printing error messages when something doesn't match. + */ + private fun maybeAbbreviateGenericType(type: Type, extraRecognisedPackage: String): String { + val packagesToAbbreviate = listOf("java.", "net.corda.core.", "kotlin.", extraRecognisedPackage) + + fun shouldAbbreviate(typeName: String) = packagesToAbbreviate.any { typeName.startsWith(it) } + fun abbreviated(typeName: String) = if (shouldAbbreviate(typeName)) typeName.split('.').last() else typeName + + fun innerLoop(type: Type): String = when (type) { + is ParameterizedType -> { + val args: List<String> = type.actualTypeArguments.map(::innerLoop) + abbreviated(type.rawType.typeName) + '<' + args.joinToString(", ") + '>' + } + is GenericArrayType -> { + innerLoop(type.genericComponentType) + "[]" + } + is Class<*> -> { + if (type.isArray) + abbreviated(type.simpleName) + else + abbreviated(type.name).replace('$', '.') + } + else -> type.toString() + } + + return innerLoop(type) + } + // TODO: This utility is generally useful and might be better moved to the node class, or an RPC, if we can commit to making it stable API. /** * Given a [FlowLogic] class and a string in one-line Yaml form, finds an applicable constructor and starts @@ -319,10 +337,17 @@ object InteractiveShell { // and keep track of the reasons we failed so we can print them out if no constructors are usable. val parser = StringToMethodCallParser(clazz, om) val errors = ArrayList<String>() + + val classPackage = clazz.packageName for (ctor in clazz.constructors) { var paramNamesFromConstructor: List<String>? = null + fun getPrototype(): List<String> { - val argTypes = ctor.genericParameterTypes.map { it.typeName } + val argTypes = ctor.genericParameterTypes.map { it: Type -> + // If the type name is in the net.corda.core or java namespaces, chop off the package name + // because these hierarchies don't have (m)any ambiguous names and the extra detail is just noise. + maybeAbbreviateGenericType(it, classPackage) + } return paramNamesFromConstructor!!.zip(argTypes).map { (name, type) -> "$name: $type" } } @@ -384,7 +409,7 @@ object InteractiveShell { } @JvmStatic - fun runRPCFromString(input: List<String>, out: RenderPrintWriter, context: InvocationContext<out Any>, cordaRPCOps: CordaRPCOps, om: ObjectMapper, isSsh: Boolean = false): Any? { + fun runRPCFromString(input: List<String>, out: RenderPrintWriter, context: InvocationContext<out Any>, cordaRPCOps: CordaRPCOps, om: ObjectMapper): Any? { val cmd = input.joinToString(" ").trim { it <= ' ' } if (cmd.startsWith("startflow", ignoreCase = true)) { // The flow command provides better support and startFlow requires special handling anyway due to @@ -393,7 +418,7 @@ object InteractiveShell { out.println("Please use the 'flow' command to interact with flows rather than the 'run' command.", Color.yellow) return null } else if (cmd.substringAfter(" ").trim().equals("gracefulShutdown", ignoreCase = true)) { - return InteractiveShell.gracefulShutdown(out, cordaRPCOps, isSsh) + return InteractiveShell.gracefulShutdown(out, cordaRPCOps) } var result: Any? = null @@ -432,9 +457,8 @@ object InteractiveShell { return result } - @JvmStatic - fun gracefulShutdown(userSessionOut: RenderPrintWriter, cordaRPCOps: CordaRPCOps, isSsh: Boolean = false) { + fun gracefulShutdown(userSessionOut: RenderPrintWriter, cordaRPCOps: CordaRPCOps) { fun display(statements: RenderPrintWriter.() -> Unit) { statements.invoke(userSessionOut) @@ -443,40 +467,48 @@ object InteractiveShell { var isShuttingDown = false try { + display { println("Orchestrating a clean shutdown, press CTRL+C to cancel...") } + isShuttingDown = true display { - println("Orchestrating a clean shutdown...") println("...enabling draining mode") - } - cordaRPCOps.setFlowsDrainingModeEnabled(true) - display { println("...waiting for in-flight flows to be completed") } - cordaRPCOps.pendingFlowsCount().updates - .doOnError { error -> - log.error(error.message) - throw error - } - .doOnNext { (first, second) -> - display { - println("...remaining: ${first}/${second}") + cordaRPCOps.terminate(true) + + val latch = CountDownLatch(1) + cordaRPCOps.pendingFlowsCount().updates.doOnError { error -> + log.error(error.message) + throw error + }.doAfterTerminate(latch::countDown).subscribe( + // For each update. + { (first, second) -> display { println("...remaining: $first / $second") } }, + // On error. + { error -> + if (!isShuttingDown) { + display { println("RPC failed: ${error.rootCause}", Color.red) } } - } - .doOnCompleted { - if (isSsh) { - // print in the original Shell process - System.out.println("Shutting down the node via remote SSH session (it may take a while)") - } - display { - println("Shutting down the node (it may take a while)") - } - cordaRPCOps.shutdown() - isShuttingDown = true + }, + // When completed. + { connection.forceClose() - display { - println("...done, quitting standalone shell now.") - } + // This will only show up in the standalone Shell, because the embedded one is killed as part of a node's shutdown. + display { println("...done, quitting the shell now.") } onExit.invoke() - }.toBlocking().single() + }) + while (!Thread.currentThread().isInterrupted) { + try { + latch.await() + break + } catch (e: InterruptedException) { + try { + cordaRPCOps.setFlowsDrainingModeEnabled(false) + display { println("...cancelled clean shutdown.") } + } finally { + Thread.currentThread().interrupt() + break + } + } + } } catch (e: StringToMethodCallParser.UnparseableCallException) { display { println(e.message, Color.red) @@ -484,9 +516,7 @@ object InteractiveShell { } } catch (e: Exception) { if (!isShuttingDown) { - display { - println("RPC failed: ${e.rootCause}", Color.red) - } + display { println("RPC failed: ${e.rootCause}", Color.red) } } } finally { InputStreamSerializer.invokeContext = null diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/ShellConfiguration.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/ShellConfiguration.kt index 90d5f8407c..4877b520cc 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/ShellConfiguration.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/ShellConfiguration.kt @@ -2,7 +2,6 @@ package net.corda.tools.shell import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.messaging.ClientRpcSslOptions -import net.corda.nodeapi.internal.config.SSLConfiguration import java.nio.file.Path data class ShellConfiguration( @@ -12,7 +11,6 @@ data class ShellConfiguration( var password: String = "", val hostAndPort: NetworkHostAndPort, val ssl: ClientRpcSslOptions? = null, - val nodeSslConfig: SSLConfiguration? = null, val sshdPort: Int? = null, val sshHostKeyDirectory: Path? = null, val noLocalShell: Boolean = false) { diff --git a/tools/shell/src/test/java/net/corda/tools/shell/InteractiveShellJavaTest.java b/tools/shell/src/test/java/net/corda/tools/shell/InteractiveShellJavaTest.java index b3581caf11..e3dde61bb6 100644 --- a/tools/shell/src/test/java/net/corda/tools/shell/InteractiveShellJavaTest.java +++ b/tools/shell/src/test/java/net/corda/tools/shell/InteractiveShellJavaTest.java @@ -52,8 +52,8 @@ public class InteractiveShellJavaTest { } } - public FlowA(Integer b) { - this(b.toString()); + public FlowA(int b) { + this(Integer.valueOf(b).toString()); } public FlowA(Integer b, String c) { @@ -111,6 +111,9 @@ public class InteractiveShellJavaTest { this.a = a; } + public FlowB(Amount<Currency> amount, int abc) { + } + @Nullable @Override public ProgressTracker getProgressTracker() { @@ -142,6 +145,7 @@ public class InteractiveShellJavaTest { this.label = label; } + @SuppressWarnings("unused") // Used via reflection. public String getLabel() { return label; } @@ -160,17 +164,17 @@ public class InteractiveShellJavaTest { private void check(String input, String expected, Class<? extends StringFlow> flowClass) throws InteractiveShell.NoApplicableConstructor { InteractiveShell.INSTANCE.runFlowFromString((clazz, args) -> { - StringFlow instance = null; try { instance = (StringFlow)clazz.getConstructor(Arrays.stream(args).map(Object::getClass).toArray(Class[]::new)).newInstance(args); } catch (Exception e) { System.out.println(e); + throw new RuntimeException(e); } output = instance.getA(); OpenFuture<String> future = CordaFutureImplKt.openFuture(); future.set("ABC"); - return new FlowProgressHandleImpl(StateMachineRunId.Companion.createRandom(), future, Observable.just("Some string")); + return new FlowProgressHandleImpl<String>(StateMachineRunId.Companion.createRandom(), future, Observable.just("Some string")); }, input, flowClass, om); assertEquals(input, expected, output); } @@ -245,4 +249,14 @@ public class InteractiveShellJavaTest { public void unwrapLambda() throws InteractiveShell.NoApplicableConstructor { check("party: \"" + megaCorp.getName() + "\", a: Bambam", "Bambam", FlowB.class); } + + @Test + public void niceErrors() { + // Most cases are checked in the Kotlin test, so we only check raw types here. + try { + check("amount: $100", "", FlowB.class); + } catch (InteractiveShell.NoApplicableConstructor e) { + assertEquals("[amount: Amount<Currency>, abc: int]: missing parameter abc", e.getErrors().get(1)); + } + } } diff --git a/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt b/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt index 6d7f614a0f..e184c6ae86 100644 --- a/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt +++ b/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt @@ -14,12 +14,13 @@ import net.corda.core.internal.concurrent.openFuture import net.corda.core.messaging.FlowProgressHandleImpl import net.corda.core.utilities.ProgressTracker import net.corda.node.services.identity.InMemoryIdentityService -import net.corda.testing.internal.DEV_ROOT_CA import net.corda.testing.core.TestIdentity +import net.corda.testing.internal.DEV_ROOT_CA import org.junit.Test import rx.Observable import java.util.* import kotlin.test.assertEquals +import kotlin.test.assertFailsWith class InteractiveShellTest { companion object { @@ -28,7 +29,7 @@ class InteractiveShellTest { @Suppress("UNUSED") class FlowA(val a: String) : FlowLogic<String>() { - constructor(b: Int?) : this(b.toString()) + constructor(b: Int) : this(b.toString()) constructor(b: Int?, c: String) : this(b.toString() + c) constructor(amount: Amount<Currency>) : this(amount.toString()) constructor(pair: Pair<Amount<Currency>, SecureHash.SHA256>) : this(pair.toString()) @@ -48,7 +49,6 @@ class InteractiveShellTest { private fun check(input: String, expected: String) { var output: String? = null InteractiveShell.runFlowFromString({ clazz, args -> - val instance = clazz.getConstructor(*args.map { it!!::class.java }.toTypedArray()).newInstance(*args) as FlowA output = instance.a val future = openFuture<String>() @@ -101,6 +101,27 @@ class InteractiveShellTest { @Test(expected = InteractiveShell.NoApplicableConstructor::class) fun flowTooManyParams() = check("b: 12, c: Yo, d: Bar", "") + @Test + fun niceTypeNamesInErrors() { + val e = assertFailsWith<InteractiveShell.NoApplicableConstructor> { + check("", expected = "") + } + val correct = setOf( + "[amounts: Amount<InteractiveShellTest.UserValue>[]]: missing parameter amounts", + "[amount: Amount<Currency>]: missing parameter amount", + "[pair: Pair<Amount<Currency>, SecureHash.SHA256>]: missing parameter pair", + "[party: Party]: missing parameter party", + "[b: Integer, amount: Amount<InteractiveShellTest.UserValue>]: missing parameter b", + "[b: String[]]: missing parameter b", + "[b: Integer, c: String]: missing parameter b", + "[a: String]: missing parameter a", + "[b: int]: missing parameter b" + ) + val errors = e.errors.toHashSet() + errors.removeAll(correct) + assert(errors.isEmpty()) { errors.joinToString(", ") } + } + @Test fun party() = check("party: \"${megaCorp.name}\"", megaCorp.name.toString()) diff --git a/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt b/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt index 5ba08b691d..69569e8dbf 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt @@ -1,8 +1,8 @@ package net.corda.webserver import com.typesafe.config.Config +import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort -import net.corda.nodeapi.internal.config.NodeSSLConfiguration import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.getValue import net.corda.nodeapi.internal.config.parseAs @@ -11,10 +11,13 @@ import java.nio.file.Path /** * [baseDirectory] is not retrieved from the config file but rather from a command line argument. */ -class WebServerConfig(override val baseDirectory: Path, val config: Config) : NodeSSLConfiguration { - override val keyStorePassword: String by config - override val trustStorePassword: String by config - override val crlCheckSoftFail: Boolean by config +class WebServerConfig(val baseDirectory: Path, val config: Config) { + + val keyStorePath: String by config + val keyStorePassword: String by config + val trustStorePath: String by config + val trustStorePassword: String by config + val useHTTPS: Boolean by config val myLegalName: String by config val rpcAddress: NetworkHostAndPort by lazy { diff --git a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt index 5bf16e4a7a..9ca4e03546 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt @@ -66,10 +66,10 @@ class NodeWebServer(val config: WebServerConfig) { httpsConfiguration.outputBufferSize = 32768 httpsConfiguration.addCustomizer(SecureRequestCustomizer()) val sslContextFactory = SslContextFactory() - sslContextFactory.keyStorePath = config.sslKeystore.toString() + sslContextFactory.keyStorePath = config.keyStorePath sslContextFactory.setKeyStorePassword(config.keyStorePassword) sslContextFactory.setKeyManagerPassword(config.keyStorePassword) - sslContextFactory.setTrustStorePath(config.trustStoreFile.toString()) + sslContextFactory.setTrustStorePath(config.trustStorePath) sslContextFactory.setTrustStorePassword(config.trustStorePassword) sslContextFactory.setExcludeProtocols("SSL.*", "TLSv1", "TLSv1.1") sslContextFactory.setIncludeProtocols("TLSv1.2") diff --git a/webserver/webcapsule/build.gradle b/webserver/webcapsule/build.gradle index 38a5765248..9d63c537a3 100644 --- a/webserver/webcapsule/build.gradle +++ b/webserver/webcapsule/build.gradle @@ -1,6 +1,6 @@ /** * This build.gradle exists to publish our capsule (executable fat jar) to maven. It cannot be placed in the - * node project because the bintray plugin cannot publish two modules from one project. + * webserver project because the bintray plugin cannot publish two modules from one project. */ apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'us.kirchmeier.capsule' @@ -26,18 +26,24 @@ dependencies { sourceCompatibility = 1.6 targetCompatibility = 1.6 -task buildWebserverJar(type: FatCapsule, dependsOn: project(':node').compileJava) { +jar.enabled = false + +capsule { + version capsule_version +} + +task buildWebserverJar(type: FatCapsule, dependsOn: project(':node').tasks.jar) { applicationClass 'net.corda.webserver.WebServer' archiveName "corda-webserver-${corda_release_version}.jar" applicationSource = files( - project(':webserver').configurations.runtime, - project(':webserver').jar, - project(':node').sourceSets.main.java.outputDir.toString() + '/CordaCaplet.class', - project(':node').sourceSets.main.java.outputDir.toString() + '/CordaCaplet$1.class', - "$rootDir/config/dev/log4j2.xml", - "$rootDir/node/build/resources/main/reference.conf" + project(':webserver').configurations.runtimeClasspath, + project(':webserver').tasks.jar, + project(':node').sourceSets.main.java.outputDir.toString() + '/CordaCaplet.class', + project(':node').sourceSets.main.java.outputDir.toString() + '/CordaCaplet$1.class', + project(':node').buildDir.toString() + '/resources/main/reference.conf', + "$rootDir/config/dev/log4j2.xml", + project(':node:capsule').projectDir.toString() + '/NOTICE' // Copy CDDL notice ) - from 'NOTICE' // Copy CDDL notice from configurations.capsuleRuntime.files.collect { zipTree(it) } capsuleManifest { @@ -57,10 +63,12 @@ task buildWebserverJar(type: FatCapsule, dependsOn: project(':node').compileJava } } +assemble.dependsOn buildWebserverJar + artifacts { runtimeArtifacts buildWebserverJar publish buildWebserverJar { - classifier "" + classifier '' } }