From 13815d252e07c3be01c169ba5a6f0286b6875a06 Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Tue, 30 Oct 2018 15:26:46 +0000 Subject: [PATCH 01/10] ENT-2659 Eliminate lots of contention in serialization (#4120) * Remove most contentious bit of mutex in the codebase Fix up SerializerFactory Make getSerializerFactory() non-blocking Workaround for DJVM issues * Some clean up and also de-contend the RPC client. * Fix up attachment class loader code. * Bug fix --- .../net/corda/client/rpc/CordaRPCClient.kt | 9 ++- .../amqp/AMQPClientSerializationScheme.kt | 15 ++-- .../kotlin/net/corda/node/internal/Node.kt | 7 +- .../amqp/AMQPServerSerializationScheme.kt | 4 +- .../internal/AttachmentsClassLoaderBuilder.kt | 5 +- .../internal/CheckpointSerializationScheme.kt | 13 ++- .../internal/SerializationScheme.kt | 21 ++--- .../internal/amqp/AMQPSerializationScheme.kt | 15 +++- .../internal/amqp/SerializerFactory.kt | 80 +++++++++++-------- ...ticInitialisationOfSerializedObjectTest.kt | 4 +- 10 files changed, 100 insertions(+), 73 deletions(-) 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 db445caa55..c5cceeb1b1 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 @@ -1,20 +1,23 @@ package net.corda.client.rpc +import com.github.benmanes.caffeine.cache.Caffeine 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.internal.PLATFORM_VERSION +import net.corda.core.messaging.ClientRpcSslOptions import net.corda.core.messaging.CordaRPCOps +import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.internal.effectiveSerializationEnv import net.corda.core.utilities.NetworkHostAndPort -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.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport -import net.corda.core.internal.PLATFORM_VERSION import net.corda.serialization.internal.AMQP_RPC_CLIENT_CONTEXT +import net.corda.serialization.internal.amqp.SerializerFactory import java.time.Duration /** @@ -293,7 +296,7 @@ class CordaRPCClient private constructor( effectiveSerializationEnv } catch (e: IllegalStateException) { try { - AMQPClientSerializationScheme.initialiseSerialization(classLoader) + AMQPClientSerializationScheme.initialiseSerialization(classLoader, Caffeine.newBuilder().maximumSize(128).build, SerializerFactory>().asMap()) } catch (e: IllegalStateException) { // Race e.g. two of these constructed in parallel, ignore. } diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt index a0e2bfc307..3ef215f9e8 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt @@ -3,7 +3,7 @@ package net.corda.client.rpc.internal.serialization.amqp import net.corda.core.cordapp.Cordapp import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationContext.* +import net.corda.core.serialization.SerializationContext.UseCase import net.corda.core.serialization.SerializationCustomSerializer import net.corda.core.serialization.internal.SerializationEnvironment import net.corda.core.serialization.internal.nodeSerializationEnv @@ -19,24 +19,25 @@ import net.corda.serialization.internal.amqp.custom.RxNotificationSerializer * This scheme is for use by the RPC Client calls. */ class AMQPClientSerializationScheme( - cordappCustomSerializers: Set>, - serializerFactoriesForContexts: AccessOrderLinkedHashMap, SerializerFactory> + cordappCustomSerializers: Set>, + serializerFactoriesForContexts: MutableMap, SerializerFactory> ) : AbstractAMQPSerializationScheme(cordappCustomSerializers, serializerFactoriesForContexts) { constructor(cordapps: List) : this(cordapps.customSerializers, AccessOrderLinkedHashMap { 128 }) + constructor(cordapps: List, serializerFactoriesForContexts: MutableMap, SerializerFactory>) : this(cordapps.customSerializers, serializerFactoriesForContexts) @Suppress("UNUSED") constructor() : this(emptySet(), AccessOrderLinkedHashMap { 128 }) companion object { /** Call from main only. */ - fun initialiseSerialization(classLoader: ClassLoader? = null) { - nodeSerializationEnv = createSerializationEnv(classLoader) + fun initialiseSerialization(classLoader: ClassLoader? = null, serializerFactoriesForContexts: MutableMap, SerializerFactory> = AccessOrderLinkedHashMap { 128 }) { + nodeSerializationEnv = createSerializationEnv(classLoader, serializerFactoriesForContexts) } - fun createSerializationEnv(classLoader: ClassLoader? = null): SerializationEnvironment { + fun createSerializationEnv(classLoader: ClassLoader? = null, serializerFactoriesForContexts: MutableMap, SerializerFactory> = AccessOrderLinkedHashMap { 128 }): SerializationEnvironment { return SerializationEnvironment.with( SerializationFactoryImpl().apply { - registerScheme(AMQPClientSerializationScheme(emptyList())) + registerScheme(AMQPClientSerializationScheme(emptyList(), serializerFactoriesForContexts)) }, storageContext = AMQP_STORAGE_CONTEXT, p2pContext = if (classLoader != null) AMQP_P2P_CONTEXT.withClassLoader(classLoader) else AMQP_P2P_CONTEXT, 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 1794cc135c..395b6c5697 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -3,6 +3,7 @@ package net.corda.node.internal import com.codahale.metrics.JmxReporter import com.codahale.metrics.MetricFilter import com.codahale.metrics.MetricRegistry +import com.github.benmanes.caffeine.cache.Caffeine import com.palominolabs.metrics.newrelic.AllEnabledMetricAttributeFilter import com.palominolabs.metrics.newrelic.NewRelicReporter import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme @@ -22,6 +23,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.ClassWhitelist import net.corda.core.serialization.internal.SerializationEnvironment import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort @@ -54,6 +56,7 @@ import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException import net.corda.serialization.internal.* +import net.corda.serialization.internal.amqp.SerializerFactory import org.apache.commons.lang.SystemUtils import org.h2.jdbc.JdbcSQLException import org.slf4j.Logger @@ -471,8 +474,8 @@ open class Node(configuration: NodeConfiguration, val classloader = cordappLoader.appClassLoader nodeSerializationEnv = SerializationEnvironment.with( SerializationFactoryImpl().apply { - registerScheme(AMQPServerSerializationScheme(cordappLoader.cordapps)) - registerScheme(AMQPClientSerializationScheme(cordappLoader.cordapps)) + registerScheme(AMQPServerSerializationScheme(cordappLoader.cordapps, Caffeine.newBuilder().maximumSize(128).build, SerializerFactory>().asMap())) + registerScheme(AMQPClientSerializationScheme(cordappLoader.cordapps, Caffeine.newBuilder().maximumSize(128).build, SerializerFactory>().asMap())) }, p2pContext = AMQP_P2P_CONTEXT.withClassLoader(classloader), rpcServerContext = AMQP_RPC_SERVER_CONTEXT.withClassLoader(classloader), diff --git a/node/src/main/kotlin/net/corda/node/serialization/amqp/AMQPServerSerializationScheme.kt b/node/src/main/kotlin/net/corda/node/serialization/amqp/AMQPServerSerializationScheme.kt index e1183be6b7..dfe6b6fafd 100644 --- a/node/src/main/kotlin/net/corda/node/serialization/amqp/AMQPServerSerializationScheme.kt +++ b/node/src/main/kotlin/net/corda/node/serialization/amqp/AMQPServerSerializationScheme.kt @@ -9,7 +9,6 @@ import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme import net.corda.serialization.internal.amqp.AccessOrderLinkedHashMap import net.corda.serialization.internal.amqp.SerializerFactory import net.corda.serialization.internal.amqp.custom.RxNotificationSerializer -import java.util.concurrent.ConcurrentHashMap /** * When set as the serialization scheme, defines the RPC Server serialization scheme as using the Corda @@ -17,9 +16,10 @@ import java.util.concurrent.ConcurrentHashMap */ class AMQPServerSerializationScheme( cordappCustomSerializers: Set>, - serializerFactoriesForContexts: AccessOrderLinkedHashMap, SerializerFactory> + serializerFactoriesForContexts: MutableMap, SerializerFactory> ) : AbstractAMQPSerializationScheme(cordappCustomSerializers, serializerFactoriesForContexts) { constructor(cordapps: List) : this(cordapps.customSerializers, AccessOrderLinkedHashMap { 128 }) + constructor(cordapps: List, serializerFactoriesForContexts: MutableMap, SerializerFactory>) : this(cordapps.customSerializers, serializerFactoriesForContexts) constructor() : this(emptySet(), AccessOrderLinkedHashMap { 128 }) diff --git a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/AttachmentsClassLoaderBuilder.kt b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/AttachmentsClassLoaderBuilder.kt index 01386d8823..7f77351952 100644 --- a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/AttachmentsClassLoaderBuilder.kt +++ b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/AttachmentsClassLoaderBuilder.kt @@ -1,13 +1,12 @@ package net.corda.serialization.internal import net.corda.core.crypto.SecureHash -import java.lang.ClassLoader /** * Drop-in replacement for [AttachmentsClassLoaderBuilder] in the serialization module. * This version is not strongly-coupled to [net.corda.core.node.ServiceHub]. */ @Suppress("UNUSED", "UNUSED_PARAMETER") -internal class AttachmentsClassLoaderBuilder(private val properties: Map, private val deserializationClassLoader: ClassLoader) { - fun build(attachmentHashes: List): AttachmentsClassLoader? = null +internal class AttachmentsClassLoaderBuilder() { + fun build(attachmentHashes: List, properties: Map, deserializationClassLoader: ClassLoader): AttachmentsClassLoader? = null } \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/CheckpointSerializationScheme.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/CheckpointSerializationScheme.kt index c370084e7a..12519312e9 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/CheckpointSerializationScheme.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/CheckpointSerializationScheme.kt @@ -2,8 +2,11 @@ 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.ClassWhitelist +import net.corda.core.serialization.EncodingWhitelist +import net.corda.core.serialization.SerializationEncoding import net.corda.core.serialization.internal.CheckpointSerializationContext +import java.lang.UnsupportedOperationException @KeepForDJVM data class CheckpointSerializationContextImpl @JvmOverloads constructor( @@ -13,17 +16,13 @@ data class CheckpointSerializationContextImpl @JvmOverloads constructor( 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. + * Unsupported for checkpoints. */ override fun withAttachmentsClassLoader(attachmentHashes: List): CheckpointSerializationContext { - properties[attachmentsClassLoaderEnabledPropertyName] as? Boolean == true || return this - val classLoader = builder.build(attachmentHashes) ?: return this - return withClassLoader(classLoader) + throw UnsupportedOperationException() } override fun withProperty(property: Any, value: Any): CheckpointSerializationContext { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt index 9c78e01926..ed5aecd987 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt @@ -31,8 +31,10 @@ data class SerializationContextImpl @JvmOverloads constructor(override val prefe override val useCase: SerializationContext.UseCase, override val encoding: SerializationEncoding?, override val encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist, - override val lenientCarpenterEnabled: Boolean = false) : SerializationContext { - private val builder = AttachmentsClassLoaderBuilder(properties, deserializationClassLoader) + override val lenientCarpenterEnabled: Boolean = false, + private val builder: AttachmentsClassLoaderBuilder = AttachmentsClassLoaderBuilder() +) : SerializationContext { + /** * {@inheritDoc} @@ -41,7 +43,7 @@ data class SerializationContextImpl @JvmOverloads constructor(override val prefe */ override fun withAttachmentsClassLoader(attachmentHashes: List): SerializationContext { properties[attachmentsClassLoaderEnabledPropertyName] as? Boolean == true || return this - val classLoader = builder.build(attachmentHashes) ?: return this + val classLoader = builder.build(attachmentHashes, properties, deserializationClassLoader) ?: return this return withClassLoader(classLoader) } @@ -75,13 +77,13 @@ data class SerializationContextImpl @JvmOverloads constructor(override val prefe * can replace it with an alternative version. */ @DeleteForDJVM -internal class AttachmentsClassLoaderBuilder(private val properties: Map, private val deserializationClassLoader: ClassLoader) { - private val cache: Cache, AttachmentsClassLoader> = Caffeine.newBuilder().weakValues().maximumSize(1024).build() +class AttachmentsClassLoaderBuilder() { + private val cache: Cache, ClassLoader>, AttachmentsClassLoader> = Caffeine.newBuilder().weakValues().maximumSize(1024).build() - fun build(attachmentHashes: List): AttachmentsClassLoader? { + fun build(attachmentHashes: List, properties: Map, deserializationClassLoader: ClassLoader): AttachmentsClassLoader? { val serializationContext = properties[serializationContextKey] as? SerializeAsTokenContext ?: return null // Some tests don't set one. try { - return cache.get(attachmentHashes) { + return cache.get(Pair(attachmentHashes, deserializationClassLoader)) { val missing = ArrayList() val attachments = ArrayList() attachmentHashes.forEach { id -> @@ -120,12 +122,13 @@ open class SerializationFactoryImpl( // truncate sequence to at most magicSize, and make sure it's a copy to avoid holding onto large ByteArrays val magic = CordaSerializationMagic(byteSequence.slice(end = magicSize).copyBytes()) val lookupKey = magic to target - return schemes.computeIfAbsent(lookupKey) { + // ConcurrentHashMap.get() is lock free, but computeIfAbsent is not, even if the key is in the map already. + return (schemes[lookupKey] ?: schemes.computeIfAbsent(lookupKey) { registeredSchemes.filter { it.canDeserializeVersion(magic, target) }.forEach { return@computeIfAbsent it } // XXX: Not single? logger.warn("Cannot find serialization scheme for: [$lookupKey, " + "${if (magic == amqpMagic) "AMQP" else "UNKNOWN MAGIC"}] registeredSchemes are: $registeredSchemes") throw UnsupportedOperationException("Serialization scheme $lookupKey not supported.") - } to magic + }) to magic } @Throws(NotSerializableException::class) 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 f6b5556f82..5be95c4306 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 @@ -40,12 +40,19 @@ interface SerializerFactoryFactory { @KeepForDJVM abstract class AbstractAMQPSerializationScheme( private val cordappCustomSerializers: Set>, - private val serializerFactoriesForContexts: AccessOrderLinkedHashMap, SerializerFactory>, + maybeNotConcurrentSerializerFactoriesForContexts: MutableMap, SerializerFactory>, val sff: SerializerFactoryFactory = createSerializerFactoryFactory() ) : SerializationScheme { @DeleteForDJVM constructor(cordapps: List) : this(cordapps.customSerializers, AccessOrderLinkedHashMap(128)) + // This is a bit gross but a broader check for ConcurrentMap is not allowed inside DJVM. + private val serializerFactoriesForContexts: MutableMap, SerializerFactory> = if (maybeNotConcurrentSerializerFactoriesForContexts is AccessOrderLinkedHashMap<*, *>) { + Collections.synchronizedMap(maybeNotConcurrentSerializerFactoriesForContexts) + } else { + maybeNotConcurrentSerializerFactoriesForContexts + } + // TODO: This method of initialisation for the Whitelist and plugin serializers will have to change // when we have per-cordapp contexts and dynamic app reloading but for now it's the easiest way companion object { @@ -166,8 +173,9 @@ abstract class AbstractAMQPSerializationScheme( open val publicKeySerializer: CustomSerializer<*> = net.corda.serialization.internal.amqp.custom.PublicKeySerializer private fun getSerializerFactory(context: SerializationContext): SerializerFactory { - return synchronized(serializerFactoriesForContexts) { - serializerFactoriesForContexts.computeIfAbsent(Pair(context.whitelist, context.deserializationClassLoader)) { + val key = Pair(context.whitelist, context.deserializationClassLoader) + // ConcurrentHashMap.get() is lock free, but computeIfAbsent is not, even if the key is in the map already. + return serializerFactoriesForContexts[key] ?: serializerFactoriesForContexts.computeIfAbsent(key) { when (context.useCase) { SerializationContext.UseCase.RPCClient -> rpcClientSerializerFactory(context) @@ -178,7 +186,6 @@ abstract class AbstractAMQPSerializationScheme( registerCustomSerializers(context, it) } } - } } override fun deserialize(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): T { 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 cfce4fa76e..c8d4b14891 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 @@ -7,7 +7,10 @@ import net.corda.core.StubOutForDJVM import net.corda.core.internal.kotlinObjectInstance import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.ClassWhitelist -import net.corda.core.utilities.* +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug +import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.trace import net.corda.serialization.internal.carpenter.* import org.apache.qpid.proton.amqp.* import java.io.NotSerializableException @@ -95,6 +98,9 @@ open class SerializerFactory( val classloader: ClassLoader get() = classCarpenter.classloader + // Used to short circuit any computation for a given input, for performance. + private data class MemoType(val actualClass: Class<*>?, val declaredType: Type) : Type + /** * Look up, and manufacture if necessary, a serializer for the given type. * @@ -106,50 +112,56 @@ 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() - val actualType: Type = if (actualClass == null) declaredType - else inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType + val ourType = MemoType(actualClass, declaredType) + // ConcurrentHashMap.get() is lock free, but computeIfAbsent is not, even if the key is in the map already. + return serializersByType[ourType] ?: run { - val serializer = when { + 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. // In this case use of CollectionSerializer is perfectly appropriate. - (Collection::class.java.isAssignableFrom(declaredClass) || - (actualClass != null && Collection::class.java.isAssignableFrom(actualClass))) && - !EnumSet::class.java.isAssignableFrom(actualClass ?: declaredClass) -> { - val declaredTypeAmended = CollectionSerializer.deriveParameterizedType(declaredType, declaredClass, actualClass) - serializersByType.computeIfAbsent(declaredTypeAmended) { - CollectionSerializer(declaredTypeAmended, this) + (Collection::class.java.isAssignableFrom(declaredClass) || + (actualClass != null && Collection::class.java.isAssignableFrom(actualClass))) && + !EnumSet::class.java.isAssignableFrom(actualClass ?: declaredClass) -> { + val declaredTypeAmended = CollectionSerializer.deriveParameterizedType(declaredType, declaredClass, actualClass) + serializersByType.computeIfAbsent(declaredTypeAmended) { + CollectionSerializer(declaredTypeAmended, this) + } } - } // Declared class may not be set to Map, but actual class could be a map. // In this case use of MapSerializer is perfectly appropriate. - (Map::class.java.isAssignableFrom(declaredClass) || - (actualClass != null && Map::class.java.isAssignableFrom(actualClass))) -> { - val declaredTypeAmended = MapSerializer.deriveParameterizedType(declaredType, declaredClass, actualClass) - serializersByType.computeIfAbsent(declaredTypeAmended) { - makeMapSerializer(declaredTypeAmended) - } - } - Enum::class.java.isAssignableFrom(actualClass ?: declaredClass) -> { - logger.trace { - "class=[${actualClass?.simpleName} | $declaredClass] is an enumeration " + - "declaredType=${declaredType.typeName} " + - "isEnum=${declaredType::class.java.isEnum}" + (Map::class.java.isAssignableFrom(declaredClass) || + (actualClass != null && Map::class.java.isAssignableFrom(actualClass))) -> { + val declaredTypeAmended = MapSerializer.deriveParameterizedType(declaredType, declaredClass, actualClass) + serializersByType.computeIfAbsent(declaredTypeAmended) { + makeMapSerializer(declaredTypeAmended) + } } + Enum::class.java.isAssignableFrom(actualClass ?: declaredClass) -> { + logger.trace { + "class=[${actualClass?.simpleName} | $declaredClass] is an enumeration " + + "declaredType=${declaredType.typeName} " + + "isEnum=${declaredType::class.java.isEnum}" + } - serializersByType.computeIfAbsent(actualClass ?: declaredClass) { - whitelist.requireWhitelisted(actualType) - EnumSerializer(actualType, actualClass ?: declaredClass, this) + serializersByType.computeIfAbsent(actualClass ?: declaredClass) { + whitelist.requireWhitelisted(actualType) + EnumSerializer(actualType, actualClass ?: declaredClass, this) + } + } + else -> { + makeClassSerializer(actualClass ?: declaredClass, actualType, declaredType) } } - else -> { - makeClassSerializer(actualClass ?: declaredClass, actualType, declaredType) - } + + serializersByDescriptor.putIfAbsent(serializer.typeDescriptor, serializer) + // Always store the short-circuit too, for performance. + serializersByType.putIfAbsent(ourType, serializer) + return serializer } - - serializersByDescriptor.putIfAbsent(serializer.typeDescriptor, serializer) - - return serializer } /** diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/StaticInitialisationOfSerializedObjectTest.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/StaticInitialisationOfSerializedObjectTest.kt index 526c52c1c7..800f5e87c7 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/StaticInitialisationOfSerializedObjectTest.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/StaticInitialisationOfSerializedObjectTest.kt @@ -63,9 +63,9 @@ class StaticInitialisationOfSerializedObjectTest { // we can't actually construct one sf.get(null, D::class.java) - // post creation of the serializer we should have one element in the map, this + // post creation of the serializer we should have two elements in the map, this // proves we didn't statically construct an instance of C when building the serializer - assertEquals(1, serialisersByType.size) + assertEquals(2, serialisersByType.size) } @Test From e06480017320a776dac7306ea27889b272c2cf96 Mon Sep 17 00:00:00 2001 From: josecoll Date: Tue, 30 Oct 2018 15:44:29 +0000 Subject: [PATCH 02/10] CORDA-2112 Update the `uploader` field for Attachments loaded upon Node CorDapp startup (#4127) * CORDA-2112 Update the `uploader` field upon Node startup for CorDapps where an Attachment is already in a nodes attachment store (from an untrusted source). * Removed incorrect assertion. * Fix broken unit test. * Reverted back throw FileAlreadyExistsException API semantics on public API. * De-duplicate DuplicateAttachmentException * Revert original Exception handling logic. --- .../persistence/AttachmentStorageInternal.kt | 6 ++++ .../persistence/NodeAttachmentService.kt | 30 ++++++++++++++----- .../persistence/NodeAttachmentServiceTest.kt | 26 ++++++++++++++++ 3 files changed, 54 insertions(+), 8 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/AttachmentStorageInternal.kt b/node/src/main/kotlin/net/corda/node/services/persistence/AttachmentStorageInternal.kt index 0ca5cd2ffb..0d9a24c898 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/AttachmentStorageInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/AttachmentStorageInternal.kt @@ -2,6 +2,7 @@ package net.corda.node.services.persistence import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentStorage +import net.corda.nodeapi.exceptions.DuplicateAttachmentException import java.io.InputStream interface AttachmentStorageInternal : AttachmentStorage { @@ -10,4 +11,9 @@ interface AttachmentStorageInternal : AttachmentStorage { * and is only for the node. */ fun privilegedImportAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId + + /** + * Similar to above but returns existing [AttachmentId] instead of throwing [DuplicateAttachmentException] + */ + fun privilegedImportOrGetAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId } 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 1ea12ffb15..37f5a38d6c 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 @@ -6,13 +6,11 @@ import com.google.common.hash.HashCode import com.google.common.hash.Hashing import com.google.common.hash.HashingInputStream import com.google.common.io.CountingInputStream -import net.corda.core.ClientRelevantError import net.corda.core.CordaRuntimeException import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.ContractClassName import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.sha256 import net.corda.core.internal.* import net.corda.core.node.ServicesForResolution @@ -286,6 +284,14 @@ class NodeAttachmentService( return import(jar, uploader, filename) } + override fun privilegedImportOrGetAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId { + return try { + import(jar, uploader, filename) + } catch (faee: java.nio.file.FileAlreadyExistsException) { + AttachmentId.parse(faee.message!!) + } + } + override fun hasAttachment(attachmentId: AttachmentId): Boolean = database.transaction { currentDBSession().find(NodeAttachmentService.DBAttachment::class.java, attachmentId.toString()) != null } @@ -306,9 +312,7 @@ class NodeAttachmentService( val id = bytes.sha256() if (!hasAttachment(id)) { checkIsAValidJAR(bytes.inputStream()) - val jarSigners = getSigners(bytes) - val session = currentDBSession() val attachment = NodeAttachmentService.DBAttachment( attId = id.toString(), @@ -318,14 +322,24 @@ class NodeAttachmentService( contractClassNames = contractClassNames, signers = jarSigners ) - session.save(attachment) attachmentCount.inc() log.info("Stored new attachment $id") - id - } else { - throw DuplicateAttachmentException(id.toString()) + return@withContractsInJar id } + if (isUploaderTrusted(uploader)) { + val session = currentDBSession() + val attachment = session.get(NodeAttachmentService.DBAttachment::class.java, id.toString()) + // update the `upLoader` field (as the existing attachment may have been resolved from a peer) + if (attachment.uploader != uploader) { + attachment.uploader = uploader + session.saveOrUpdate(attachment) + log.info("Updated attachment $id with uploader $uploader") + attachmentCache.invalidate(id) + attachmentContentCache.invalidate(id) + } + } + throw DuplicateAttachmentException(id.toString()) } } } 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 e87d60de3c..bd584796ba 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 @@ -9,6 +9,7 @@ import com.nhaarman.mockito_kotlin.whenever import net.corda.core.JarSignatureTestUtils.createJar import net.corda.core.JarSignatureTestUtils.generateKey import net.corda.core.JarSignatureTestUtils.signJar +import net.corda.core.contracts.ContractAttachment import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 import net.corda.core.flows.FlowLogic @@ -47,6 +48,7 @@ import javax.tools.StandardLocation import javax.tools.ToolProvider import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertNotEquals import kotlin.test.assertNull @@ -123,6 +125,30 @@ class NodeAttachmentServiceTest { } } + @Test + fun `insert contract attachment as an untrusted uploader and then as trusted CorDapp uploader`() { + val contractJarName = makeTestContractJar("com.example.MyContract") + val testJar = dir.resolve(contractJarName) + val expectedHash = testJar.readAll().sha256() + + // PRIVILEGED_UPLOADERS = listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER, P2P_UPLOADER, UNKNOWN_UPLOADER) + // TRUSTED_UPLOADERS = listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER) + + database.transaction { + val id = testJar.read { storage.privilegedImportOrGetAttachment(it, P2P_UPLOADER, null) } + assertEquals(expectedHash, id) + val attachment1 = storage.openAttachment(expectedHash) + + val id2 = testJar.read { storage.privilegedImportOrGetAttachment(it, DEPLOYED_CORDAPP_UPLOADER, null) } + assertEquals(expectedHash, id2) + val attachment2 = storage.openAttachment(expectedHash) + + assertNotEquals(attachment1, attachment2) + assertEquals(P2P_UPLOADER, (attachment1 as ContractAttachment).uploader) + assertEquals(DEPLOYED_CORDAPP_UPLOADER, (attachment2 as ContractAttachment).uploader) + } + } + @Test fun `missing is not cached`() { val (testJar, expectedHash) = makeTestJar() From ab1deaac99914613cfc75bdb756ed41d7bf957d1 Mon Sep 17 00:00:00 2001 From: Stefano Franz Date: Tue, 30 Oct 2018 17:14:47 +0000 Subject: [PATCH 03/10] CORDA-1953 Display error when unable to open connection to Docker (#4136) * Display error when unable to open connection to Docker * review comments --- .../java/net/corda/bootstrapper/GuiUtils.java | 60 +++++++++++-------- .../kotlin/net/corda/bootstrapper/Main.kt | 18 ++++++ .../kotlin/net/corda/bootstrapper/gui/Gui.kt | 2 +- 3 files changed, 55 insertions(+), 25 deletions(-) diff --git a/tools/network-bootstrapper/src/main/java/net/corda/bootstrapper/GuiUtils.java b/tools/network-bootstrapper/src/main/java/net/corda/bootstrapper/GuiUtils.java index 204a9529fa..97bbc243aa 100644 --- a/tools/network-bootstrapper/src/main/java/net/corda/bootstrapper/GuiUtils.java +++ b/tools/network-bootstrapper/src/main/java/net/corda/bootstrapper/GuiUtils.java @@ -1,5 +1,7 @@ package net.corda.bootstrapper; +import javafx.application.Platform; +import javafx.embed.swing.JFXPanel; import javafx.scene.control.Alert; import javafx.scene.control.Label; import javafx.scene.control.TextArea; @@ -9,6 +11,7 @@ import javafx.stage.StageStyle; import java.io.PrintWriter; import java.io.StringWriter; +import java.util.concurrent.CountDownLatch; public class GuiUtils { @@ -18,31 +21,40 @@ public class GuiUtils { alert.setTitle("Exception"); alert.setHeaderText(title); alert.setContentText(message); - - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - exception.printStackTrace(pw); - String exceptionText = sw.toString(); - - Label label = new Label("Details:"); - - TextArea textArea = new TextArea(exceptionText); - textArea.setEditable(false); - textArea.setWrapText(true); - - textArea.setMaxWidth(Double.MAX_VALUE); - textArea.setMaxHeight(Double.MAX_VALUE); - GridPane.setVgrow(textArea, Priority.ALWAYS); - GridPane.setHgrow(textArea, Priority.ALWAYS); - - GridPane expContent = new GridPane(); - expContent.setMaxWidth(Double.MAX_VALUE); - expContent.add(label, 0, 0); - expContent.add(textArea, 0, 1); - - alert.getDialogPane().setExpandableContent(expContent); - + if (exception != null) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + exception.printStackTrace(pw); + String exceptionText = sw.toString(); + TextArea textArea = new TextArea(exceptionText); + textArea.setEditable(false); + textArea.setWrapText(true); + textArea.setMaxWidth(Double.MAX_VALUE); + textArea.setMaxHeight(Double.MAX_VALUE); + Label label = new Label("Details:"); + GridPane.setVgrow(textArea, Priority.ALWAYS); + GridPane.setHgrow(textArea, Priority.ALWAYS); + GridPane expContent = new GridPane(); + expContent.setMaxWidth(Double.MAX_VALUE); + expContent.add(label, 0, 0); + expContent.add(textArea, 0, 1); + alert.getDialogPane().setExpandableContent(expContent); + } alert.showAndWait(); } + public static void showAndQuit(String title, String message, Throwable exception) { + CountDownLatch countDownLatch = new CountDownLatch(1); + new JFXPanel(); + Platform.runLater(() -> { + showException(title, message, exception); + countDownLatch.countDown(); + System.exit(1); + }); + try { + countDownLatch.await(); + } catch (InterruptedException e) { + } + } + } diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt index eb38bf11cb..76623cd21c 100644 --- a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt @@ -1,4 +1,5 @@ @file:JvmName("Main") + package net.corda.bootstrapper import javafx.application.Application @@ -7,15 +8,19 @@ import net.corda.bootstrapper.backends.Backend.BackendType.AZURE import net.corda.bootstrapper.cli.AzureParser import net.corda.bootstrapper.cli.CliParser import net.corda.bootstrapper.cli.CommandLineInterface +import net.corda.bootstrapper.docker.DockerUtils import net.corda.bootstrapper.gui.Gui import net.corda.bootstrapper.serialization.SerializationEngine import picocli.CommandLine +import javax.ws.rs.ProcessingException +import kotlin.system.exitProcess val baseArgs = CliParser() fun main(args: Array) { SerializationEngine.init() CommandLine(baseArgs).parse(*args) + testDockerConnectivity() if (baseArgs.gui) { Application.launch(Gui::class.java) @@ -32,3 +37,16 @@ fun main(args: Array) { } CommandLineInterface().run(argParser) } + +private fun testDockerConnectivity() { + try { + DockerUtils.createLocalDockerClient().listImagesCmd().exec() + } catch (se: ProcessingException) { + if (baseArgs.gui) { + GuiUtils.showAndQuit("Could not connect to Docker", "Please ensure that docker is running locally", null) + } else { + System.err.println("Could not connect to Docker, please ensure that docker is running locally") + exitProcess(1) + } + } +} diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/gui/Gui.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/gui/Gui.kt index 841022f502..a517ba7599 100644 --- a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/gui/Gui.kt +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/gui/Gui.kt @@ -1,7 +1,7 @@ package net.corda.bootstrapper.gui import javafx.stage.Stage -import tornadofx.* +import tornadofx.App class Gui : App(BootstrapperView::class) { override fun start(stage: Stage) { From a1da0b4a636c5bfcc4484ae3cd32176e879439ee Mon Sep 17 00:00:00 2001 From: Richard Gendal Brown Date: Tue, 30 Oct 2018 17:53:54 +0000 Subject: [PATCH 04/10] Add Apache 2 license badge --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 19459d4cff..eb0a7a29f0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png) +![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png) - + [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) # Corda From 7eca7515d6c38681fe4c657d15974cd3e83c51dd Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Tue, 30 Oct 2018 18:06:01 +0000 Subject: [PATCH 05/10] Clarifies that only one serialisation mechanism is required. Clarifies dependencies required for Client RPC. (#4135) * Makes it clear that only one serialisation mechanism is required. Clarifies dependencies required for Client RPC. * Addresses review feedback. --- docs/source/clientrpc.rst | 21 ++++++++++++--------- docs/source/serialization.rst | 27 ++++++++++++++------------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/docs/source/clientrpc.rst b/docs/source/clientrpc.rst index 7390f69a55..dbcee97aeb 100644 --- a/docs/source/clientrpc.rst +++ b/docs/source/clientrpc.rst @@ -11,20 +11,23 @@ Interacting with a node Overview -------- -You should interact with your node using the `CordaRPCClient`_ library. This library that allows you to easily -write clients in a JVM-compatible language to interact with a running node. The library connects to the node using a -message queue protocol and then provides a simple RPC interface to interact with the node. You make calls on a JVM -object as normal, and the marshalling back and forth is handled for you. +To interact with your node, you need to write a client in a JVM-compatible language using the `CordaRPCClient`_ class. +This class allows you to connect to your node via a message queue protocol and provides a simple RPC interface for +interacting with the node. You make calls on a JVM 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 using the popular - Spring Boot server `here `_. + your node via HTTP, you will need to stand up your own webserver that connects to your node using the + `CordaRPCClient`_ class. You can find an example of how to do this using the popular Spring Boot server + `here `_. Connecting to a node via RPC ---------------------------- -`CordaRPCClient`_ provides a ``start`` method that takes the node's RPC address and returns a `CordaRPCConnection`_. -`CordaRPCConnection`_ provides a ``proxy`` method that takes an RPC username and password and returns a `CordaRPCOps`_ +To use `CordaRPCClient`_, you must add ``net.corda:corda-rpc:$corda_release_version`` as a ``cordaCompile`` dependency +in your client's ``build.gradle`` file. + +`CordaRPCClient`_ has a ``start`` method that takes the node's RPC address and returns a `CordaRPCConnection`_. +`CordaRPCConnection`_ has a ``proxy`` method that takes an RPC username and password and returns a `CordaRPCOps`_ object that you can use to interact with the node. Here is an example of using `CordaRPCClient`_ to connect to a node and log the current time on its internal clock: diff --git a/docs/source/serialization.rst b/docs/source/serialization.rst index 00f4baa07a..b394754147 100644 --- a/docs/source/serialization.rst +++ b/docs/source/serialization.rst @@ -39,22 +39,23 @@ weakly or untyped string-based serialisation schemes like JSON or XML. The prima Whitelisting ------------ -In classic Java serialization, any class on the JVM classpath can be deserialized. This has shown to be a source of exploits -and vulnerabilities by exploiting the large set of 3rd party libraries on the classpath as part of the dependencies of -a JVM application and a carefully crafted stream of bytes to be deserialized. In Corda, we prevent just any class from -being deserialized (and pro-actively during serialization) by insisting that each object's class belongs on a whitelist -of allowed classes. +In classic Java serialization, any class on the JVM classpath can be deserialized. This is a source of exploits and +vulnerabilities that exploit the large set of third-party libraries that are added to the classpath as part of a JVM +application's dependencies and carefully craft a malicious stream of bytes to be deserialized. In Corda, we strictly +control which classes can be deserialized (and, pro-actively, serialized) by insisting that each (de)serializable class +is part of a whitelist of allowed classes. -Classes get onto the whitelist via one of three mechanisms: +To add a class to the whitelist, you must use either of the following mechanisms: -#. Via the ``@CordaSerializable`` annotation. In order to whitelist a class, this annotation can be present on the - class itself, on any of the super classes or on any interface implemented by the class or super classes or any - interface extended by an interface implemented by the class or superclasses. -#. By implementing the ``SerializationWhitelist`` interface and specifying a list of `whitelist` classes. -#. Via the built in Corda whitelist (see the class ``DefaultWhitelist``). Whilst this is not user editable, it does list - common JDK classes that have been whitelisted for your convenience. +#. Add the ``@CordaSerializable`` annotation to the class. This annotation can be present on the + class itself, on any super class of the class, on any interface implemented by the class or its super classes, or any + interface extended by an interface implemented by the class or its super classes. +#. Implement the ``SerializationWhitelist`` interface and specify a list of whitelisted classes. -The annotation is the preferred method for whitelisting. An example is shown in :doc:`tutorial-clientrpc-api`. +There is also a built-in Corda whitelist (see the ``DefaultWhitelist`` class) that whitelists common JDK classes for +convenience. This whitelist is not user-editable. + +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/kotlin/ClientRpcTutorial.kt From fa87054b821adceff5730b37f3f2793ba29b2f40 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Wed, 31 Oct 2018 11:09:02 +0000 Subject: [PATCH 06/10] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eb0a7a29f0..a7639a2dcf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png) + [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) From f159629e367b97a0b07115811f6235f31f6acaa2 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Wed, 31 Oct 2018 11:12:31 +0000 Subject: [PATCH 07/10] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a7639a2dcf..0cb5a4d017 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ - +

+ Corda +

[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) From 8ddd8d383d3d225a60277fb6c12661274c503ee7 Mon Sep 17 00:00:00 2001 From: Anthony Keenan Date: Wed, 31 Oct 2018 12:58:57 +0000 Subject: [PATCH 08/10] Remove unused test class (#4139) --- .../corda/core/contracts/ContractsDSLTests.kt | 308 +++++++++--------- 1 file changed, 152 insertions(+), 156 deletions(-) diff --git a/core/src/test/kotlin/net/corda/core/contracts/ContractsDSLTests.kt b/core/src/test/kotlin/net/corda/core/contracts/ContractsDSLTests.kt index 57320d7eb9..294aebd427 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/ContractsDSLTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/ContractsDSLTests.kt @@ -13,170 +13,166 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -class ContractsDSLTests { - class UnwantedCommand : CommandData +class UnwantedCommand : CommandData - interface TestCommands : CommandData { - class CommandOne : TypeOnlyCommandData(), TestCommands - class CommandTwo : TypeOnlyCommandData(), TestCommands +interface TestCommands : CommandData { + class CommandOne : TypeOnlyCommandData(), TestCommands + class CommandTwo : TypeOnlyCommandData(), TestCommands +} + +val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) +val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")) + +val validCommandOne = CommandWithParties(listOf(megaCorp.publicKey, miniCorp.publicKey), listOf(megaCorp.party, miniCorp.party), TestCommands.CommandOne()) +val validCommandTwo = CommandWithParties(listOf(megaCorp.publicKey), listOf(megaCorp.party), TestCommands.CommandTwo()) +val invalidCommand = CommandWithParties(emptyList(), emptyList(), UnwantedCommand()) + +@RunWith(Parameterized::class) +class RequireSingleCommandTests(private val testFunction: (Collection>) -> CommandWithParties, + @Suppress("UNUSED_PARAMETER") description: String) { + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{1}") + fun data(): Collection> = listOf( + arrayOf({ commands: Collection> -> commands.requireSingleCommand() }, "Inline version"), + arrayOf({ commands: Collection> -> commands.requireSingleCommand(TestCommands::class.java) }, "Interop version") + ) } - private companion object { - val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) - val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")) - - val validCommandOne = CommandWithParties(listOf(megaCorp.publicKey, miniCorp.publicKey), listOf(megaCorp.party, miniCorp.party), TestCommands.CommandOne()) - val validCommandTwo = CommandWithParties(listOf(megaCorp.publicKey), listOf(megaCorp.party), TestCommands.CommandTwo()) - val invalidCommand = CommandWithParties(emptyList(), emptyList(), UnwantedCommand()) + @Test + fun `check function returns one value`() { + val commands = listOf(validCommandOne, invalidCommand) + val returnedCommand = testFunction(commands) + assertEquals(returnedCommand, validCommandOne, "they should be the same") } - @RunWith(Parameterized::class) - class RequireSingleCommandTests(private val testFunction: (Collection>) -> CommandWithParties, + @Test(expected = IllegalArgumentException::class) + fun `check error is thrown if more than one valid command`() { + val commands = listOf(validCommandOne, validCommandTwo) + testFunction(commands) + } + + @Test + fun `check error is thrown when command is of wrong type`() { + val commands = listOf(invalidCommand) + Assertions.assertThatThrownBy { testFunction(commands) } + .isInstanceOf(IllegalStateException::class.java) + .hasMessage("Required net.corda.core.contracts.TestCommands command") + } +} + +@RunWith(Parameterized::class) +class SelectWithSingleInputsTests(private val testFunction: (Collection>, PublicKey?, AbstractParty?) -> Iterable>, + @Suppress("UNUSED_PARAMETER") description: String) { + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{1}") + fun data(): Collection> = listOf( + arrayOf({ commands: Collection>, signer: PublicKey?, party: AbstractParty? -> commands.select(signer, party) }, "Inline version"), + arrayOf({ commands: Collection>, signer: PublicKey?, party: AbstractParty? -> commands.select(TestCommands::class.java, signer, party) }, "Interop version") + ) + } + + @Test + fun `check that function returns all values`() { + val commands = listOf(validCommandOne, validCommandTwo) + testFunction(commands, null, null) + assertEquals(2, commands.size) + assertTrue(commands.contains(validCommandOne)) + assertTrue(commands.contains(validCommandTwo)) + } + + @Test + fun `check that function does not return invalid command types`() { + val commands = listOf(validCommandOne, invalidCommand) + val filteredCommands = testFunction(commands, null, null).toList() + assertEquals(1, filteredCommands.size) + assertTrue(filteredCommands.contains(validCommandOne)) + assertFalse(filteredCommands.contains(invalidCommand)) + } + + @Test + fun `check that function returns commands from valid signers`() { + val commands = listOf(validCommandOne, validCommandTwo) + val filteredCommands = testFunction(commands, miniCorp.publicKey, null).toList() + assertEquals(1, filteredCommands.size) + assertTrue(filteredCommands.contains(validCommandOne)) + assertFalse(filteredCommands.contains(validCommandTwo)) + } + + @Test + fun `check that function returns commands from valid parties`() { + val commands = listOf(validCommandOne, validCommandTwo) + val filteredCommands = testFunction(commands, null, miniCorp.party).toList() + assertEquals(1, filteredCommands.size) + assertTrue(filteredCommands.contains(validCommandOne)) + assertFalse(filteredCommands.contains(validCommandTwo)) + } +} + +@RunWith(Parameterized::class) +class SelectWithMultipleInputsTests(private val testFunction: (Collection>, Collection?, Collection?) -> Iterable>, @Suppress("UNUSED_PARAMETER") description: String) { - companion object { - @JvmStatic - @Parameterized.Parameters(name = "{1}") - fun data(): Collection> = listOf( - arrayOf({ commands: Collection> -> commands.requireSingleCommand() }, "Inline version"), - arrayOf({ commands: Collection> -> commands.requireSingleCommand(TestCommands::class.java) }, "Interop version") - ) - } - - @Test - fun `check function returns one value`() { - val commands = listOf(validCommandOne, invalidCommand) - val returnedCommand = testFunction(commands) - assertEquals(returnedCommand, validCommandOne, "they should be the same") - } - - @Test(expected = IllegalArgumentException::class) - fun `check error is thrown if more than one valid command`() { - val commands = listOf(validCommandOne, validCommandTwo) - testFunction(commands) - } - - @Test - fun `check error is thrown when command is of wrong type`() { - val commands = listOf(invalidCommand) - Assertions.assertThatThrownBy { testFunction(commands) } - .isInstanceOf(IllegalStateException::class.java) - .hasMessage("Required net.corda.core.contracts.ContractsDSLTests.TestCommands command") - } + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{1}") + fun data(): Collection> = listOf( + arrayOf({ commands: Collection>, signers: Collection?, party: Collection? -> commands.select(signers, party) }, "Inline version"), + arrayOf({ commands: Collection>, signers: Collection?, party: Collection? -> commands.select(TestCommands::class.java, signers, party) }, "Interop version") + ) } - @RunWith(Parameterized::class) - class SelectWithSingleInputsTests(private val testFunction: (Collection>, PublicKey?, AbstractParty?) -> Iterable>, - @Suppress("UNUSED_PARAMETER") description: String) { - companion object { - @JvmStatic - @Parameterized.Parameters(name = "{1}") - fun data(): Collection> = listOf( - arrayOf({ commands: Collection>, signer: PublicKey?, party: AbstractParty? -> commands.select(signer, party) }, "Inline version"), - arrayOf({ commands: Collection>, signer: PublicKey?, party: AbstractParty? -> commands.select(TestCommands::class.java, signer, party) }, "Interop version") - ) - } - - @Test - fun `check that function returns all values`() { - val commands = listOf(validCommandOne, validCommandTwo) - testFunction(commands, null, null) - assertEquals(2, commands.size) - assertTrue(commands.contains(validCommandOne)) - assertTrue(commands.contains(validCommandTwo)) - } - - @Test - fun `check that function does not return invalid command types`() { - val commands = listOf(validCommandOne, invalidCommand) - val filteredCommands = testFunction(commands, null, null).toList() - assertEquals(1, filteredCommands.size) - assertTrue(filteredCommands.contains(validCommandOne)) - assertFalse(filteredCommands.contains(invalidCommand)) - } - - @Test - fun `check that function returns commands from valid signers`() { - val commands = listOf(validCommandOne, validCommandTwo) - val filteredCommands = testFunction(commands, miniCorp.publicKey, null).toList() - assertEquals(1, filteredCommands.size) - assertTrue(filteredCommands.contains(validCommandOne)) - assertFalse(filteredCommands.contains(validCommandTwo)) - } - - @Test - fun `check that function returns commands from valid parties`() { - val commands = listOf(validCommandOne, validCommandTwo) - val filteredCommands = testFunction(commands, null, miniCorp.party).toList() - assertEquals(1, filteredCommands.size) - assertTrue(filteredCommands.contains(validCommandOne)) - assertFalse(filteredCommands.contains(validCommandTwo)) - } + @Test + fun `check that function returns all values`() { + val commands = listOf(validCommandOne, validCommandTwo) + testFunction(commands, null, null) + assertEquals(2, commands.size) + assertTrue(commands.contains(validCommandOne)) + assertTrue(commands.contains(validCommandTwo)) } - @RunWith(Parameterized::class) - class SelectWithMultipleInputsTests(private val testFunction: (Collection>, Collection?, Collection?) -> Iterable>, - @Suppress("UNUSED_PARAMETER") description: String) { - companion object { - @JvmStatic - @Parameterized.Parameters(name = "{1}") - fun data(): Collection> = listOf( - arrayOf({ commands: Collection>, signers: Collection?, party: Collection? -> commands.select(signers, party) }, "Inline version"), - arrayOf({ commands: Collection>, signers: Collection?, party: Collection? -> commands.select(TestCommands::class.java, signers, party) }, "Interop version") - ) - } - - @Test - fun `check that function returns all values`() { - val commands = listOf(validCommandOne, validCommandTwo) - testFunction(commands, null, null) - assertEquals(2, commands.size) - assertTrue(commands.contains(validCommandOne)) - assertTrue(commands.contains(validCommandTwo)) - } - - @Test - fun `check that function does not return invalid command types`() { - val commands = listOf(validCommandOne, invalidCommand) - val filteredCommands = testFunction(commands, null, null).toList() - assertEquals(1, filteredCommands.size) - assertTrue(filteredCommands.contains(validCommandOne)) - assertFalse(filteredCommands.contains(invalidCommand)) - } - - @Test - fun `check that function returns commands from valid signers`() { - val commands = listOf(validCommandOne, validCommandTwo) - val filteredCommands = testFunction(commands, listOf(megaCorp.publicKey), null).toList() - assertEquals(2, filteredCommands.size) - assertTrue(filteredCommands.contains(validCommandOne)) - assertTrue(filteredCommands.contains(validCommandTwo)) - } - - @Test - fun `check that function returns commands from all valid signers`() { - val commands = listOf(validCommandOne, validCommandTwo) - val filteredCommands = testFunction(commands, listOf(miniCorp.publicKey, megaCorp.publicKey), null).toList() - assertEquals(1, filteredCommands.size) - assertTrue(filteredCommands.contains(validCommandOne)) - assertFalse(filteredCommands.contains(validCommandTwo)) - } - - @Test - fun `check that function returns commands from valid parties`() { - val commands = listOf(validCommandOne, validCommandTwo) - val filteredCommands = testFunction(commands, null, listOf(megaCorp.party)).toList() - assertEquals(2, filteredCommands.size) - assertTrue(filteredCommands.contains(validCommandOne)) - assertTrue(filteredCommands.contains(validCommandTwo)) - } - - @Test - fun `check that function returns commands from all valid parties`() { - val commands = listOf(validCommandOne, validCommandTwo) - val filteredCommands = testFunction(commands, null, listOf(miniCorp.party, megaCorp.party)).toList() - assertEquals(1, filteredCommands.size) - assertTrue(filteredCommands.contains(validCommandOne)) - assertFalse(filteredCommands.contains(validCommandTwo)) - } + @Test + fun `check that function does not return invalid command types`() { + val commands = listOf(validCommandOne, invalidCommand) + val filteredCommands = testFunction(commands, null, null).toList() + assertEquals(1, filteredCommands.size) + assertTrue(filteredCommands.contains(validCommandOne)) + assertFalse(filteredCommands.contains(invalidCommand)) } -} \ No newline at end of file + + @Test + fun `check that function returns commands from valid signers`() { + val commands = listOf(validCommandOne, validCommandTwo) + val filteredCommands = testFunction(commands, listOf(megaCorp.publicKey), null).toList() + assertEquals(2, filteredCommands.size) + assertTrue(filteredCommands.contains(validCommandOne)) + assertTrue(filteredCommands.contains(validCommandTwo)) + } + + @Test + fun `check that function returns commands from all valid signers`() { + val commands = listOf(validCommandOne, validCommandTwo) + val filteredCommands = testFunction(commands, listOf(miniCorp.publicKey, megaCorp.publicKey), null).toList() + assertEquals(1, filteredCommands.size) + assertTrue(filteredCommands.contains(validCommandOne)) + assertFalse(filteredCommands.contains(validCommandTwo)) + } + + @Test + fun `check that function returns commands from valid parties`() { + val commands = listOf(validCommandOne, validCommandTwo) + val filteredCommands = testFunction(commands, null, listOf(megaCorp.party)).toList() + assertEquals(2, filteredCommands.size) + assertTrue(filteredCommands.contains(validCommandOne)) + assertTrue(filteredCommands.contains(validCommandTwo)) + } + + @Test + fun `check that function returns commands from all valid parties`() { + val commands = listOf(validCommandOne, validCommandTwo) + val filteredCommands = testFunction(commands, null, listOf(miniCorp.party, megaCorp.party)).toList() + assertEquals(1, filteredCommands.size) + assertTrue(filteredCommands.contains(validCommandOne)) + assertFalse(filteredCommands.contains(validCommandTwo)) + } +} From 437a05303771ccd22bab68f5b8e42f83efcfc0aa Mon Sep 17 00:00:00 2001 From: szymonsztuka Date: Wed, 31 Oct 2018 14:39:22 +0000 Subject: [PATCH 09/10] Minor docs update - Persistence API and node database (#4061) Note about database considerations regarding Cordapp compatibility between OS and ENT. Remove database configuration "schema" as this is not implemented in OS, it was already mentioned as ignored for SQL server. --- docs/source/api-persistence.rst | 7 ++++++- docs/source/node-database.rst | 14 ++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/source/api-persistence.rst b/docs/source/api-persistence.rst index 8fabd37e17..ef0b1ead76 100644 --- a/docs/source/api-persistence.rst +++ b/docs/source/api-persistence.rst @@ -100,7 +100,7 @@ associate a ``StateRef`` with a persisted representation of a ``ContractState`` unconsumed states in the vault. The ``PersistentState`` subclass should be marked up as a JPA 2.1 *Entity* with a defined table name and having -properties (in Kotlin, getters/setters in Java) annotated to map to the appropriate columns and SQL types. Additional +properties (in Kotlin, getters/setters in Java) annotated to map to the appropriate columns and SQL types. Additional entities can be included to model these properties where they are more complex, for example collections, so the mapping does not have to be *flat*. The ``MappedSchema`` must provide a list of all of the JPA entity classes for that schema in order to initialise the ORM layer. @@ -111,6 +111,11 @@ Several examples of entities and mappings are provided in the codebase, includin .. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/schemas/CashSchemaV1.kt :language: kotlin +.. note:: If Cordapp needs to be portable between Corda OS (running against H2) and Corda Enterprise (running against a standalone database), + consider database vendors specific requirements. + Ensure that table and column names are compatible with the naming convention of the database vendors for which the Cordapp will be deployed, + e.g. for Oracle database, prior to version 12.2 the maximum length of table/column name is 30 bytes (the exact number of characters depends on the database encoding). + Identity mapping ---------------- Schema entity attributes defined by identity types (``AbstractParty``, ``Party``, ``AnonymousParty``) are automatically diff --git a/docs/source/node-database.rst b/docs/source/node-database.rst index f9c7e3a06c..e40c3bf520 100644 --- a/docs/source/node-database.rst +++ b/docs/source/node-database.rst @@ -22,21 +22,17 @@ configuration for PostgreSQL: dataSourceProperties = { dataSourceClassName = "org.postgresql.ds.PGSimpleDataSource" - dataSource.url = "jdbc:postgresql://[HOST]:[PORT]/postgres" + dataSource.url = "jdbc:postgresql://[HOST]:[PORT]/[DATABASE]" dataSource.user = [USER] dataSource.password = [PASSWORD] } database = { transactionIsolationLevel = READ_COMMITTED - schema = [SCHEMA] } Note that: - -* The ``database.schema`` property is optional -* The value of ``database.schema`` is not wrapped in double quotes and Postgres always treats it as a lower-case value - (e.g. ``AliceCorp`` becomes ``alicecorp``) -* If you provide a custom ``database.schema``, its value must either match the ``dataSource.user`` value to end up +* Database schema name can be set in JDBC URL string e.g. currentSchema=myschema +* Database schema name must either match the ``dataSource.user`` value to end up on the standard schema search path according to the `PostgreSQL documentation `_, or the schema search path must be set explicitly for the user. @@ -56,15 +52,13 @@ an example node configuration for SQLServer: } database = { transactionIsolationLevel = READ_COMMITTED - schema = [SCHEMA] } jarDirs = ["[FULL_PATH]/sqljdbc_6.2/enu/"] Note that: -* The ``database.schema`` property is optional and is ignored as of release 3.1. * Ensure the directory referenced by jarDirs contains only one JDBC driver JAR file; by the default, - sqljdbc_6.2/enu/contains two JDBC JAR file for different Java versions. + sqljdbc_6.2/enu/contains two JDBC JAR files for different Java versions. Node database tables ^^^^^^^^^^^^^^^^^^^^ From 7cb9e174a98780999a411257bb723ddf8cba24e4 Mon Sep 17 00:00:00 2001 From: szymonsztuka Date: Wed, 31 Oct 2018 16:20:11 +0000 Subject: [PATCH 10/10] Documentation for CORDA-1915 Build system for jarsigner (#4006) Documenting new cordapp and conformation plugins capabilities to sign CorDapp JAR, enabled by corda-gradle-plugins releases 4.0.32 and 4.0.33. --- docs/source/api-contract-constraints.rst | 2 + docs/source/cordapp-build-systems.rst | 102 +++++++++++++++++++++++ docs/source/generating-a-node.rst | 60 +++++++++++++ finance/build.gradle | 2 + 4 files changed, 166 insertions(+) diff --git a/docs/source/api-contract-constraints.rst b/docs/source/api-contract-constraints.rst index df09bb5f26..caf9a58168 100644 --- a/docs/source/api-contract-constraints.rst +++ b/docs/source/api-contract-constraints.rst @@ -85,6 +85,8 @@ time effectively stop being a part of the network. **Signature constraints.** These are not yet supported, but once implemented they will allow a state to require a JAR signed by a specified identity, via the regular Java ``jarsigner`` tool. This will be the most flexible type and the smoothest to deploy: no restarts or contract upgrade transactions are needed. +When CorDapp is build using :ref:`corda-gradle-plugin ` the JAR is signed +by Corda development key by default, an external keystore can be configured or signing can be disabled. **Defaults.** The default constraint type is either a zone constraint, if the network parameters in effect when the transaction is built contain an entry for that contract class, or a hash constraint if not. diff --git a/docs/source/cordapp-build-systems.rst b/docs/source/cordapp-build-systems.rst index 7ca31b2b1a..7c02b7648d 100644 --- a/docs/source/cordapp-build-systems.rst +++ b/docs/source/cordapp-build-systems.rst @@ -109,6 +109,108 @@ in Gradle. See the example below, specifically the ``apache-commons`` include. For further information about managing dependencies, see `the Gradle docs `_. +.. _cordapp_build_system_signing_cordapp_jar_ref: + +Signing the CorDapp JAR +^^^^^^^^^^^^^^^^^^^^^^^ +The ``cordapp`` plugin can sign the generated CorDapp JAR file using `JAR signing and verification tool `_. +Signing the CorDapp enables its contract classes to use signature constraints instead of other types of the constraints, +for constraints explanation refer to :doc:`api-contract-constraints`. +By default the JAR file is signed by Corda development certificate. +The signing process can be disabled or configured to use an external keystore. +The ``signing`` entry may contain the following parameters: + + * ``enabled`` the control flag to enable signing process, by default is set to ``true``, set to ``false`` to disable signing + * ``options`` any relevant parameters of `SignJar ANT task `_, + by default the JAR file is signed with Corda development key, the external keystore can be specified, + the minimal list of required options is shown below, for other options referer to `SignJar task `_: + + * ``keystore`` the path to the keystore file, by default *cordadevcakeys.jks* keystore is shipped with the plugin + * ``alias`` the alias to sign under, the default value is *cordaintermediateca* + * ``storepass`` the keystore password, the default value is *cordacadevpass* + * ``keypass`` the private key password if it's different than the password for the keystore, the default value is *cordacadevkeypass* + * ``storetype`` the keystore type, the default value is *JKS* + +The parameters can be also set by system properties passed to Gradle build process. +The system properties should be named as the relevant option name prefixed with '*signing.*', e.g. +a value for ``alias`` can be taken from the ``signing.alias`` system property. The following system properties can be used: +``signing.enabled``, ``signing.keystore``, ``signing.alias``, ``signing.storepass``, ``signing.keypass``, ``signing.storetype``. +The resolution order of a configuration value is as follows: the signing process takes a value specified in the ``signing`` entry first, +the empty string *""* is also considered as the correct value. +If the option is not set, the relevant system property named *signing.option* is tried. +If the system property is not set then the value defaults to the configuration of the Corda development certificate. + +The example ``cordapp`` plugin with plugin ``signing`` configuration: + +.. sourcecode:: groovy + + cordapp { + signing { + enabled true + options { + keystore "/path/to/jarSignKeystore.p12" + alias "cordapp-signer" + storepass "secret1!" + keypass "secret1!" + storetype "PKCS12" + } + } + //... + +CorDapp auto-signing allows to use signature constraints for contracts from the CorDapp +without need to create a keystore and configure the ``cordapp`` plugin. +For production deployment ensure to sign the CorDapp using your own certificate e.g. by setting system properties to point to an external keystore +or by disabling signing in ``cordapp`` plugin and signing the CordDapp JAR downstream in your build pipeline. +CorDapp signed by Corda development certificate is accepted by Corda node only when running in the development mode. + +Signing options can be contextually overwritten by the relevant system properties as described above. +This allows the single ``build.gradle`` file to be used for a development build (defaulting to the Corda development keystore) +and for a production build (using an external keystore). +The example system properties setup for the build process which overrides signing options: + +.. sourcecode:: shell + + ./gradlew -Dsigning.keystore="/path/to/keystore.jks" -Dsigning.alias="alias" -Dsigning.storepass="password" -Dsigning.keypass="password" + +Without providing the system properties, the build will sign the CorDapp with the default Corda development keystore: + +.. sourcecode:: shell + + ./gradlew + +CorDapp signing can be disabled for a build: + +.. sourcecode:: shell + + ./gradlew -Dsigning.enabled=false + +Other system properties can be explicitly assigned to options by calling ``System.getProperty`` in ``cordapp`` plugin configuration. +For example the below configuration sets the specific signing algorithm when a system property is available otherwise defaults to an empty string: + +.. sourcecode:: groovy + + cordapp { + signing { + options { + sigalg System.getProperty('custom.sigalg','') + } + } + //... + +Then the build process can set the value for *custom.sigalg* system property and other system properties recognized by ``cordapp`` plugin: + +.. sourcecode:: shell + + ./gradlew -Dcustom.sigalg="SHA256withECDSA" -Dsigning.keystore="/path/to/keystore.jks" -Dsigning.alias="alias" -Dsigning.storepass="password" -Dsigning.keypass="password" + +To check if CorDapp is signed use `JAR signing and verification tool `_: + +.. sourcecode:: shell + + jarsigner --verify path/to/cordapp.jar + +Cordformation plugin can also sign CorDapps JARs, when deploying set of nodes, see :doc:`generating-a-node`. + Example ^^^^^^^ Below is a sample of what a CorDapp's Gradle dependencies block might look like. When building your own CorDapp, you should diff --git a/docs/source/generating-a-node.rst b/docs/source/generating-a-node.rst index 052bdd3af5..2277f80c69 100644 --- a/docs/source/generating-a-node.rst +++ b/docs/source/generating-a-node.rst @@ -138,6 +138,66 @@ To copy the same file to all nodes `ext.drivers` can be defined in the top level } } +Signing Cordapp JARs +^^^^^^^^^^^^^^^^^^^^ +Cordform entry ``signing`` configures the signing of CorDapp JARs. +Signing the CorDapp enables its contract classes to use signature constraints instead of other types of the constraints :doc:`api-contract-constraints`. +By default all CorDapp JARs are signed by Corda development certificate. +The sign task may use an external keystore, or create a new one. +The ``signing`` entry may contain the following parameters: + +* ``enabled`` the control flag to enable signing process, by default is set to ``true``, set to ``false`` to disable signing +* ``all`` if set to ``true`` (by default) all CorDapps inside *cordapp* subdirectory will be signed, otherwise if ``false`` then only the generated Cordapp will be signed +* ``options`` any relevant parameters of `SignJar ANT task `_ and `GenKey ANT task `_, + by default the JAR file is signed by Corda development key, the external keystore can be specified, + the minimal list of required options is shown below, for other options referer to `SignJar task `_: + + * ``keystore`` the path to the keystore file, by default *cordadevcakeys.jks* keystore is shipped with the plugin + * ``alias`` the alias to sign under, the default value is *cordaintermediateca* + * ``storepass`` the keystore password, the default value is *cordacadevpass* + * ``keypass`` the private key password if it's different than the password for the keystore, the default value is *cordacadevkeypass* + * ``storetype`` the keystore type, the default value is *JKS* + * ``dname`` the distinguished name for entity, the option is used when ``generateKeystore true`` only + * ``keyalg`` the method to use when generating name-value pair, the value defaults to *RSA* as Corda doesn't support *DSA*, the option is used when ``generateKeystore true`` only + +* ``generateKeystore`` the flag to generate a keystore, it is set to ``false`` by default. If set to ``true`` then ad hock keystore is created and its key isused + instead of the default Corda development key or any external key. + The same ``options`` to specify an external keystore are used to define the newly created keystore. Additionally + ``dname`` and ``keyalg`` are required. Other options are described in `GenKey task `_. + If the existing keystore is already present the task will reuse it, however if the file is inside the *build* directory, + then it will be deleted when Gradle *clean* task is run. + +The example below shows the minimal set of ``options`` needed to create a dummy keystore: + +.. sourcecode:: groovy + + task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { + signing { + enabled true + generateKeystore true + all false + options { + keystore "./build/nodes/jarSignKeystore.p12" + alias "cordapp-signer" + storepass "secret1!" + storetype "PKCS12" + dname "OU=Dummy Cordapp Distributor, O=Corda, L=London, C=GB" + keyalg "RSA" + } + } + //... + +Contracts classes from signed CorDapp JARs will be checked by signature constraints by default. +You can force them to be checked by zone constraints by adding contract class names to ``includeWhitelist`` entry, +the list will generate *include_whitelist.txt* file used internally by :doc:`network-bootstrapper` tool. +Refer to :doc:`api-contract-constraints` to understand implication of different constraint types before adding ``includeWhitelist`` to ``deployNodes`` task. +The snippet below configures contracts classes from Finance CorDapp to be verified using zone constraints instead of signature constraints: + +.. sourcecode:: groovy + + task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { + includeWhitelist = [ "net.corda.finance.contracts.asset.Cash", "net.corda.finance.contracts.asset.CommercialPaper" ] + //... Specifying a custom webserver ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/finance/build.gradle b/finance/build.gradle index be028830eb..320e850e2a 100644 --- a/finance/build.gradle +++ b/finance/build.gradle @@ -90,6 +90,8 @@ cordapp { targetPlatformVersion corda_platform_version.toInteger() minimumPlatformVersion 1 } + // By default the Cordapp is signed by Corda development certificate, for production build pass the following system properties to Gradle to use specific keystore e.g: + // ./gradlew -Dsigning.enabled="true" -Dsigning.keystore="/path/to/keystore.jks" -Dsigning.alias="alias" -Dsigning.storepass="password" -Dsigning.keypass="password" } publish {