From 20a9892123eb24accfee7eb97b99c2092b5ceca0 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Fri, 22 Sep 2017 15:22:40 +0200 Subject: [PATCH 001/185] RPC: Report failures during deserialisation of method arguments to the client as an exception. --- .../main/kotlin/net/corda/nodeapi/RPCApi.kt | 31 +++++++++++++++---- .../node/services/messaging/RPCServer.kt | 26 +++++++++++++--- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt b/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt index 8fc01b0e7d..7abeae27ad 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt @@ -128,15 +128,26 @@ object RPCApi { } companion object { + fun fromClientMessage(context: SerializationContext, message: ClientMessage): ClientToServer { val tag = Tag.values()[message.getIntProperty(TAG_FIELD_NAME)] return when (tag) { - RPCApi.ClientToServer.Tag.RPC_REQUEST -> RpcRequest( - clientAddress = MessageUtil.getJMSReplyTo(message), - id = RpcRequestId(message.getLongProperty(RPC_ID_FIELD_NAME)), - methodName = message.getStringProperty(METHOD_NAME_FIELD_NAME), - arguments = message.getBodyAsByteArray().deserialize(context = context) - ) + RPCApi.ClientToServer.Tag.RPC_REQUEST -> { + val partialReq = RpcRequest( + clientAddress = MessageUtil.getJMSReplyTo(message), + id = RpcRequestId(message.getLongProperty(RPC_ID_FIELD_NAME)), + methodName = message.getStringProperty(METHOD_NAME_FIELD_NAME), + arguments = emptyList() + ) + // Deserialisation of the arguments can fail, but we'd like to return a response mapped to + // this specific RPC, so we throw the partial request with ID and method name. + try { + val arguments = message.getBodyAsByteArray().deserialize>(context = context) + return partialReq.copy(arguments = arguments) + } catch (t: Throwable) { + throw ArgumentDeserialisationException(t, partialReq) + } + } RPCApi.ClientToServer.Tag.OBSERVABLES_CLOSED -> { val ids = ArrayList() val buffer = message.bodyBuffer @@ -149,6 +160,7 @@ object RPCApi { } } } + } /** @@ -215,6 +227,13 @@ object RPCApi { } } } + + /** + * Thrown when the arguments passed to an RPC couldn't be deserialised by the server. This will + * typically indicate a missing application on the server side, or different versions between + * client and server. + */ + class ArgumentDeserialisationException(cause: Throwable, val reqWithNoArguments: ClientToServer.RpcRequest) : RPCException("Failed to deserialise RPC arguments: version or app skew between client and server?", cause) } data class ArtemisProducer( 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 b4c61c2574..a36cc675e7 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 @@ -260,13 +260,31 @@ class RPCServer( private fun clientArtemisMessageHandler(artemisMessage: ClientMessage) { lifeCycle.requireState(State.STARTED) - val clientToServer = RPCApi.ClientToServer.fromClientMessage(RPC_SERVER_CONTEXT, artemisMessage) + + // Attempt de-serialisation of the RPC request message, in such a way that we can route the error back to + // the RPC that was being tried if it fails in a method/rpc specific way. + val clientToServerTry = Try.on { RPCApi.ClientToServer.fromClientMessage(RPC_SERVER_CONTEXT, artemisMessage) } + val clientToServer = try { + clientToServerTry.getOrThrow() + } catch (e: RPCApi.ArgumentDeserialisationException) { + // The exception itself has a more informative error message, and this could be caused by buggy apps, so + // let's just log it as a warning instead of an error. Relay the failure to the client so they can see it. + log.warn("Inbound RPC failed", e) + sendReply(e.reqWithNoArguments.id, e.reqWithNoArguments.clientAddress, Try.Failure(e.cause!!)) + return + } catch (e: Exception) { + // This path indicates something more fundamental went wrong, like a missing message header. + log.error("Failed to parse an inbound RPC: version skew between client and server?", e) + return + } finally { + artemisMessage.acknowledge() + } + + // Now try dispatching the request itself. log.debug { "-> RPC -> $clientToServer" } when (clientToServer) { is RPCApi.ClientToServer.RpcRequest -> { - val rpcContext = RpcContext( - currentUser = getUser(artemisMessage) - ) + val rpcContext = RpcContext(currentUser = getUser(artemisMessage)) rpcExecutor!!.submit { val result = invokeRpc(rpcContext, clientToServer.methodName, clientToServer.arguments) sendReply(clientToServer.id, clientToServer.clientAddress, result) From 9d115a2111dbcfa2f11e9689c2912a33ba213a6b Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Tue, 26 Sep 2017 10:53:07 +0100 Subject: [PATCH 002/185] Delay RPC arguments deserialisation to allow routing of errors --- .../net/corda/client/rpc/RPCStabilityTests.kt | 5 ++- .../rpc/internal/RPCClientProxyHandler.kt | 6 ++- .../java/net/corda/docs/FlowCookbookJava.java | 1 - .../main/kotlin/net/corda/nodeapi/RPCApi.kt | 41 +++++------------- .../node/services/messaging/RPCServer.kt | 43 ++++++++----------- 5 files changed, 36 insertions(+), 60 deletions(-) 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 4433ba03be..2e83e1f08a 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 @@ -7,6 +7,7 @@ import net.corda.core.internal.concurrent.fork import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.RPCOps import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.serialize import net.corda.core.utilities.* import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.nodeapi.RPCApi @@ -315,9 +316,9 @@ class RPCStabilityTests { clientAddress = SimpleString(myQueue), id = RPCApi.RpcRequestId(random63BitValue()), methodName = SlowConsumerRPCOps::streamAtInterval.name, - arguments = listOf(10.millis, 123456) + serialisedArguments = listOf(10.millis, 123456).serialize(context = SerializationDefaults.RPC_SERVER_CONTEXT).bytes ) - request.writeToClientMessage(SerializationDefaults.RPC_SERVER_CONTEXT, message) + request.writeToClientMessage(message) producer.send(message) session.commit() 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 10e901eb70..a777a0c96a 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 @@ -19,6 +19,7 @@ import net.corda.core.internal.LifeCycle import net.corda.core.internal.ThreadBox import net.corda.core.messaging.RPCOps import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.serialize import net.corda.core.utilities.Try import net.corda.core.utilities.debug import net.corda.core.utilities.getOrThrow @@ -208,11 +209,12 @@ class RPCClientProxyHandler( val rpcId = RPCApi.RpcRequestId(random63BitValue()) callSiteMap?.set(rpcId.toLong, Throwable("")) try { - val request = RPCApi.ClientToServer.RpcRequest(clientAddress, rpcId, method.name, arguments?.toList() ?: emptyList()) + val serialisedArguments = (arguments?.toList() ?: emptyList()).serialize(context = serializationContextWithObservableContext) + val request = RPCApi.ClientToServer.RpcRequest(clientAddress, rpcId, method.name, serialisedArguments.bytes) val replyFuture = SettableFuture.create() sessionAndProducerPool.run { val message = it.session.createMessage(false) - request.writeToClientMessage(serializationContextWithObservableContext, message) + request.writeToClientMessage(message) log.debug { val argumentsString = arguments?.joinToString() ?: "" 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/FlowCookbookJava.java index 6619fb47b9..370e69cbb9 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/FlowCookbookJava.java @@ -548,7 +548,6 @@ public class FlowCookbookJava { // DOCSTART 37 twiceSignedTx.checkSignaturesAreValid(); // DOCEND 37 - } catch (GeneralSecurityException e) { // Handle this as required. } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt b/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt index 7abeae27ad..d2de3777f6 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt @@ -97,20 +97,20 @@ object RPCApi { * @param clientAddress return address to contact the client at. * @param id a unique ID for the request, which the server will use to identify its response with. * @param methodName name of the method (procedure) to be called. - * @param arguments arguments to pass to the method, if any. + * @param serialisedArguments Serialised arguments to pass to the method, if any. */ data class RpcRequest( val clientAddress: SimpleString, val id: RpcRequestId, val methodName: String, - val arguments: List + val serialisedArguments: ByteArray ) : ClientToServer() { - fun writeToClientMessage(context: SerializationContext, message: ClientMessage) { + fun writeToClientMessage(message: ClientMessage) { MessageUtil.setJMSReplyTo(message, clientAddress) message.putIntProperty(TAG_FIELD_NAME, Tag.RPC_REQUEST.ordinal) message.putLongProperty(RPC_ID_FIELD_NAME, id.toLong) message.putStringProperty(METHOD_NAME_FIELD_NAME, methodName) - message.bodyBuffer.writeBytes(arguments.serialize(context = context).bytes) + message.bodyBuffer.writeBytes(serialisedArguments) } } @@ -128,26 +128,15 @@ object RPCApi { } companion object { - - fun fromClientMessage(context: SerializationContext, message: ClientMessage): ClientToServer { + fun fromClientMessage(message: ClientMessage): ClientToServer { val tag = Tag.values()[message.getIntProperty(TAG_FIELD_NAME)] return when (tag) { - RPCApi.ClientToServer.Tag.RPC_REQUEST -> { - val partialReq = RpcRequest( - clientAddress = MessageUtil.getJMSReplyTo(message), - id = RpcRequestId(message.getLongProperty(RPC_ID_FIELD_NAME)), - methodName = message.getStringProperty(METHOD_NAME_FIELD_NAME), - arguments = emptyList() - ) - // Deserialisation of the arguments can fail, but we'd like to return a response mapped to - // this specific RPC, so we throw the partial request with ID and method name. - try { - val arguments = message.getBodyAsByteArray().deserialize>(context = context) - return partialReq.copy(arguments = arguments) - } catch (t: Throwable) { - throw ArgumentDeserialisationException(t, partialReq) - } - } + RPCApi.ClientToServer.Tag.RPC_REQUEST -> RpcRequest( + clientAddress = MessageUtil.getJMSReplyTo(message), + id = RpcRequestId(message.getLongProperty(RPC_ID_FIELD_NAME)), + methodName = message.getStringProperty(METHOD_NAME_FIELD_NAME), + serialisedArguments = message.getBodyAsByteArray() + ) RPCApi.ClientToServer.Tag.OBSERVABLES_CLOSED -> { val ids = ArrayList() val buffer = message.bodyBuffer @@ -160,7 +149,6 @@ object RPCApi { } } } - } /** @@ -227,13 +215,6 @@ object RPCApi { } } } - - /** - * Thrown when the arguments passed to an RPC couldn't be deserialised by the server. This will - * typically indicate a missing application on the server side, or different versions between - * client and server. - */ - class ArgumentDeserialisationException(cause: Throwable, val reqWithNoArguments: ClientToServer.RpcRequest) : RPCException("Failed to deserialise RPC arguments: version or app skew between client and server?", cause) } data class ArtemisProducer( 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 a36cc675e7..70761cfc4e 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 @@ -19,6 +19,7 @@ import net.corda.core.internal.LifeCycle import net.corda.core.messaging.RPCOps import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationDefaults.RPC_SERVER_CONTEXT +import net.corda.core.serialization.deserialize import net.corda.core.utilities.Try import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor @@ -260,34 +261,26 @@ class RPCServer( private fun clientArtemisMessageHandler(artemisMessage: ClientMessage) { lifeCycle.requireState(State.STARTED) - - // Attempt de-serialisation of the RPC request message, in such a way that we can route the error back to - // the RPC that was being tried if it fails in a method/rpc specific way. - val clientToServerTry = Try.on { RPCApi.ClientToServer.fromClientMessage(RPC_SERVER_CONTEXT, artemisMessage) } - val clientToServer = try { - clientToServerTry.getOrThrow() - } catch (e: RPCApi.ArgumentDeserialisationException) { - // The exception itself has a more informative error message, and this could be caused by buggy apps, so - // let's just log it as a warning instead of an error. Relay the failure to the client so they can see it. - log.warn("Inbound RPC failed", e) - sendReply(e.reqWithNoArguments.id, e.reqWithNoArguments.clientAddress, Try.Failure(e.cause!!)) - return - } catch (e: Exception) { - // This path indicates something more fundamental went wrong, like a missing message header. - log.error("Failed to parse an inbound RPC: version skew between client and server?", e) - return - } finally { - artemisMessage.acknowledge() - } - - // Now try dispatching the request itself. + val clientToServer = RPCApi.ClientToServer.fromClientMessage(artemisMessage) log.debug { "-> RPC -> $clientToServer" } when (clientToServer) { is RPCApi.ClientToServer.RpcRequest -> { - val rpcContext = RpcContext(currentUser = getUser(artemisMessage)) - rpcExecutor!!.submit { - val result = invokeRpc(rpcContext, clientToServer.methodName, clientToServer.arguments) - sendReply(clientToServer.id, clientToServer.clientAddress, result) + val arguments = Try.on { + clientToServer.serialisedArguments.deserialize>(context = RPC_SERVER_CONTEXT) + } + when (arguments) { + is Try.Success -> { + val rpcContext = RpcContext(currentUser = getUser(artemisMessage)) + rpcExecutor!!.submit { + val result = invokeRpc(rpcContext, clientToServer.methodName, arguments.value) + sendReply(clientToServer.id, clientToServer.clientAddress, result) + } + } + is Try.Failure -> { + // We failed to deserialise the arguments, route back the error + log.warn("Inbound RPC failed", arguments.exception) + sendReply(clientToServer.id, clientToServer.clientAddress, arguments) + } } } is RPCApi.ClientToServer.ObservablesClosed -> { From 24ff7efd5f991a434f85413f1aa1f8c539be5e79 Mon Sep 17 00:00:00 2001 From: Clinton Date: Tue, 26 Sep 2017 17:15:11 +0100 Subject: [PATCH 003/185] Cordapps now have a name field (#1645) * Cordapps now have a name field. --- core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt | 2 ++ .../main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt | 3 +++ docs/source/changelog.rst | 2 ++ node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt | 1 + 4 files changed, 8 insertions(+) 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 ff9426ec90..2df896cbbe 100644 --- a/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt +++ b/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt @@ -13,6 +13,7 @@ import java.net.URL * * This will only need to be constructed manually for certain kinds of tests. * + * @property name Cordapp name - derived from the base name of the Cordapp JAR (therefore may not be unique) * @property contractClassNames List of contracts * @property initiatedFlows List of initiatable flow classes * @property rpcFlows List of RPC initiable flows classes @@ -22,6 +23,7 @@ import java.net.URL * @property jarPath The path to the JAR for this CorDapp */ interface Cordapp { + val name: String val contractClassNames: List val initiatedFlows: List>> val rpcFlows: List>> 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 4f7aecd6fe..38a5a0ecf4 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 @@ -5,6 +5,7 @@ import net.corda.core.flows.FlowLogic import net.corda.core.node.CordaPluginRegistry import net.corda.core.schemas.MappedSchema import net.corda.core.serialization.SerializeAsToken +import java.io.File import java.net.URL data class CordappImpl( @@ -15,6 +16,8 @@ data class CordappImpl( override val plugins: List, override val customSchemas: Set, override val jarPath: URL) : Cordapp { + override val name: String = File(jarPath.toURI()).name.removeSuffix(".jar") + /** * An exhaustive list of all classes relevant to the node within this CorDapp * diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 194a00deee..af1374367b 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -7,6 +7,8 @@ from the previous milestone release. UNRELEASED ---------- +* ``Cordapp`` now has a name field for identifying CorDapps and all CorDapp names are printed to console at startup. + Release 1.0 ----------- 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 6fb990d27e..0b645a2c48 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -263,6 +263,7 @@ open class NodeStartup(val args: Array) { node.configuration.extraAdvertisedServiceIds.let { if (it.isNotEmpty()) Node.printBasicNodeInfo("Providing network services", it.joinToString()) } + Node.printBasicNodeInfo("Loaded CorDapps", node.cordappProvider.cordapps.map { it.name }.joinToString()) val plugins = node.pluginRegistries .map { it.javaClass.name } .filterNot { it.startsWith("net.corda.node.") || it.startsWith("net.corda.core.") || it.startsWith("net.corda.nodeapi.") } From aff4d35ccbbbc73ca1556660ee3390d72fbd4711 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Tue, 26 Sep 2017 18:22:35 +0100 Subject: [PATCH 004/185] Enforce JSON serialized key format (#1606) Move Jackson public key encode/decode support away from Kryo serialization format for compactness and to DER format encoded as base64 for compatibility with other systems. --- .../corda/client/jackson/JacksonSupport.kt | 67 +++++++------------ .../client/jackson/JacksonSupportTest.kt | 30 +++++++-- .../net/corda/irs/simulation/trade.json | 6 +- 3 files changed, 54 insertions(+), 49 deletions(-) diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt index a9935f12fc..4fcd8415b4 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt @@ -28,8 +28,9 @@ import net.corda.core.transactions.NotaryChangeWireTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.OpaqueBytes -import net.corda.core.utilities.parsePublicKeyBase58 -import net.corda.core.utilities.toBase58String +import net.corda.core.utilities.base58ToByteArray +import net.corda.core.utilities.base64ToByteArray +import net.corda.core.utilities.toBase64 import net.i2p.crypto.eddsa.EdDSAPublicKey import java.math.BigDecimal import java.security.PublicKey @@ -83,13 +84,9 @@ object JacksonSupport { addDeserializer(SecureHash::class.java, SecureHashDeserializer()) addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer()) - // For ed25519 pubkeys - addSerializer(EdDSAPublicKey::class.java, PublicKeySerializer) - addDeserializer(EdDSAPublicKey::class.java, PublicKeyDeserializer) - - // For composite keys - addSerializer(CompositeKey::class.java, CompositeKeySerializer) - addDeserializer(CompositeKey::class.java, CompositeKeyDeserializer) + // Public key types + addSerializer(PublicKey::class.java, PublicKeySerializer) + addDeserializer(PublicKey::class.java, PublicKeyDeserializer) // For NodeInfo // TODO this tunnels the Kryo representation as a Base58 encoded string. Replace when RPC supports this. @@ -158,7 +155,7 @@ object JacksonSupport { object AnonymousPartySerializer : JsonSerializer() { override fun serialize(obj: AnonymousParty, generator: JsonGenerator, provider: SerializerProvider) { - generator.writeString(obj.owningKey.toBase58String()) + PublicKeySerializer.serialize(obj.owningKey, generator, provider) } } @@ -168,8 +165,7 @@ object JacksonSupport { parser.nextToken() } - // TODO this needs to use some industry identifier(s) instead of these keys - val key = parsePublicKeyBase58(parser.text) + val key = PublicKeyDeserializer.deserialize(parser, context) return AnonymousParty(key) } } @@ -187,19 +183,24 @@ object JacksonSupport { } val mapper = parser.codec as PartyObjectMapper - // TODO: We should probably have a better specified way of identifying X.500 names vs keys - // Base58 keys never include an equals character, while X.500 names always will, so we use that to determine - // how to parse the content - return if (parser.text.contains("=")) { + // The comma character is invalid in base64, and required as a separator for X.500 names. As Corda + // X.500 names all involve at least three attributes (organisation, locality, country), they must + // include a comma. As such we can use it as a distinguisher between the two types. + return if (parser.text.contains(",")) { val principal = CordaX500Name.parse(parser.text) mapper.wellKnownPartyFromX500Name(principal) ?: throw JsonParseException(parser, "Could not find a Party with name $principal") } else { val nameMatches = mapper.partiesFromName(parser.text) if (nameMatches.isEmpty()) { + val derBytes = try { + parser.text.base64ToByteArray() + } catch (e: AddressFormatException) { + throw JsonParseException(parser, "Could not find a matching party for '${parser.text}' and is not a base64 encoded public key: " + e.message) + } val key = try { - parsePublicKeyBase58(parser.text) + Crypto.decodePublicKey(derBytes) } catch (e: Exception) { - throw JsonParseException(parser, "Could not find a matching party for '${parser.text}' and is not a base58 encoded public key") + throw JsonParseException(parser, "Could not find a matching party for '${parser.text}' and is not a valid public key: " + e.message) } mapper.partyFromKey(key) ?: throw JsonParseException(parser, "Could not find a Party with key ${key.toStringShort()}") } else if (nameMatches.size == 1) { @@ -273,39 +274,23 @@ object JacksonSupport { } } - object PublicKeySerializer : JsonSerializer() { - override fun serialize(obj: EdDSAPublicKey, generator: JsonGenerator, provider: SerializerProvider) { - check(obj.params == Crypto.EDDSA_ED25519_SHA512.algSpec) - generator.writeString(obj.toBase58String()) + object PublicKeySerializer : JsonSerializer() { + override fun serialize(obj: PublicKey, generator: JsonGenerator, provider: SerializerProvider) { + generator.writeString(obj.encoded.toBase64()) } } - object PublicKeyDeserializer : JsonDeserializer() { - override fun deserialize(parser: JsonParser, context: DeserializationContext): EdDSAPublicKey { + object PublicKeyDeserializer : JsonDeserializer() { + override fun deserialize(parser: JsonParser, context: DeserializationContext): PublicKey { return try { - parsePublicKeyBase58(parser.text) as EdDSAPublicKey + val derBytes = parser.text.base64ToByteArray() + Crypto.decodePublicKey(derBytes) } catch (e: Exception) { throw JsonParseException(parser, "Invalid public key ${parser.text}: ${e.message}") } } } - object CompositeKeySerializer : JsonSerializer() { - override fun serialize(obj: CompositeKey, generator: JsonGenerator, provider: SerializerProvider) { - generator.writeString(obj.toBase58String()) - } - } - - object CompositeKeyDeserializer : JsonDeserializer() { - override fun deserialize(parser: JsonParser, context: DeserializationContext): CompositeKey { - return try { - parsePublicKeyBase58(parser.text) as CompositeKey - } catch (e: Exception) { - throw JsonParseException(parser, "Invalid composite key ${parser.text}: ${e.message}") - } - } - } - object AmountSerializer : JsonSerializer>() { override fun serialize(value: Amount<*>, gen: JsonGenerator, serializers: SerializerProvider) { gen.writeString(value.toString()) 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 961e2978ac..6f057b2ff5 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 @@ -14,15 +14,16 @@ import net.corda.testing.DUMMY_NOTARY import net.corda.testing.MINI_CORP import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.contracts.DummyContract -import net.i2p.crypto.eddsa.EdDSAPublicKey import org.junit.Before import org.junit.Test +import java.math.BigInteger +import java.security.PublicKey import java.util.* -import kotlin.reflect.jvm.jvmName import kotlin.test.assertEquals class JacksonSupportTest : TestDependencyInjectionBase() { companion object { + private val SEED = BigInteger.valueOf(20170922L) val mapper = JacksonSupport.createNonRpcMapper() } @@ -37,15 +38,34 @@ class JacksonSupportTest : TestDependencyInjectionBase() { } @Test - fun publicKeySerializingWorks() { - val publicKey = generateKeyPair().public + fun `should serialize Composite keys`() { + val expected = "\"MIHAMBUGE2mtoq+J1bjir/ONk6yd5pab0FoDgaYAMIGiAgECMIGcMDIDLQAwKjAFBgMrZXADIQAgIX1QlJRgaLlD0ttLlJF5kNqT/7P7QwCvrWc9+/248gIBATAyAy0AMCowBQYDK2VwAyEAqS0JPGlzdviBZjB9FaNY+w6cVs3/CQ2A5EimE9Lyng4CAQEwMgMtADAqMAUGAytlcAMhALq4GG0gBQZIlaKE6ucooZsuoKUbH4MtGSmA6cwj136+AgEB\"" + val innerKeys = (1..3).map { i -> + Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, SEED.plus(BigInteger.valueOf(i.toLong()))).public + } + // Build a 2 of 3 composite key + val publicKey = CompositeKey.Builder().let { + innerKeys.forEach { key -> it.addKey(key, 1) } + it.build(2) + } val serialized = mapper.writeValueAsString(publicKey) - val parsedKey = mapper.readValue(serialized, EdDSAPublicKey::class.java) + assertEquals(expected, serialized) + val parsedKey = mapper.readValue(serialized, PublicKey::class.java) assertEquals(publicKey, parsedKey) } private class Dummy(val notional: Amount) + @Test + fun `should serialize EdDSA keys`() { + val expected = "\"MCowBQYDK2VwAyEACFTgLk1NOqYXAfxLoR7ctSbZcl9KMXu58Mq31Kv1Dwk=\"" + val publicKey = Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, SEED).public + val serialized = mapper.writeValueAsString(publicKey) + assertEquals(expected, serialized) + val parsedKey = mapper.readValue(serialized, PublicKey::class.java) + assertEquals(publicKey, parsedKey) + } + @Test fun readAmount() { val oldJson = """ diff --git a/samples/irs-demo/src/main/resources/net/corda/irs/simulation/trade.json b/samples/irs-demo/src/main/resources/net/corda/irs/simulation/trade.json index 81bd5a4162..dc4a5be379 100644 --- a/samples/irs-demo/src/main/resources/net/corda/irs/simulation/trade.json +++ b/samples/irs-demo/src/main/resources/net/corda/irs/simulation/trade.json @@ -1,6 +1,6 @@ { "fixedLeg": { - "fixedRatePayer": "8Kqd4oWdx4KQGHGR7xcgpFf9JmP6HiXqTf85NpSgdSu431EGEhujA6ePaFD", + "fixedRatePayer": "MCowBQYDK2VwAyEAzswVB9wd3XKVlRwpCIjwla25BE0bc9aW5t8GXWg71Pw=", "notional": "$25000000", "paymentFrequency": "SemiAnnual", "effectiveDate": "2016-03-11", @@ -22,7 +22,7 @@ "interestPeriodAdjustment": "Adjusted" }, "floatingLeg": { - "floatingRatePayer": "8Kqd4oWdx4KQGHGJSFTX4kdZukmHohBRN3gvPekticL4eHTdmbJTVZNZJUj", + "floatingRatePayer": "MCowBQYDK2VwAyEAa3nFfmoJUjkoLASBjpYRLz8DpAAbqXpWTCOFKj8epfw=", "notional": { "quantity": 2500000000, "token": "USD" @@ -71,7 +71,7 @@ "notificationTime": "2:00pm London", "resolutionTime": "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ", "interestRate": { - "oracle": "Rates Service Provider", + "oracle": "C=ES,L=Madrid,O=Rates Service Provider", "tenor": { "name": "6M" }, From 8a842d1d531460e465631b350397842d2621f23c Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Wed, 27 Sep 2017 09:19:25 +0100 Subject: [PATCH 005/185] CORDA-540: Ensure that covariance of type is handled correctly when serializing with AMQP (#1631) --- .../amqp/DeserializationInput.kt | 16 ++- .../serialization/amqp/SerializationHelper.kt | 2 + .../serialization/amqp/SerializerFactory.kt | 2 + .../amqp/ListsSerializationJavaTest.java | 134 ++++++++++++++++++ .../serialization/ListsSerializationTest.kt | 18 +++ 5 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ListsSerializationJavaTest.java diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt index 7522844db0..af7fcb3f81 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt @@ -11,6 +11,8 @@ import org.apache.qpid.proton.codec.Data import java.io.NotSerializableException import java.lang.reflect.ParameterizedType import java.lang.reflect.Type +import java.lang.reflect.TypeVariable +import java.lang.reflect.WildcardType import java.nio.ByteBuffer data class ObjectAndEnvelope(val obj: T, val envelope: Envelope) @@ -142,10 +144,18 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) { } /** - * TODO: Currently performs rather basic checks aimed in particular at [java.util.List>] and - * [java.lang.Class] + * Currently performs checks aimed at: + * * [java.util.List>] and [java.lang.Class] + * * [T : Parent] and [Parent] + * * [? extends Parent] and [Parent] + * * In the future tighter control might be needed */ private fun Type.materiallyEquivalentTo(that: Type): Boolean = - asClass() == that.asClass() && that is ParameterizedType + when(that) { + is ParameterizedType -> asClass() == that.asClass() + is TypeVariable<*> -> isSubClassOf(that.bounds.first()) + is WildcardType -> isSubClassOf(that.upperBounds.first()) + else -> false + } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt index 2cc91ecb88..45df43799e 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt @@ -189,6 +189,8 @@ internal fun Type.asClass(): Class<*>? { 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 } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt index e7a59f8d5c..0077285e77 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt @@ -105,6 +105,8 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) { val declaredComponent = declaredType.genericComponentType inferTypeVariables(actualClass?.componentType, declaredComponent.asClass()!!, declaredComponent)?.asArray() } + is TypeVariable<*> -> actualClass + is WildcardType -> actualClass else -> null } diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ListsSerializationJavaTest.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ListsSerializationJavaTest.java new file mode 100644 index 0000000000..bcd1d1f1f7 --- /dev/null +++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ListsSerializationJavaTest.java @@ -0,0 +1,134 @@ +package net.corda.nodeapi.internal.serialization.amqp; + +import net.corda.core.serialization.CordaSerializable; +import net.corda.core.serialization.SerializedBytes; +import net.corda.nodeapi.internal.serialization.AllWhitelist; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +public class ListsSerializationJavaTest { + + @CordaSerializable + interface Parent {} + + public static class Child implements Parent { + private final int value; + + Child(int value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Child child = (Child) o; + + return value == child.value; + } + + @Override + public int hashCode() { + return value; + } + + // Needed to show that there is a property called "value" + @SuppressWarnings("unused") + public int getValue() { + return value; + } + } + + @CordaSerializable + public static class CovariantContainer { + private final List content; + + CovariantContainer(List content) { + this.content = content; + } + + @Override + @SuppressWarnings("unchecked") + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CovariantContainer that = (CovariantContainer) o; + + return content != null ? content.equals(that.content) : that.content == null; + } + + @Override + public int hashCode() { + return content != null ? content.hashCode() : 0; + } + + // Needed to show that there is a property called "content" + @SuppressWarnings("unused") + public List getContent() { + return content; + } + } + + @CordaSerializable + public static class CovariantContainer2 { + private final List content; + + CovariantContainer2(List content) { + this.content = content; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CovariantContainer2 that = (CovariantContainer2) o; + + return content != null ? content.equals(that.content) : that.content == null; + } + + @Override + public int hashCode() { + return content != null ? content.hashCode() : 0; + } + + // Needed to show that there is a property called "content" + @SuppressWarnings("unused") + public List getContent() { + return content; + } + } + + @Test + public void checkCovariance() throws Exception { + List payload = new ArrayList<>(); + payload.add(new Child(1)); + payload.add(new Child(2)); + CovariantContainer container = new CovariantContainer<>(payload); + assertEqualAfterRoundTripSerialization(container, CovariantContainer.class); + } + + @Test + public void checkCovariance2() throws Exception { + List payload = new ArrayList<>(); + payload.add(new Child(1)); + payload.add(new Child(2)); + CovariantContainer2 container = new CovariantContainer2(payload); + assertEqualAfterRoundTripSerialization(container, CovariantContainer2.class); + } + + // Have to have own version as Kotlin inline functions cannot be easily called from Java + private static void assertEqualAfterRoundTripSerialization(T container, Class clazz) throws Exception { + SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader()); + SerializationOutput ser = new SerializationOutput(factory1); + SerializedBytes bytes = ser.serialize(container); + DeserializationInput des = new DeserializationInput(factory1); + T deserialized = des.deserialize(bytes, clazz); + Assert.assertEquals(container, deserialized); + } +} \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt index 07780c9fd1..176f7468a0 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt @@ -68,6 +68,24 @@ class ListsSerializationTest : TestDependencyInjectionBase() { Assertions.assertThatThrownBy { wrongPayloadType.serialize() } .isInstanceOf(NotSerializableException::class.java).hasMessageContaining("Cannot derive collection type for declaredType") } + + @CordaSerializable + interface Parent + + data class Child(val value: Int) : Parent + + @CordaSerializable + data class CovariantContainer(val payload: List) + + @Test + fun `check covariance`() { + val payload = ArrayList() + payload.add(Child(1)) + payload.add(Child(2)) + val container = CovariantContainer(payload) + assertEqualAfterRoundTripSerialization(container) + } + } internal inline fun assertEqualAfterRoundTripSerialization(obj: T) { From d3e1a8e1c03d39bb16ff3d484542aaa6c6b60370 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 26 Sep 2017 17:17:58 +0200 Subject: [PATCH 006/185] Minor: improve javadocs in NodeInfo --- .../kotlin/net/corda/core/node/NodeInfo.kt | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt index 4abfca9b92..0022d556ef 100644 --- a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt +++ b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt @@ -6,21 +6,34 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.NetworkHostAndPort /** - * Info about a network node that acts on behalf of some form of contract party. - * @param legalIdentitiesAndCerts is a non-empty list, where the first identity is assumed to be the default identity of the node. + * Information about a network node that acts on behalf of some party. NodeInfos can be found via the network map + * cache, accessible from a [net.corda.core.node.services.NetworkMapCache]. + * + * @property addresses An ordered list of IP addresses/hostnames where the node can be contacted. + * @property legalIdentitiesAndCerts A non-empty list, where the first identity is assumed to be the default identity of the node. + * @property platformVersion An integer representing the set of protocol features the node supports. See the docsite + * for information on how the platform is versioned. + * @property serial An arbitrary number incremented each time the NodeInfo is changed. This is analogous to the same + * concept in DNS. */ -// TODO We currently don't support multi-IP/multi-identity nodes, we only left slots in the data structures. @CordaSerializable data class NodeInfo(val addresses: List, val legalIdentitiesAndCerts: List, val platformVersion: Int, val serial: Long ) { + // TODO We currently don't support multi-IP/multi-identity nodes, we only left slots in the data structures. init { require(legalIdentitiesAndCerts.isNotEmpty()) { "Node should have at least one legal identity" } } @Transient private var _legalIdentities: List? = null + + /** + * An ordered list of legal identities supported by this node. The node will always have at least one, so if you + * are porting code from earlier versions of Corda that expected a single party per node, just use the first item + * in the returned list. + */ val legalIdentities: List get() { return _legalIdentities ?: legalIdentitiesAndCerts.map { it.party }.also { _legalIdentities = it } } From e49da94418193e6464a72d1a13b1ede2b6ad26c5 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 26 Sep 2017 18:12:03 +0200 Subject: [PATCH 007/185] Minor: use package descriptions in Kotlin build of api docs too, not just javadocs. --- docs/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/build.gradle b/docs/build.gradle index daefe91e23..6112f63647 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -10,6 +10,7 @@ dokka { outputDirectory = file("${rootProject.rootDir}/docs/build/html/api/kotlin") processConfigurations = ['compile'] sourceDirs = files('../core/src/main/kotlin', '../client/jfx/src/main/kotlin', '../client/mock/src/main/kotlin', '../client/rpc/src/main/kotlin', '../node-api/src/main/kotlin', '../finance/src/main/kotlin', '../client/jackson/src/main/kotlin', '../testing/node-driver/src/main/kotlin', '../testing/test-utils/src/main/kotlin') + includes = ['packages.md'] jdkVersion = 8 externalDocumentationLink { From c79d14cb6eba963a92758906bce650a4ff34a367 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 26 Sep 2017 18:13:22 +0200 Subject: [PATCH 008/185] RPC: make RPCConnection non-internal, as it's a core API. Move docs around so they're on public API not internal API. --- .../client/rpc/CordaRPCJavaClientTest.java | 3 +- .../net/corda/client/rpc/CordaRPCClient.kt | 73 +++++++++++++++---- .../net/corda/client/rpc/RPCConnection.kt | 38 ++++++++++ .../corda/client/rpc/internal/RPCClient.kt | 56 +------------- 4 files changed, 99 insertions(+), 71 deletions(-) create mode 100644 client/rpc/src/main/kotlin/net/corda/client/rpc/RPCConnection.kt 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 90e4b8faca..1a6ce172cd 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 @@ -1,6 +1,5 @@ package net.corda.client.rpc; -import net.corda.client.rpc.internal.RPCClient; import net.corda.core.concurrent.CordaFuture; import net.corda.core.contracts.Amount; import net.corda.core.messaging.CordaRPCOps; @@ -42,7 +41,7 @@ public class CordaRPCJavaClientTest extends NodeBasedTest { private StartedNode node; private CordaRPCClient client; - private RPCClient.RPCConnection connection = null; + private RPCConnection connection = null; private CordaRPCOps rpcProxy; private void login(String username, String password) { 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 e5ac356d76..635db3afea 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 @@ -10,31 +10,62 @@ import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.internal.serialization.KRYO_RPC_CLIENT_CONTEXT import java.time.Duration -/** @see RPCClient.RPCConnection */ -class CordaRPCConnection internal constructor( - connection: RPCClient.RPCConnection -) : RPCClient.RPCConnection by connection +/** + * This class is essentially just a wrapper for an RPCConnection and can be treated identically. + * + * @see RPCConnection + */ +class CordaRPCConnection internal constructor(connection: RPCConnection) : RPCConnection by connection -/** @see RPCClientConfiguration */ -data class CordaRPCClientConfiguration( - val connectionMaxRetryInterval: Duration -) { +/** + * Can be used to configure the RPC client connection. + * + * @property connectionMaxRetryInterval How much time to wait between connection retries if the server goes down. This + * time will be reached via exponential backoff. + */ +data class CordaRPCClientConfiguration(val connectionMaxRetryInterval: Duration) { internal fun toRpcClientConfiguration(): RPCClientConfiguration { return RPCClientConfiguration.default.copy( connectionMaxRetryInterval = connectionMaxRetryInterval ) } + companion object { + /** + * Returns the default configuration we recommend you use. + */ @JvmStatic - val default = CordaRPCClientConfiguration( - connectionMaxRetryInterval = RPCClientConfiguration.default.connectionMaxRetryInterval - ) + val default = CordaRPCClientConfiguration(connectionMaxRetryInterval = RPCClientConfiguration.default.connectionMaxRetryInterval) } } -/** @see RPCClient */ -//TODO Add SSL support -class CordaRPCClient( +/** + * An RPC client connects to the specified server and allows you to make calls to the server that perform various + * useful tasks. Please see the Client RPC section of docs.corda.net to learn more about how this API works. A brief + * description is provided here. + * + * Calling [start] returns an [RPCConnection] containing a proxy that lets you invoke RPCs on the server. Calls on + * it block, and if the server throws an exception then it will be rethrown on the client. Proxies are thread safe and + * may be used to invoke multiple RPCs in parallel. + * + * RPC sends and receives are logged on the net.corda.rpc logger. + * + * The [CordaRPCOps] defines what client RPCs are available. If an RPC returns an [rx.Observable] anywhere in the object + * graph returned then the server-side observable is transparently forwarded to the client side here. + * *You are expected to use it*. The server will begin sending messages immediately that will be buffered on the + * client, you are expected to drain by subscribing to the returned observer. You can opt-out of this by simply + * calling the [net.corda.client.rpc.notUsed] method on it. + * + * You don't have to explicitly close the observable if you actually subscribe to it: it will close itself and free up + * the server-side resources either when the client or JVM itself is shutdown, or when there are no more subscribers to + * it. Once all the subscribers to a returned observable are unsubscribed or the observable completes successfully or + * with an error, the observable is closed and you can't then re-subscribe again: you'll have to re-request a fresh + * observable with another RPC. + * + * @param hostAndPort The network address to connect to. + * @param configuration An optional configuration used to tweak client behaviour. + */ +class CordaRPCClient @JvmOverloads constructor( hostAndPort: NetworkHostAndPort, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default, initialiseSerialization: Boolean = true @@ -54,10 +85,24 @@ class CordaRPCClient( KRYO_RPC_CLIENT_CONTEXT ) + /** + * 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. + * @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): CordaRPCConnection { return CordaRPCConnection(rpcClient.start(CordaRPCOps::class.java, username, password)) } + /** + * A helper for Kotlin users that simply closes the connection after the block has executed. Be careful not to + * over-use this, as setting up and closing connections takes time. + */ inline fun use(username: String, password: String, block: (CordaRPCConnection) -> A): A { return start(username, password).use(block) } diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/RPCConnection.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/RPCConnection.kt new file mode 100644 index 0000000000..ae53adee53 --- /dev/null +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/RPCConnection.kt @@ -0,0 +1,38 @@ +package net.corda.client.rpc + +import net.corda.core.messaging.RPCOps +import java.io.Closeable + +/** + * Holds a [proxy] object implementing [I] that forwards requests to the RPC server. The server version can be queried + * via this interface. + * + * [Closeable.close] may be used to shut down the connection and release associated resources. It is an + * alias for [notifyServerAndClose]. + */ +interface RPCConnection : Closeable { + /** + * Holds a synthetic class that automatically forwards method calls to the server, and returns the response. + */ + val proxy: I + + /** The RPC protocol version reported by the server. */ + val serverProtocolVersion: Int + + /** + * Closes this client gracefully by sending a notification to the server, so it can immediately clean up resources. + * If the server is not available this method may block for a short period until it's clear the server is not + * coming back. + */ + fun notifyServerAndClose() + + /** + * Closes this client without notifying the server. + * + * The server will eventually clear out the RPC message queue and disconnect subscribed observers, + * but this may take longer than desired, so to conserve resources you should normally use [notifyServerAndClose]. + * This method is helpful when the node may be shutting down or have already shut down and you don't want to + * block waiting for it to come back, which typically happens in integration tests and demos rather than production. + */ + fun forceClose() +} \ 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 f0fc941668..d4716f947f 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 @@ -1,5 +1,6 @@ package net.corda.client.rpc.internal +import net.corda.client.rpc.RPCConnection import net.corda.client.rpc.RPCException import net.corda.core.crypto.random63BitValue import net.corda.core.internal.logElapsedTime @@ -17,7 +18,6 @@ import net.corda.nodeapi.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 -import java.io.Closeable import java.lang.reflect.Proxy import java.time.Duration @@ -79,12 +79,6 @@ data class RPCClientConfiguration( } } -/** - * An RPC client that may be used to create connections to an RPC server. - * - * @param transport The Artemis transport to use to connect to the server. - * @param rpcConfiguration Configuration used to tweak client behaviour. - */ class RPCClient( val transport: TransportConfiguration, val rpcConfiguration: RPCClientConfiguration = RPCClientConfiguration.default, @@ -101,54 +95,6 @@ class RPCClient( private val log = loggerFor>() } - /** - * Holds a proxy object implementing [I] that forwards requests to the RPC server. - * - * [Closeable.close] may be used to shut down the connection and release associated resources. - */ - interface RPCConnection : Closeable { - val proxy: I - /** The RPC protocol version reported by the server */ - val serverProtocolVersion: Int - - /** - * Closes this client without notifying the server. - * The server will eventually clear out the RPC message queue and disconnect subscribed observers, - * but this may take longer than desired, so to conserve resources you should normally use [notifyServerAndClose]. - * This method is helpful when the node may be shutting down or - * have already shut down and you don't want to block waiting for it to come back. - */ - fun forceClose() - - /** - * Closes this client gracefully by sending a notification to the server, so it can immediately clean up resources. - * If the server is not available this method may block for a short period until it's clear the server is not coming back. - */ - fun notifyServerAndClose() - } - - /** - * Returns an [RPCConnection] containing a proxy that lets you invoke RPCs on the server. Calls on it block, and if - * the server throws an exception then it will be rethrown on the client. Proxies are thread safe and may be used to - * invoke multiple RPCs in parallel. - * - * RPC sends and receives are logged on the net.corda.rpc logger. - * - * The [RPCOps] defines what client RPCs are available. If an RPC returns an [Observable] anywhere in the object - * graph returned then the server-side observable is transparently forwarded to the client side here. - * *You are expected to use it*. The server will begin sending messages immediately that will be buffered on the - * client, you are expected to drain by subscribing to the returned observer. You can opt-out of this by simply - * calling the [net.corda.client.rpc.notUsed] method on it. You don't have to explicitly close the observable if you actually - * subscribe to it: it will close itself and free up the server-side resources either when the client or JVM itself - * is shutdown, or when there are no more subscribers to it. Once all the subscribers to a returned observable are - * unsubscribed or the observable completes successfully or with an error, the observable is closed and you can't - * then re-subscribe again: you'll have to re-request a fresh observable with another RPC. - * - * @param rpcOpsClass The [Class] of the RPC interface. - * @param username The username to authenticate with. - * @param password The password to authenticate with. - * @throws RPCException if the server version is too low or if the server isn't reachable within the given time. - */ fun start( rpcOpsClass: Class, username: String, From 7a082a659877b89759414504f5992de583983131 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 26 Sep 2017 18:20:58 +0200 Subject: [PATCH 009/185] Add an IntelliJ scope that covers the currently supported Corda API. This is useful when used in combination with the "Highlight public declarations with missing KDoc" inspection. --- .idea/scopes/Corda_API.xml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .idea/scopes/Corda_API.xml diff --git a/.idea/scopes/Corda_API.xml b/.idea/scopes/Corda_API.xml new file mode 100644 index 0000000000..fa137ebcf0 --- /dev/null +++ b/.idea/scopes/Corda_API.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file From b75c9f9254cefee0c28bfe66eae87186a86ae0f5 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 26 Sep 2017 18:32:05 +0200 Subject: [PATCH 010/185] Ironic: upgrade the version of the Gradle plugin that checks for upgraded versions of things. It had broken due being incompatible with the new versions of Gradle itself. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f32dfbe9f7..0466b24455 100644 --- a/build.gradle +++ b/build.gradle @@ -59,7 +59,7 @@ buildscript { classpath "net.corda.plugins:publish-utils:$gradle_plugins_version" classpath "net.corda.plugins:quasar-utils:$gradle_plugins_version" classpath "net.corda.plugins:cordformation:$gradle_plugins_version" - classpath 'com.github.ben-manes:gradle-versions-plugin:0.13.0' + classpath 'com.github.ben-manes:gradle-versions-plugin:0.15.0' classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version" classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}" classpath "org.ajoberstar:grgit:1.1.0" From 6686b054a25a793c674aa0872af483b86e1ed521 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 26 Sep 2017 18:38:03 +0200 Subject: [PATCH 011/185] Docs: flesh out javadocs on ServiceHub --- .../kotlin/net/corda/core/node/ServiceHub.kt | 65 ++++++++++++++++--- .../core/node/services/VaultQueryService.kt | 4 +- .../corda/core/node/services/VaultService.kt | 1 - 3 files changed, 60 insertions(+), 10 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index cf484b6b24..99960d04ca 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -6,6 +6,7 @@ import net.corda.core.crypto.Crypto import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.TransactionSignature +import net.corda.core.flows.ContractUpgradeFlow import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party @@ -22,10 +23,13 @@ import java.time.Clock /** * Subset of node services that are used for loading transactions from the wire into fully resolved, looked up * forms ready for verification. - * - * @see ServiceHub */ interface ServicesForResolution { + /** + * An identity service maintains a directory of parties by their associated distinguished name/public keys and thus + * supports lookup of a party given its key, or name. The service also manages the certificates linking confidential + * identities back to the well known identity (i.e. the identity in the network map) of a party. + */ val identityService: IdentityService /** Provides access to storage of arbitrary JAR files (which may contain only data, no code). */ @@ -44,17 +48,42 @@ interface ServicesForResolution { } /** - * A service hub simply vends references to the other services a node has. Some of those services may be missing or - * mocked out. This class is useful to pass to chunks of pluggable code that might have need of many different kinds of - * functionality and you don't want to hard-code which types in the interface. + * A service hub is the starting point for most operations you can do inside the node. You are provided with one + * when a class annotated with [CordaService] is constructed, and you have access to one inside flows. Most RPCs + * simply forward to the services found here after some access checking. * - * Any services exposed to flows (public view) need to implement [SerializeAsToken] or similar to avoid their internal - * state from being serialized in checkpoints. + * The APIs are organised roughly by category, with a few very important top level APIs available on the ServiceHub + * itself. Inside a flow, it's safe to keep a reference to services found here on the stack: checkpointing will do the + * right thing (it won't try to serialise the internals of the service). + * + * In unit test environments, some of those services may be missing or mocked out. */ interface ServiceHub : ServicesForResolution { + // NOTE: Any services exposed to flows (public view) need to implement [SerializeAsToken] or similar to avoid + // their internal state from being serialized in checkpoints. + + /** + * The vault service lets you observe, soft lock and add notes to states that involve you or are relevant to your + * node in some way. + */ val vaultService: VaultService + /** The vault query service lets you select and track states that correspond to various criteria. */ val vaultQueryService: VaultQueryService + /** + * The key management service is responsible for storing and using private keys to sign things. An + * implementation of this may, for example, call out to a hardware security module that enforces various + * auditing and frequency-of-use requirements. + * + * You don't normally need to use this directly. If you have a [TransactionBuilder] and wish to sign it to + * get a [SignedTransaction], look at [signInitialTransaction]. + */ val keyManagementService: KeyManagementService + /** + * The [ContractUpgradeService] is responsible for securely upgrading contract state objects according to + * a specified and mutually agreed (amongst participants) contract version. + * + * @see ContractUpgradeFlow to understand the workflow associated with contract upgrades. + */ val contractUpgradeService: ContractUpgradeService /** @@ -64,9 +93,26 @@ interface ServiceHub : ServicesForResolution { */ val validatedTransactions: TransactionStorage + /** + * A network map contains lists of nodes on the network along with information about their identity keys, services + * they provide and host names or IP addresses where they can be connected to. The cache wraps around a map fetched + * from an authoritative service, and adds easy lookup of the data stored within it. Generally it would be initialised + * with a specified network map service, which it fetches data from and then subscribes to updates of. + */ val networkMapCache: NetworkMapCache + + /** + * Provides verification service. The implementation may be a simple in-memory verify() call or perhaps an IPC/RPC. + */ val transactionVerifierService: TransactionVerifierService + + /** + * A [Clock] representing the node's current time. This should be used in preference to directly accessing the + * clock so the current time can be controlled during unit testing. + */ val clock: Clock + + /** The [NodeInfo] object corresponding to our own entry in the network map. */ val myInfo: NodeInfo /** @@ -268,9 +314,12 @@ interface ServiceHub : ServicesForResolution { /** * Exposes a JDBC connection (session) object using the currently configured database. * Applications can use this to execute arbitrary SQL queries (native, direct, prepared, callable) - * against its Node database tables (including custom contract tables defined by extending [Queryable]). + * against its Node database tables (including custom contract tables defined by extending + * [net.corda.core.schemas.QueryableState]). + * * When used within a flow, this session automatically forms part of the enclosing flow transaction boundary, * and thus queryable data will include everything committed as of the last checkpoint. + * * @throws IllegalStateException if called outside of a transaction. * @return A new [Connection] */ diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultQueryService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultQueryService.kt index 2cfacd51f5..deace9212f 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultQueryService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultQueryService.kt @@ -7,8 +7,10 @@ import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.Sort +/** + * The vault query service lets you select and track states that correspond to various criteria. + */ interface VaultQueryService { - // DOCSTART VaultQueryAPI /** * Generic vault query function which takes a [QueryCriteria] object to define filters, 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 754bbd69fd..a2353a7b99 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 @@ -149,7 +149,6 @@ class Vault(val states: Iterable>) { * Note that transactions we've seen are held by the storage service, not the vault. */ interface VaultService { - /** * Prefer the use of [updates] unless you know why you want to use this instead. * From df453bdaa874f32b01e3bf1246b715645a40885b Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 26 Sep 2017 18:56:08 +0200 Subject: [PATCH 012/185] Docs: add @suppress to a few things that were polluting the Dokka docs. --- core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt | 3 ++- core/src/main/kotlin/net/corda/core/node/ServiceHub.kt | 3 ++- .../net/corda/core/node/services/TransactionVerifierService.kt | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) 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 cbb80eb2ae..0ef6a73f84 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -235,11 +235,12 @@ fun Class.castIfPossible(obj: Any): T? = if (isInstance(obj)) cast(obj) e fun Class<*>.staticField(name: String): DeclaredField = DeclaredField(this, name, null) /** Returns a [DeclaredField] wrapper around the declared (possibly non-public) static field of the receiver [KClass]. */ fun KClass<*>.staticField(name: String): DeclaredField = DeclaredField(java, name, null) -/** Returns a [DeclaredField] wrapper around the declared (possibly non-public) instance field of the receiver object. */ +/** @suppress Returns a [DeclaredField] wrapper around the declared (possibly non-public) instance field of the receiver object. */ fun Any.declaredField(name: String): DeclaredField = DeclaredField(javaClass, name, this) /** * Returns a [DeclaredField] wrapper around the (possibly non-public) instance field of the receiver object, but declared * in its superclass [clazz]. + * @suppress */ fun Any.declaredField(clazz: KClass<*>, name: String): DeclaredField = DeclaredField(clazz.java, name, this) diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index 99960d04ca..f4c0f205cd 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -102,7 +102,8 @@ interface ServiceHub : ServicesForResolution { val networkMapCache: NetworkMapCache /** - * Provides verification service. The implementation may be a simple in-memory verify() call or perhaps an IPC/RPC. + * INTERNAL. DO NOT USE. + * @suppress */ val transactionVerifierService: TransactionVerifierService diff --git a/core/src/main/kotlin/net/corda/core/node/services/TransactionVerifierService.kt b/core/src/main/kotlin/net/corda/core/node/services/TransactionVerifierService.kt index 99f40a0492..8cc5c7af65 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/TransactionVerifierService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/TransactionVerifierService.kt @@ -5,6 +5,7 @@ import net.corda.core.transactions.LedgerTransaction /** * Provides verification service. The implementation may be a simple in-memory verify() call or perhaps an IPC/RPC. + * @suppress */ interface TransactionVerifierService { /** From 1b349214ff3836b8d2c4ce2f7a654cceb553802c Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Wed, 27 Sep 2017 11:46:16 +0200 Subject: [PATCH 013/185] Docs: mention RPC access in NodeInfo javadoc --- core/src/main/kotlin/net/corda/core/node/NodeInfo.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt index 0022d556ef..d5ef1df776 100644 --- a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt +++ b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt @@ -7,7 +7,8 @@ import net.corda.core.utilities.NetworkHostAndPort /** * Information about a network node that acts on behalf of some party. NodeInfos can be found via the network map - * cache, accessible from a [net.corda.core.node.services.NetworkMapCache]. + * cache, accessible from a [net.corda.core.node.services.NetworkMapCache]. They are also available via RPC + * using the [net.corda.core.messaging.CordaRPCOps.networkMapSnapshot] method. * * @property addresses An ordered list of IP addresses/hostnames where the node can be contacted. * @property legalIdentitiesAndCerts A non-empty list, where the first identity is assumed to be the default identity of the node. From c6c4c13bee3fe7794901a02ff312ba4c6a597034 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Wed, 27 Sep 2017 11:32:10 +0100 Subject: [PATCH 014/185] Declare this internal message string as "const". (#1674) --- .../main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt b/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt index 548521a2e8..dedc468904 100644 --- a/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt +++ b/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt @@ -28,7 +28,7 @@ fun firstOf(vararg futures: CordaFuture, handler: (CordaFuture firstOf(futures: Array>, log: Logger, handler: (CordaFuture) -> W): CordaFuture { val resultFuture = openFuture() From 762b2a5123208e61d2398895177fdbb757733b9e Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Wed, 27 Sep 2017 10:40:16 +0100 Subject: [PATCH 015/185] More memory to attachment demo test nodes --- .../attachmentdemo/AttachmentDemoTest.kt | 9 ++-- .../corda/attachmentdemo/AttachmentDemo.kt | 1 + .../kotlin/net/corda/testing/driver/Driver.kt | 50 +++++++++++-------- .../corda/testing/driver/ProcessUtilities.kt | 12 +++-- 4 files changed, 43 insertions(+), 29 deletions(-) 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 eaccdee089..3f3458997b 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 @@ -8,6 +8,7 @@ import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_BANK_B import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.driver import org.junit.Test import java.util.concurrent.CompletableFuture.supplyAsync @@ -16,11 +17,11 @@ class AttachmentDemoTest { // run with a 10,000,000 bytes in-memory zip file. In practice, a slightly bigger file will be used (~10,002,000 bytes). @Test fun `attachment demo using a 10MB zip file`() { val numOfExpectedBytes = 10_000_000 - driver(dsl = { + driver(isDebug = true, portAllocation = PortAllocation.Incremental(20000)) { val demoUser = listOf(User("demo", "demo", setOf(startFlowPermission()))) val (nodeA, nodeB) = listOf( - startNode(providedName = DUMMY_BANK_A.name, rpcUsers = demoUser), - startNode(providedName = DUMMY_BANK_B.name, rpcUsers = demoUser), + startNode(providedName = DUMMY_BANK_A.name, rpcUsers = demoUser, maximumHeapSize = "1g"), + startNode(providedName = DUMMY_BANK_B.name, rpcUsers = demoUser, maximumHeapSize = "1g"), startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))) .map { it.getOrThrow() } startWebserver(nodeB).getOrThrow() @@ -39,6 +40,6 @@ class AttachmentDemoTest { senderThread.getOrThrow() recipientThread.getOrThrow() - }, isDebug = true) + } } } diff --git a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt index 8eba73075f..833d1bec1c 100644 --- a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt +++ b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt @@ -93,6 +93,7 @@ private fun sender(rpc: CordaRPCOps, inputStream: InputStream, hash: SecureHash. // Make sure we have the file in storage if (!rpc.attachmentExists(hash)) { inputStream.use { + val avail = inputStream.available() val id = rpc.uploadAttachment(it) require(hash == id) { "Id was '$id' instead of '$hash'" } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index c65f5ffcea..c7ad45cdf0 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -94,7 +94,8 @@ interface DriverDSLExposedInterface : CordformContext { rpcUsers: List = defaultParameters.rpcUsers, verifierType: VerifierType = defaultParameters.verifierType, customOverrides: Map = defaultParameters.customOverrides, - startInSameProcess: Boolean? = defaultParameters.startInSameProcess): CordaFuture + startInSameProcess: Boolean? = defaultParameters.startInSameProcess, + maximumHeapSize: String = defaultParameters.maximumHeapSize): CordaFuture /** * Helper function for starting a [node] with custom parameters from Java. @@ -109,7 +110,8 @@ interface DriverDSLExposedInterface : CordformContext { fun startNodes( nodes: List, - startInSameProcess: Boolean? = null + startInSameProcess: Boolean? = null, + maximumHeapSize: String = "200m" ): List> /** @@ -137,7 +139,7 @@ interface DriverDSLExposedInterface : CordformContext { * * @param handle The handle for the node that this webserver connects to via RPC. */ - fun startWebserver(handle: NodeHandle): CordaFuture + fun startWebserver(handle: NodeHandle, maximumHeapSize: String = "200m"): CordaFuture /** * Starts a network map service node. Note that only a single one should ever be running, so you will probably want @@ -145,7 +147,7 @@ interface DriverDSLExposedInterface : CordformContext { * @param startInProcess Determines if the node should be started inside this process. If null the Driver-level * value will be used. */ - fun startDedicatedNetworkMapService(startInProcess: Boolean? = null): CordaFuture + fun startDedicatedNetworkMapService(startInProcess: Boolean? = null, maximumHeapSize: String = "200m"): CordaFuture fun waitForAllNodesToFinish() @@ -250,7 +252,7 @@ sealed class PortAllocation { } /** - * Helper builder for configuring a [node] from Java. + * Helper builder for configuring a [Node] from Java. */ data class NodeParameters( val providedName: CordaX500Name? = null, @@ -258,7 +260,8 @@ data class NodeParameters( val rpcUsers: List = emptyList(), val verifierType: VerifierType = VerifierType.InMemory, val customOverrides: Map = emptyMap(), - val startInSameProcess: Boolean? = null + val startInSameProcess: Boolean? = null, + val maximumHeapSize: String = "200m" ) { fun setProvidedName(providedName: CordaX500Name?) = copy(providedName = providedName) fun setAdvertisedServices(advertisedServices: Set) = copy(advertisedServices = advertisedServices) @@ -266,6 +269,7 @@ data class NodeParameters( fun setVerifierType(verifierType: VerifierType) = copy(verifierType = verifierType) fun setCustomerOverrides(customOverrides: Map) = copy(customOverrides = customOverrides) fun setStartInSameProcess(startInSameProcess: Boolean?) = copy(startInSameProcess = startInSameProcess) + fun setMaximumHeapSize(maximumHeapSize: String) = copy(maximumHeapSize = maximumHeapSize) } /** @@ -674,7 +678,8 @@ class DriverDSL( rpcUsers: List, verifierType: VerifierType, customOverrides: Map, - startInSameProcess: Boolean? + startInSameProcess: Boolean?, + maximumHeapSize: String ): CordaFuture { val p2pAddress = portAllocation.nextHostAndPort() val rpcAddress = portAllocation.nextHostAndPort() @@ -700,10 +705,10 @@ class DriverDSL( "verifierType" to verifierType.name ) + customOverrides ) - return startNodeInternal(config, webAddress, startInSameProcess) + return startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize) } - override fun startNodes(nodes: List, startInSameProcess: Boolean?): List> { + override fun startNodes(nodes: List, startInSameProcess: Boolean?, maximumHeapSize: String): List> { val networkMapServiceConfigLookup = networkMapServiceConfigLookup(nodes) return nodes.map { node -> portAllocation.nextHostAndPort() // rpcAddress @@ -720,7 +725,7 @@ class DriverDSL( "notaryClusterAddresses" to node.notaryClusterAddresses ) ) - startNodeInternal(config, webAddress, startInSameProcess) + startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize) } } @@ -781,9 +786,9 @@ class DriverDSL( throw IllegalStateException("Webserver at ${handle.webAddress} has died") } - override fun startWebserver(handle: NodeHandle): CordaFuture { + override fun startWebserver(handle: NodeHandle, maximumHeapSize: String): CordaFuture { val debugPort = if (isDebug) debugPortAllocation.nextPort() else null - val processFuture = DriverDSL.startWebserver(executorService, handle, debugPort) + val processFuture = DriverDSL.startWebserver(executorService, handle, debugPort, maximumHeapSize) registerProcess(processFuture) return processFuture.map { queryWebserver(handle, it) } } @@ -806,7 +811,7 @@ class DriverDSL( override fun baseDirectory(nodeName: String): Path = baseDirectory(CordaX500Name.parse(nodeName)) - override fun startDedicatedNetworkMapService(startInProcess: Boolean?): CordaFuture { + override fun startDedicatedNetworkMapService(startInProcess: Boolean?, maximumHeapSize: String): CordaFuture { val webAddress = portAllocation.nextHostAndPort() val networkMapLegalName = networkMapStartStrategy.legalName val config = ConfigHelper.loadConfig( @@ -822,10 +827,10 @@ class DriverDSL( "extraAdvertisedServiceIds" to listOf(ServiceInfo(NetworkMapService.type).toString()) ) ) - return startNodeInternal(config, webAddress, startInProcess) + return startNodeInternal(config, webAddress, startInProcess, maximumHeapSize) } - private fun startNodeInternal(config: Config, webAddress: NetworkHostAndPort, startInProcess: Boolean?): CordaFuture { + private fun startNodeInternal(config: Config, webAddress: NetworkHostAndPort, startInProcess: Boolean?, maximumHeapSize: String): CordaFuture { val nodeConfiguration = config.parseAs() if (startInProcess ?: startNodesInProcess) { val nodeAndThreadFuture = startInProcessNode(executorService, nodeConfiguration, config) @@ -846,7 +851,7 @@ class DriverDSL( } } else { val debugPort = if (isDebug) debugPortAllocation.nextPort() else null - val processFuture = startOutOfProcessNode(executorService, nodeConfiguration, config, quasarJarPath, debugPort, systemProperties, packagesToScanString.joinToString(",")) + val processFuture = startOutOfProcessNode(executorService, nodeConfiguration, config, quasarJarPath, debugPort, systemProperties, packagesToScanString.joinToString(","), maximumHeapSize) registerProcess(processFuture) return processFuture.flatMap { process -> val processDeathFuture = poll(executorService, "process death") { @@ -910,7 +915,8 @@ class DriverDSL( quasarJarPath: String, debugPort: Int?, overriddenSystemProperties: Map, - packagesToScanString: String + packagesToScanString: String, + maximumHeapSize: String ): CordaFuture { val processFuture = executorService.fork { log.info("Starting out-of-process Node ${nodeConf.myLegalName.organisation}") @@ -939,7 +945,8 @@ class DriverDSL( jdwpPort = debugPort, extraJvmArguments = extraJvmArguments, errorLogPath = nodeConf.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME / "error.log", - workingDirectory = nodeConf.baseDirectory + workingDirectory = nodeConf.baseDirectory, + maximumHeapSize = maximumHeapSize ) } return processFuture.flatMap { process -> @@ -950,7 +957,8 @@ class DriverDSL( private fun startWebserver( executorService: ScheduledExecutorService, handle: NodeHandle, - debugPort: Int? + debugPort: Int?, + maximumHeapSize: String ): CordaFuture { return executorService.fork { val className = "net.corda.webserver.WebServer" @@ -962,7 +970,9 @@ class DriverDSL( "-Dname=node-${handle.configuration.p2pAddress}-webserver", "-Djava.io.tmpdir=${System.getProperty("java.io.tmpdir")}" // Inherit from parent process ), - errorLogPath = Paths.get("error.$className.log") + errorLogPath = Paths.get("error.$className.log"), + workingDirectory = null, + maximumHeapSize = maximumHeapSize ) }.flatMap { process -> addressMustBeBoundFuture(executorService, handle.webAddress, process).map { process } } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/ProcessUtilities.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/ProcessUtilities.kt index c020d830a1..d0a5c6492d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/ProcessUtilities.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/ProcessUtilities.kt @@ -10,7 +10,7 @@ object ProcessUtilities { arguments: List, jdwpPort: Int? = null ): Process { - return startJavaProcessImpl(C::class.java.name, arguments, defaultClassPath, jdwpPort, emptyList(), null, null) + return startJavaProcessImpl(C::class.java.name, arguments, defaultClassPath, jdwpPort, emptyList(), null, null, null) } fun startCordaProcess( @@ -19,11 +19,12 @@ object ProcessUtilities { jdwpPort: Int?, extraJvmArguments: List, errorLogPath: Path?, - workingDirectory: Path? = null + workingDirectory: Path?, + maximumHeapSize: String ): Process { // FIXME: Instead of hacking our classpath, use the correct classpath for className. val classpath = defaultClassPath.split(pathSeparator).filter { !(it / "log4j2-test.xml").exists() }.joinToString(pathSeparator) - return startJavaProcessImpl(className, arguments, classpath, jdwpPort, extraJvmArguments, errorLogPath, workingDirectory) + return startJavaProcessImpl(className, arguments, classpath, jdwpPort, extraJvmArguments, errorLogPath, workingDirectory, maximumHeapSize) } fun startJavaProcessImpl( @@ -33,12 +34,13 @@ object ProcessUtilities { jdwpPort: Int?, extraJvmArguments: List, errorLogPath: Path?, - workingDirectory: Path? + workingDirectory: Path?, + maximumHeapSize: String? ): Process { val command = mutableListOf().apply { add((System.getProperty("java.home") / "bin" / "java").toString()) (jdwpPort != null) && add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$jdwpPort") - add("-Xmx200m") + if (maximumHeapSize != null) add("-Xmx$maximumHeapSize") add("-XX:+UseG1GC") addAll(extraJvmArguments) add("-cp") From c8739e83a9908ee1e42485ce8a5813cd699b7777 Mon Sep 17 00:00:00 2001 From: Maksymilian Pawlak <120831+m4ksio@users.noreply.github.com> Date: Tue, 26 Sep 2017 16:43:32 +0100 Subject: [PATCH 016/185] IRS Fixes to bring UI closer to declared financial types --- .../net/corda/finance/contracts/FinanceTypes.kt | 2 +- .../main/resources/irsweb/view/create-deal.html | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt b/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt index 2a1c02a11e..b79c9d01a6 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt @@ -160,7 +160,7 @@ enum class DayCountBasisDay { enum class DayCountBasisYear { // Ditto above comment for years. Y360, - Y365F, Y365L, Y365Q, Y366, YActual, YActualA, Y365B, Y365, YISMA, YICMA, Y252; + Y365F, Y365L, Y365Q, Y366, YActual, YActualA, Y365B, Y365, YISMA, YISDA, YICMA, Y252; override fun toString(): String { return super.toString().drop(1) diff --git a/samples/irs-demo/src/main/resources/irsweb/view/create-deal.html b/samples/irs-demo/src/main/resources/irsweb/view/create-deal.html index 7ea4e8d3b9..53ea8d4c8e 100644 --- a/samples/irs-demo/src/main/resources/irsweb/view/create-deal.html +++ b/samples/irs-demo/src/main/resources/irsweb/view/create-deal.html @@ -64,10 +64,9 @@
@@ -120,20 +119,18 @@
From 9874e1ff341dc45130c6f5c648a67756f8225cef Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Wed, 27 Sep 2017 12:58:48 +0100 Subject: [PATCH 017/185] uncheckedCast crusade (#1667) --- .../corda/client/jackson/JacksonSupport.kt | 4 ++-- .../client/jfx/model/ContractStateModel.kt | 4 ++-- .../net/corda/client/jfx/model/Models.kt | 5 ++-- .../client/jfx/utils/ObservableUtilities.kt | 13 ++++------- .../kotlin/net/corda/client/mock/Generator.kt | 11 ++++----- .../corda/client/rpc/internal/RPCClient.kt | 6 ++--- .../net/corda/core/contracts/ContractsDSL.kt | 3 ++- .../net/corda/core/crypto/SignedData.kt | 4 ++-- .../corda/core/flows/ContractUpgradeFlow.kt | 4 ++-- .../net/corda/core/internal/FetchDataFlow.kt | 3 +-- .../net/corda/core/internal/InternalUtils.kt | 7 +++--- .../node/services/vault/QueryCriteriaUtils.kt | 4 ++-- .../core/transactions/BaseTransaction.kt | 7 +++--- .../core/transactions/LedgerTransaction.kt | 10 ++++---- .../net/corda/core/utilities/KotlinUtils.kt | 4 ++-- .../kotlin/net/corda/core/utilities/Try.kt | 12 ++++------ .../contracts/universal/UniversalContract.kt | 23 +++++++++---------- .../corda/finance/contracts/GetBalances.kt | 1 - .../finance/contracts/asset/CashTests.kt | 3 --- .../corda/nodeapi/config/ConfigUtilities.kt | 11 ++++----- .../nodeapi/internal/serialization/Kryo.kt | 15 +++++------- .../serialization/SerializationScheme.kt | 6 ++--- .../amqp/CollectionSerializer.kt | 4 ++-- .../serialization/amqp/CustomSerializer.kt | 21 ++++++++--------- .../serialization/amqp/MapSerializer.kt | 7 +++--- .../internal/serialization/amqp/Schema.kt | 13 ++++------- .../serialization/amqp/SerializerFactory.kt | 4 ++-- .../amqp/custom/EnumSetSerializer.kt | 8 +++---- .../serialization/carpenter/ClassCarpenter.kt | 13 ++++------- .../carpenter/ClassCarpenterTest.kt | 17 +++++--------- .../net/corda/node/internal/AbstractNode.kt | 7 +++--- .../node/services/api/ServiceHubInternal.kt | 4 ++-- .../messaging/ArtemisMessagingServer.kt | 8 +++---- .../node/services/messaging/Messaging.kt | 4 ++-- .../statemachine/FlowStateMachineImpl.kt | 13 ++++------- .../statemachine/StateMachineManager.kt | 7 ++---- .../node/services/transactions/PathManager.kt | 4 ++-- .../vault/HibernateQueryCriteriaParser.kt | 10 ++++---- .../services/vault/HibernateVaultQueryImpl.kt | 12 ++++------ .../net/corda/node/shell/InteractiveShell.kt | 7 ++---- .../kotlin/net/corda/irs/flows/FixingFlow.kt | 6 ++--- .../corda/netmap/simulation/IRSSimulation.kt | 4 ++-- .../kotlin/net/corda/testing/RPCDriver.kt | 4 ++-- .../kotlin/net/corda/testing/CoreTestUtils.kt | 2 +- .../main/kotlin/net/corda/testing/Expect.kt | 9 ++++---- .../net/corda/testing/FlowStackSnapshot.kt | 6 ++--- .../main/kotlin/net/corda/testing/Measure.kt | 21 ++++++++--------- .../main/kotlin/net/corda/testing/TestDSL.kt | 8 +++---- .../kotlin/net/corda/demobench/model/User.kt | 4 ++-- .../net/corda/explorer/model/SettingsModel.kt | 12 +++++----- .../corda/webserver/converters/Converters.kt | 4 ++-- 51 files changed, 172 insertions(+), 231 deletions(-) diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt index 4fcd8415b4..809dbc45a9 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt @@ -17,6 +17,7 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.NodeInfo import net.corda.core.node.services.IdentityService @@ -266,8 +267,7 @@ object JacksonSupport { parser.nextToken() } try { - @Suppress("UNCHECKED_CAST") - return SecureHash.parse(parser.text) as T + return uncheckedCast(SecureHash.parse(parser.text)) } catch (e: Exception) { throw JsonParseException(parser, "Invalid hash ${parser.text}: ${e.message}") } diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ContractStateModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ContractStateModel.kt index 6a81eb0dc9..e62f531dcf 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ContractStateModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ContractStateModel.kt @@ -6,6 +6,7 @@ import net.corda.client.jfx.utils.fold import net.corda.client.jfx.utils.map import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef +import net.corda.core.internal.uncheckedCast import net.corda.core.node.services.Vault import net.corda.finance.contracts.asset.Cash import rx.Observable @@ -37,10 +38,9 @@ class ContractStateModel { companion object { private fun Collection>.filterCashStateAndRefs(): List> { return this.map { stateAndRef -> - @Suppress("UNCHECKED_CAST") if (stateAndRef.state.data is Cash.State) { // Kotlin doesn't unify here for some reason - stateAndRef as StateAndRef + uncheckedCast(stateAndRef) } else { null } diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/Models.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/Models.kt index 71a6b66733..feb3548ed2 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/Models.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/Models.kt @@ -4,6 +4,7 @@ import javafx.beans.property.ObjectProperty import javafx.beans.value.ObservableValue import javafx.beans.value.WritableValue import javafx.collections.ObservableList +import net.corda.core.internal.uncheckedCast import org.reactfx.EventSink import org.reactfx.EventStream import rx.Observable @@ -78,9 +79,7 @@ object Models { if (model.javaClass != klass.java) { throw IllegalStateException("Model stored as ${klass.qualifiedName} has type ${model.javaClass}") } - - @Suppress("UNCHECKED_CAST") - return model as M + return uncheckedCast(model) } inline fun get(origin: KClass<*>): M = get(M::class, origin) diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableUtilities.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableUtilities.kt index 10ae4aeff6..89366935fa 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableUtilities.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableUtilities.kt @@ -14,6 +14,7 @@ import javafx.collections.ObservableMap import javafx.collections.transformation.FilteredList import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef +import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.DataFeed import net.corda.core.node.services.Vault import org.fxmisc.easybind.EasyBind @@ -92,8 +93,7 @@ fun ObservableValue.bind(function: (A) -> ObservableValue): Obs * propagate variance constraints and type inference fails. */ fun ObservableValue.bindOut(function: (A) -> ObservableValue): ObservableValue = - @Suppress("UNCHECKED_CAST") - EasyBind.monadic(this).flatMap(function as (A) -> ObservableValue) + EasyBind.monadic(this).flatMap(uncheckedCast(function)) /** * enum class FilterCriterion { HEIGHT, NAME } @@ -105,8 +105,7 @@ fun ObservableValue.bindOut(function: (A) -> ObservableValue ObservableList.filter(predicate: ObservableValue<(A) -> Boolean>): ObservableList { // We cast here to enforce variance, FilteredList should be covariant - @Suppress("UNCHECKED_CAST") - return FilteredList(this as ObservableList).apply { + return FilteredList(uncheckedCast(this)).apply { predicateProperty().bind(predicate.map { predicateFunction -> Predicate { predicateFunction(it) } }) @@ -120,13 +119,11 @@ fun ObservableList.filter(predicate: ObservableValue<(A) -> Boolean>) */ fun ObservableList.filterNotNull(): ObservableList { //TODO This is a tactical work round for an issue with SAM conversion (https://youtrack.jetbrains.com/issue/ALL-1552) so that the M10 explorer works. - @Suppress("UNCHECKED_CAST") - return (this as ObservableList).filtered(object : Predicate { + return uncheckedCast(uncheckedCast>(this).filtered(object : Predicate { override fun test(t: A?): Boolean { return t != null - } - }) as ObservableList + })) } /** diff --git a/client/mock/src/main/kotlin/net/corda/client/mock/Generator.kt b/client/mock/src/main/kotlin/net/corda/client/mock/Generator.kt index 3993383a1a..e01c39d6f9 100644 --- a/client/mock/src/main/kotlin/net/corda/client/mock/Generator.kt +++ b/client/mock/src/main/kotlin/net/corda/client/mock/Generator.kt @@ -1,6 +1,7 @@ package net.corda.client.mock import net.corda.client.mock.Generator.Companion.choice +import net.corda.core.internal.uncheckedCast import net.corda.core.utilities.Try import java.util.* @@ -115,14 +116,13 @@ class Generator(val generate: (SplittableRandom) -> Try) { fun frequency(vararg generators: Pair>) = frequency(generators.toList()) - fun sequence(generators: List>) = Generator { + fun sequence(generators: List>) = Generator> { val result = mutableListOf() for (generator in generators) { val element = generator.generate(it) - @Suppress("UNCHECKED_CAST") when (element) { is Try.Success -> result.add(element.value) - is Try.Failure -> return@Generator element as Try> + is Try.Failure -> return@Generator uncheckedCast(element) } } Try.Success(result) @@ -175,7 +175,7 @@ class Generator(val generate: (SplittableRandom) -> Try) { } - fun replicatePoisson(meanSize: Double, generator: Generator, atLeastOne: Boolean = false) = Generator { + fun replicatePoisson(meanSize: Double, generator: Generator, atLeastOne: Boolean = false) = Generator> { val chance = (meanSize - 1) / meanSize val result = mutableListOf() var finish = false @@ -191,8 +191,7 @@ class Generator(val generate: (SplittableRandom) -> Try) { } } if (res is Try.Failure) { - @Suppress("UNCHECKED_CAST") - return@Generator res as Try> + return@Generator uncheckedCast(res) } } Try.Success(result) 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 d4716f947f..773c511da9 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 @@ -4,6 +4,7 @@ import net.corda.client.rpc.RPCConnection import net.corda.client.rpc.RPCException import net.corda.core.crypto.random63BitValue import net.corda.core.internal.logElapsedTime +import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.RPCOps import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationDefaults @@ -114,10 +115,7 @@ class RPCClient( val proxyHandler = RPCClientProxyHandler(rpcConfiguration, username, password, serverLocator, clientAddress, rpcOpsClass, serializationContext) try { proxyHandler.start() - - @Suppress("UNCHECKED_CAST") - val ops = Proxy.newProxyInstance(rpcOpsClass.classLoader, arrayOf(rpcOpsClass), proxyHandler) as I - + val ops: I = uncheckedCast(Proxy.newProxyInstance(rpcOpsClass.classLoader, arrayOf(rpcOpsClass), proxyHandler)) val serverProtocolVersion = ops.protocolVersion if (serverProtocolVersion < rpcConfiguration.minimumServerProtocolVersion) { throw RPCException("Requested minimum protocol version (${rpcConfiguration.minimumServerProtocolVersion}) is higher" + diff --git a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt index 51036f8dfe..d43ae5c001 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt @@ -4,6 +4,7 @@ package net.corda.core.contracts import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party +import net.corda.core.internal.uncheckedCast import java.security.PublicKey import java.util.* @@ -58,7 +59,7 @@ inline fun Collection> /** Ensures that a transaction has only one command that is of the given type, otherwise throws an exception. */ fun Collection>.requireSingleCommand(klass: Class) = - mapNotNull { @Suppress("UNCHECKED_CAST") if (klass.isInstance(it.value)) it as CommandWithParties else null }.single() + mapNotNull { if (klass.isInstance(it.value)) uncheckedCast, CommandWithParties>(it) else null }.single() /** * Simple functionality for verifying a move command. Verifies that each input has a signature from its owning key. diff --git a/core/src/main/kotlin/net/corda/core/crypto/SignedData.kt b/core/src/main/kotlin/net/corda/core/crypto/SignedData.kt index 472a8a1024..bc39699f55 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SignedData.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SignedData.kt @@ -1,5 +1,6 @@ package net.corda.core.crypto +import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize @@ -23,8 +24,7 @@ open class SignedData(val raw: SerializedBytes, val sig: DigitalSign @Throws(SignatureException::class) fun verified(): T { sig.by.verify(raw.bytes, sig) - @Suppress("UNCHECKED_CAST") - val data = raw.deserialize() as T + val data: T = uncheckedCast(raw.deserialize()) verifyData(data) return data } diff --git a/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt b/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt index 0612d5d9aa..c3ee51a861 100644 --- a/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt @@ -3,6 +3,7 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* import net.corda.core.internal.ContractUpgradeUtils +import net.corda.core.internal.uncheckedCast import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import java.security.PublicKey @@ -30,8 +31,7 @@ object ContractUpgradeFlow { val command = commandData.value val participantKeys: Set = input.data.participants.map { it.owningKey }.toSet() val keysThatSigned: Set = commandData.signers.toSet() - @Suppress("UNCHECKED_CAST") - val upgradedContract = javaClass.classLoader.loadClass(command.upgradedContractClass).newInstance() as UpgradedContract + val upgradedContract: UpgradedContract = uncheckedCast(javaClass.classLoader.loadClass(command.upgradedContractClass).newInstance()) requireThat { "The signing keys include all participant keys" using keysThatSigned.containsAll(participantKeys) "Inputs state reference the legacy contract" using (input.contract == upgradedContract.legacyContract) 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 0789c535ba..28b44dfa4e 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt @@ -114,8 +114,7 @@ sealed class FetchDataFlow( protected abstract fun load(txid: SecureHash): T? - @Suppress("UNCHECKED_CAST") - protected open fun convert(wire: W): T = wire as T + protected open fun convert(wire: W): T = uncheckedCast(wire) private fun validateFetchResponse(maybeItems: UntrustworthyData>, requests: List): List { 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 0ef6a73f84..5d3eafdca1 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -226,8 +226,8 @@ private fun IntProgression.toSpliterator(): Spliterator.OfInt { fun IntProgression.stream(parallel: Boolean = false): IntStream = StreamSupport.intStream(toSpliterator(), parallel) -@Suppress("UNCHECKED_CAST") // When toArray has filled in the array, the component type is no longer T? but T (that may itself be nullable). -inline fun Stream.toTypedArray() = toArray { size -> arrayOfNulls(size) } as Array +// When toArray has filled in the array, the component type is no longer T? but T (that may itself be nullable): +inline fun Stream.toTypedArray(): Array = uncheckedCast(toArray { size -> arrayOfNulls(size) }) fun Class.castIfPossible(obj: Any): T? = if (isInstance(obj)) cast(obj) else null @@ -256,8 +256,7 @@ fun KClass.objectOrNewInstance(): T { class DeclaredField(clazz: Class<*>, name: String, private val receiver: Any?) { private val javaField = clazz.getDeclaredField(name).apply { isAccessible = true } var value: T - @Suppress("UNCHECKED_CAST") - get() = javaField.get(receiver) as T + get() = uncheckedCast(javaField.get(receiver)) set(value) = javaField.set(receiver, value) } 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 811bc1ee2e..1ee9ab89d4 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 @@ -2,6 +2,7 @@ package net.corda.core.node.services.vault +import net.corda.core.internal.uncheckedCast import net.corda.core.schemas.PersistentState import net.corda.core.serialization.CordaSerializable import java.lang.reflect.Field @@ -88,8 +89,7 @@ fun resolveEnclosingObjectFromExpression(expression: CriteriaExpression resolveEnclosingObjectFromColumn(column: Column): Class = column.declaringClass as Class +fun resolveEnclosingObjectFromColumn(column: Column): Class = uncheckedCast(column.declaringClass) fun getColumnName(column: Column): String = column.name /** diff --git a/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt index da0d98dbe7..3ff6773082 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt @@ -4,6 +4,7 @@ import net.corda.core.contracts.* import net.corda.core.identity.Party import net.corda.core.internal.indexOfOrThrow import net.corda.core.internal.castIfPossible +import net.corda.core.internal.uncheckedCast import java.util.function.Predicate /** @@ -40,8 +41,7 @@ abstract class BaseTransaction : NamedByHash { /** * Returns a [StateAndRef] for the given output index. */ - @Suppress("UNCHECKED_CAST") - fun outRef(index: Int): StateAndRef = StateAndRef(outputs[index] as TransactionState, StateRef(id, index)) + fun outRef(index: Int): StateAndRef = StateAndRef(uncheckedCast(outputs[index]), StateRef(id, index)) /** * Returns a [StateAndRef] for the requested output state, or throws [IllegalArgumentException] if not found. @@ -111,8 +111,7 @@ abstract class BaseTransaction : NamedByHash { */ fun outRefsOfType(clazz: Class): List> { return outputs.mapIndexedNotNull { index, state -> - @Suppress("UNCHECKED_CAST") - clazz.castIfPossible(state.data)?.let { StateAndRef(state as TransactionState, StateRef(id, index)) } + clazz.castIfPossible(state.data)?.let { StateAndRef(uncheckedCast(state), StateRef(id, index)) } } } diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 4fa5778071..f54d02e5fc 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -4,6 +4,7 @@ import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party import net.corda.core.internal.castIfPossible +import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.CordaSerializable import java.util.* import java.util.function.Predicate @@ -52,8 +53,7 @@ data class LedgerTransaction( * @param index The index into the inputs. * @return The [StateAndRef] */ - @Suppress("UNCHECKED_CAST") - fun inRef(index: Int): StateAndRef = inputs[index] as StateAndRef + fun inRef(index: Int): StateAndRef = uncheckedCast(inputs[index]) /** * Verifies this transaction and runs contract code. At this stage it is assumed that signatures have already been verified. @@ -230,8 +230,7 @@ data class LedgerTransaction( * @return the possibly empty list of inputs [StateAndRef] matching the clazz restriction. */ fun inRefsOfType(clazz: Class): List> { - @Suppress("UNCHECKED_CAST") - return inputs.mapNotNull { if (clazz.isInstance(it.state.data)) it as StateAndRef else null } + return inputs.mapNotNull { if (clazz.isInstance(it.state.data)) uncheckedCast, StateAndRef>(it) else null } } inline fun inRefsOfType(): List> = inRefsOfType(T::class.java) @@ -307,8 +306,7 @@ data class LedgerTransaction( * @param index the position of the item in the commands. * @return The Command at the requested index */ - @Suppress("UNCHECKED_CAST") - fun getCommand(index: Int): Command = Command(commands[index].value as T, commands[index].signers) + fun getCommand(index: Int): Command = Command(uncheckedCast(commands[index].value), commands[index].signers) /** * Helper to simplify getting all [Command] items with a [CommandData] of a particular class, interface, or base class. diff --git a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt index a726dbf7fb..b12ea8353d 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt @@ -1,6 +1,7 @@ package net.corda.core.utilities import net.corda.core.internal.concurrent.get +import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.CordaSerializable import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -101,14 +102,13 @@ private class TransientProperty internal constructor(private val initiali @Transient private var initialised = false @Transient private var value: T? = null - @Suppress("UNCHECKED_CAST") @Synchronized override operator fun getValue(thisRef: Any?, property: KProperty<*>): T { if (!initialised) { value = initialiser() initialised = true } - return value as T + return uncheckedCast(value) } } diff --git a/core/src/main/kotlin/net/corda/core/utilities/Try.kt b/core/src/main/kotlin/net/corda/core/utilities/Try.kt index 4ff2224ade..cfe471c1b1 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/Try.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/Try.kt @@ -1,5 +1,6 @@ package net.corda.core.utilities +import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.Try.Failure import net.corda.core.utilities.Try.Success @@ -35,30 +36,27 @@ sealed class Try { abstract fun getOrThrow(): A /** Maps the given function to the value from this [Success], or returns `this` if this is a [Failure]. */ - @Suppress("UNCHECKED_CAST") inline fun map(function: (A) -> B): Try = when (this) { is Success -> Success(function(value)) - is Failure -> this as Try + is Failure -> uncheckedCast(this) } /** Returns the given function applied to the value from this [Success], or returns `this` if this is a [Failure]. */ - @Suppress("UNCHECKED_CAST") inline fun flatMap(function: (A) -> Try): Try = when (this) { is Success -> function(value) - is Failure -> this as Try + is Failure -> uncheckedCast(this) } /** * Maps the given function to the values from this [Success] and [other], or returns `this` if this is a [Failure] * or [other] if [other] is a [Failure]. */ - @Suppress("UNCHECKED_CAST") inline fun combine(other: Try, function: (A, B) -> C): Try = when (this) { is Success -> when (other) { is Success -> Success(function(value, other.value)) - is Failure -> other as Try + is Failure -> uncheckedCast(other) } - is Failure -> this as Try + is Failure -> uncheckedCast(this) } data class Success(val value: A) : Try() { diff --git a/experimental/src/main/kotlin/net/corda/finance/contracts/universal/UniversalContract.kt b/experimental/src/main/kotlin/net/corda/finance/contracts/universal/UniversalContract.kt index 6f12d53844..7abb5ed97d 100644 --- a/experimental/src/main/kotlin/net/corda/finance/contracts/universal/UniversalContract.kt +++ b/experimental/src/main/kotlin/net/corda/finance/contracts/universal/UniversalContract.kt @@ -3,6 +3,7 @@ package net.corda.finance.contracts.universal import net.corda.core.contracts.* import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party +import net.corda.core.internal.uncheckedCast import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.finance.contracts.BusinessCalendar @@ -124,19 +125,18 @@ class UniversalContract : Contract { } } - @Suppress("UNCHECKED_CAST") fun replaceStartEnd(p: Perceivable, start: Instant, end: Instant): Perceivable = when (p) { is Const -> p - is TimePerceivable -> TimePerceivable(p.cmp, replaceStartEnd(p.instant, start, end)) as Perceivable - is EndDate -> const(end) as Perceivable - is StartDate -> const(start) as Perceivable + is TimePerceivable -> uncheckedCast(TimePerceivable(p.cmp, replaceStartEnd(p.instant, start, end))) + is EndDate -> uncheckedCast(const(end)) + is StartDate -> uncheckedCast(const(start)) is UnaryPlus -> UnaryPlus(replaceStartEnd(p.arg, start, end)) is PerceivableOperation -> PerceivableOperation(replaceStartEnd(p.left, start, end), p.op, replaceStartEnd(p.right, start, end)) - is Interest -> Interest(replaceStartEnd(p.amount, start, end), p.dayCountConvention, replaceStartEnd(p.interest, start, end), replaceStartEnd(p.start, start, end), replaceStartEnd(p.end, start, end)) as Perceivable - is Fixing -> Fixing(p.source, replaceStartEnd(p.date, start, end), p.tenor) as Perceivable - is PerceivableAnd -> (replaceStartEnd(p.left, start, end) and replaceStartEnd(p.right, start, end)) as Perceivable - is PerceivableOr -> (replaceStartEnd(p.left, start, end) or replaceStartEnd(p.right, start, end)) as Perceivable + is Interest -> uncheckedCast(Interest(replaceStartEnd(p.amount, start, end), p.dayCountConvention, replaceStartEnd(p.interest, start, end), replaceStartEnd(p.start, start, end), replaceStartEnd(p.end, start, end))) + is Fixing -> uncheckedCast(Fixing(p.source, replaceStartEnd(p.date, start, end), p.tenor)) + is PerceivableAnd -> uncheckedCast(replaceStartEnd(p.left, start, end) and replaceStartEnd(p.right, start, end)) + is PerceivableOr -> uncheckedCast(replaceStartEnd(p.left, start, end) or replaceStartEnd(p.right, start, end)) is ActorPerceivable -> p else -> throw NotImplementedError("replaceStartEnd " + p.javaClass.name) } @@ -276,7 +276,6 @@ class UniversalContract : Contract { } } - @Suppress("UNCHECKED_CAST") fun replaceFixing(tx: LedgerTransaction, perceivable: Perceivable, fixings: Map, unusedFixings: MutableSet): Perceivable = when (perceivable) { @@ -284,14 +283,14 @@ class UniversalContract : Contract { is UnaryPlus -> UnaryPlus(replaceFixing(tx, perceivable.arg, fixings, unusedFixings)) is PerceivableOperation -> PerceivableOperation(replaceFixing(tx, perceivable.left, fixings, unusedFixings), perceivable.op, replaceFixing(tx, perceivable.right, fixings, unusedFixings)) - is Interest -> Interest(replaceFixing(tx, perceivable.amount, fixings, unusedFixings), + is Interest -> uncheckedCast(Interest(replaceFixing(tx, perceivable.amount, fixings, unusedFixings), perceivable.dayCountConvention, replaceFixing(tx, perceivable.interest, fixings, unusedFixings), - perceivable.start, perceivable.end) as Perceivable + perceivable.start, perceivable.end)) is Fixing -> { val dt = eval(tx, perceivable.date) if (dt != null && fixings.containsKey(FixOf(perceivable.source, dt.toLocalDate(), perceivable.tenor))) { unusedFixings.remove(FixOf(perceivable.source, dt.toLocalDate(), perceivable.tenor)) - Const(fixings[FixOf(perceivable.source, dt.toLocalDate(), perceivable.tenor)]!!) as Perceivable + uncheckedCast(Const(fixings[FixOf(perceivable.source, dt.toLocalDate(), perceivable.tenor)]!!)) } else perceivable } else -> throw NotImplementedError("replaceFixing - " + perceivable.javaClass.name) 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 7f6c84010d..2e28208a6f 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt @@ -39,7 +39,6 @@ private fun rowsToAmount(currency: Currency, rows: Vault.Page>) } else { require(rows.otherResults.size == 2) require(rows.otherResults[1] == currency.currencyCode) - @Suppress("UNCHECKED_CAST") val quantity = rows.otherResults[0] as Long Amount(quantity, currency) } diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt index 770e0ae5a2..b9b7bd7091 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt @@ -594,7 +594,6 @@ class CashTests : TestDependencyInjectionBase() { makeSpend(100.DOLLARS, THEIR_IDENTITY_1) } database.transaction { - @Suppress("UNCHECKED_CAST") val vaultState = vaultStatesUnconsumed.elementAt(0) assertEquals(vaultState.ref, wtx.inputs[0]) assertEquals(vaultState.state.data.copy(owner = THEIR_IDENTITY_1), wtx.getOutput(0)) @@ -622,7 +621,6 @@ class CashTests : TestDependencyInjectionBase() { makeSpend(10.DOLLARS, THEIR_IDENTITY_1) } database.transaction { - @Suppress("UNCHECKED_CAST") val vaultState = vaultStatesUnconsumed.elementAt(0) val changeAmount = 90.DOLLARS `issued by` defaultIssuer val likelyChangeState = wtx.outputs.map(TransactionState<*>::data).filter { state -> @@ -649,7 +647,6 @@ class CashTests : TestDependencyInjectionBase() { makeSpend(500.DOLLARS, THEIR_IDENTITY_1) } database.transaction { - @Suppress("UNCHECKED_CAST") val vaultState0 = vaultStatesUnconsumed.elementAt(0) val vaultState1 = vaultStatesUnconsumed.elementAt(1) assertEquals(vaultState0.ref, wtx.inputs[0]) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt index 052a361267..1463229b13 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt @@ -5,6 +5,7 @@ import com.typesafe.config.Config import com.typesafe.config.ConfigUtil import net.corda.core.identity.CordaX500Name import net.corda.core.internal.noneOrSingle +import net.corda.core.internal.uncheckedCast import net.corda.core.utilities.NetworkHostAndPort import org.slf4j.LoggerFactory import java.net.Proxy @@ -25,7 +26,7 @@ import kotlin.reflect.jvm.jvmErasure annotation class OldConfig(val value: String) // TODO Move other config parsing to use parseAs and remove this -operator fun Config.getValue(receiver: Any, metadata: KProperty<*>): T { +operator fun Config.getValue(receiver: Any, metadata: KProperty<*>): T { return getValueInternal(metadata.name, metadata.returnType) } @@ -52,9 +53,8 @@ fun Config.toProperties(): Properties { { it.value.unwrapped().toString() }) } -@Suppress("UNCHECKED_CAST") -private fun Config.getValueInternal(path: String, type: KType): T { - return (if (type.arguments.isEmpty()) getSingleValue(path, type) else getCollectionValue(path, type)) as T +private fun Config.getValueInternal(path: String, type: KType): T { + return uncheckedCast(if (type.arguments.isEmpty()) getSingleValue(path, type) else getCollectionValue(path, type)) } private fun Config.getSingleValue(path: String, type: KType): Any? { @@ -122,8 +122,7 @@ private fun Config.defaultToOldPath(property: KProperty<*>): String { return property.name } -@Suppress("UNCHECKED_CAST") -private fun parseEnum(enumType: Class<*>, name: String): Enum<*> = enumBridge(enumType as Class, name) // Any enum will do +private fun parseEnum(enumType: Class<*>, name: String): Enum<*> = enumBridge(uncheckedCast(enumType), name) // Any enum will do private fun > enumBridge(clazz: Class, name: String): T = java.lang.Enum.valueOf(clazz, name) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt index 06f73a4a95..06820db6c2 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt @@ -14,6 +14,7 @@ import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.Crypto import net.corda.core.crypto.TransactionSignature import net.corda.core.identity.Party +import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializeAsTokenContext import net.corda.core.serialization.SerializedBytes @@ -245,9 +246,8 @@ object WireTransactionSerializer : Serializer() { kryo.writeClassAndObject(output, obj.privacySalt) } - @Suppress("UNCHECKED_CAST") override fun read(kryo: Kryo, input: Input, type: Class): WireTransaction { - val componentGroups = kryo.readClassAndObject(input) as List + val componentGroups: List = uncheckedCast(kryo.readClassAndObject(input)) val privacySalt = kryo.readClassAndObject(input) as PrivacySalt return WireTransaction(componentGroups, privacySalt) } @@ -261,9 +261,8 @@ object NotaryChangeWireTransactionSerializer : Serializer): NotaryChangeWireTransaction { - val inputs = kryo.readClassAndObject(input) as List + val inputs: List = uncheckedCast(kryo.readClassAndObject(input)) val notary = kryo.readClassAndObject(input) as Party val newNotary = kryo.readClassAndObject(input) as Party @@ -278,11 +277,10 @@ object SignedTransactionSerializer : Serializer() { kryo.writeClassAndObject(output, obj.sigs) } - @Suppress("UNCHECKED_CAST") override fun read(kryo: Kryo, input: Input, type: Class): SignedTransaction { return SignedTransaction( - kryo.readClassAndObject(input) as SerializedBytes, - kryo.readClassAndObject(input) as List + uncheckedCast>(kryo.readClassAndObject(input)), + uncheckedCast>(kryo.readClassAndObject(input)) ) } } @@ -600,8 +598,7 @@ class ThrowableSerializer(kryo: Kryo, type: Class) : Serializer } } - @Suppress("UNCHECKED_CAST") - private val delegate: Serializer = ReflectionSerializerFactory.makeSerializer(kryo, FieldSerializer::class.java, type) as Serializer + private val delegate: Serializer = uncheckedCast(ReflectionSerializerFactory.makeSerializer(kryo, FieldSerializer::class.java, type)) override fun write(kryo: Kryo, output: Output, throwable: Throwable) { delegate.write(kryo, output, throwable) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt index 299b77ae84..648d6e5817 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt @@ -14,6 +14,7 @@ import com.google.common.cache.CacheBuilder import net.corda.core.contracts.Attachment import net.corda.core.crypto.SecureHash import net.corda.core.internal.LazyPool +import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.* import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.OpaqueBytes @@ -204,11 +205,10 @@ abstract class AbstractKryoSerializationScheme : SerializationScheme { Input(byteSequence.bytes, byteSequence.offset + headerSize, byteSequence.size - headerSize).use { input -> return pool.run { kryo -> withContext(kryo, context) { - @Suppress("UNCHECKED_CAST") if (context.objectReferencesEnabled) { - kryo.readClassAndObject(input) as T + uncheckedCast(kryo.readClassAndObject(input)) } else { - kryo.withoutReferences { kryo.readClassAndObject(input) as T } + kryo.withoutReferences { uncheckedCast(kryo.readClassAndObject(input)) } } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt index 615cea26af..43a0234f95 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.internal.uncheckedCast import net.corda.core.utilities.NonEmptySet import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.codec.Data @@ -36,8 +37,7 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali fun deriveParameterizedType(declaredType: Type, declaredClass: Class<*>, actualClass: Class<*>?): ParameterizedType { if(supportedTypes.containsKey(declaredClass)) { // Simple case - it is already known to be a collection. - @Suppress("UNCHECKED_CAST") - return deriveParametrizedType(declaredType, declaredClass as Class>) + return deriveParametrizedType(declaredType, uncheckedCast(declaredClass)) } else if (actualClass != null && Collection::class.java.isAssignableFrom(actualClass)) { // Declared class is not collection, but [actualClass] is - represent it accordingly. diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt index 7188a99b50..242f829511 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.internal.uncheckedCast import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory.Companion.nameForType import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.codec.Data @@ -9,7 +10,7 @@ import java.lang.reflect.Type * Base class for serializers of core platform types that do not conform to the usual serialization rules and thus * cannot be automatically serialized. */ -abstract class CustomSerializer : AMQPSerializer { +abstract class CustomSerializer : AMQPSerializer { /** * This is a collection of custom serializers that this custom serializer depends on. e.g. for proxy objects * that refer to other custom types etc. @@ -36,8 +37,7 @@ abstract class CustomSerializer : AMQPSerializer { override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) { data.withDescribed(descriptor) { - @Suppress("UNCHECKED_CAST") - writeDescribedObject(obj as T, data, type, output) + writeDescribedObject(uncheckedCast(obj), data, type, output) } } @@ -49,7 +49,7 @@ abstract class CustomSerializer : AMQPSerializer { * subclass in the schema, so that we can distinguish between subclasses. */ // TODO: should this be a custom serializer at all, or should it just be a plain AMQPSerializer? - class SubClass(protected val clazz: Class<*>, protected val superClassSerializer: CustomSerializer) : CustomSerializer() { + class SubClass(protected val clazz: Class<*>, protected val superClassSerializer: CustomSerializer) : CustomSerializer() { // TODO: should this be empty or contain the schema of the super? override val schemaForDocumentation = Schema(emptyList()) @@ -76,7 +76,7 @@ abstract class CustomSerializer : AMQPSerializer { * Additional base features for a custom serializer for a particular class [withInheritance] is false * or super class / interfaces [withInheritance] is true */ - abstract class CustomSerializerImp(protected val clazz: Class, protected val withInheritance: Boolean) : CustomSerializer() { + abstract class CustomSerializerImp(protected val clazz: Class, protected val withInheritance: Boolean) : CustomSerializer() { override val type: Type get() = clazz override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${nameForType(clazz)}") override fun writeClassInfo(output: SerializationOutput) {} @@ -87,12 +87,12 @@ abstract class CustomSerializer : AMQPSerializer { /** * Additional base features for a custom serializer for a particular class, that excludes subclasses. */ - abstract class Is(clazz: Class) : CustomSerializerImp(clazz, false) + abstract class Is(clazz: Class) : CustomSerializerImp(clazz, false) /** * Additional base features for a custom serializer for all implementations of a particular interface or super class. */ - abstract class Implements(clazz: Class) : CustomSerializerImp(clazz, true) + abstract class Implements(clazz: Class) : CustomSerializerImp(clazz, true) /** * Additional base features over and above [Implements] or [Is] custom serializer for when the serialized form should be @@ -101,7 +101,7 @@ abstract class CustomSerializer : AMQPSerializer { * The proxy class must use only types which are either native AMQP or other types for which there are pre-registered * custom serializers. */ - abstract class Proxy(clazz: Class, + abstract class Proxy(clazz: Class, protected val proxyClass: Class

, protected val factory: SerializerFactory, withInheritance: Boolean = true) : CustomSerializerImp(clazz, withInheritance) { @@ -134,8 +134,7 @@ abstract class CustomSerializer : AMQPSerializer { } override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T { - @Suppress("UNCHECKED_CAST") - val proxy = proxySerializer.readObject(obj, schema, input) as P + val proxy: P = uncheckedCast(proxySerializer.readObject(obj, schema, input)) return fromProxy(proxy) } } @@ -151,7 +150,7 @@ abstract class CustomSerializer : AMQPSerializer { * @param make A lambda for constructing an instance, that defaults to calling a constructor that expects a string. * @param unmake A lambda that extracts the string value for an instance, that defaults to the [toString] method. */ - abstract class ToString(clazz: Class, withInheritance: Boolean = false, + abstract class ToString(clazz: Class, withInheritance: Boolean = false, private val maker: (String) -> T = clazz.getConstructor(String::class.java).let { `constructor` -> { string -> `constructor`.newInstance(string) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt index b343e96469..1fa710d59a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.internal.uncheckedCast import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.codec.Data import java.io.NotSerializableException @@ -31,8 +32,7 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial LinkedHashMap::class.java to { map -> LinkedHashMap(map) }, TreeMap::class.java to { map -> TreeMap(map) }, EnumMap::class.java to { map -> - @Suppress("UNCHECKED_CAST") - EnumMap(map as Map) + EnumMap(uncheckedCast, Map>(map)) } )) @@ -44,8 +44,7 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial declaredClass.checkSupportedMapType() if(supportedTypes.containsKey(declaredClass)) { // Simple case - it is already known to be a map. - @Suppress("UNCHECKED_CAST") - return deriveParametrizedType(declaredType, declaredClass as Class>) + return deriveParametrizedType(declaredType, uncheckedCast(declaredClass)) } else if (actualClass != null && Map::class.java.isAssignableFrom(actualClass)) { // Declared class is not map, but [actualClass] is - represent it accordingly. diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt index 4ec6bb32cf..adb0c44f0c 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt @@ -2,6 +2,7 @@ package net.corda.nodeapi.internal.serialization.amqp import com.google.common.hash.Hasher import com.google.common.hash.Hashing +import net.corda.core.internal.uncheckedCast import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.loggerFor import net.corda.core.utilities.toBase64 @@ -94,8 +95,7 @@ data class Schema(val types: List) : DescribedType { override fun newInstance(described: Any?): Schema { val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list") - @Suppress("UNCHECKED_CAST") - return Schema(list[0] as List) + return Schema(uncheckedCast(list[0])) } } @@ -162,8 +162,7 @@ data class Field(val name: String, val type: String, val requires: List, override fun newInstance(described: Any?): Field { val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list") - @Suppress("UNCHECKED_CAST") - return Field(list[0] as String, list[1] as String, list[2] as List, list[3] as? String, list[4] as? String, list[5] as Boolean, list[6] as Boolean) + return Field(list[0] as String, list[1] as String, uncheckedCast(list[2]), list[3] as? String, list[4] as? String, list[5] as Boolean, list[6] as Boolean) } } @@ -223,8 +222,7 @@ data class CompositeType(override val name: String, override val label: String?, override fun newInstance(described: Any?): CompositeType { val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list") - @Suppress("UNCHECKED_CAST") - return CompositeType(list[0] as String, list[1] as? String, list[2] as List, list[3] as Descriptor, list[4] as List) + return CompositeType(list[0] as String, list[1] as? String, uncheckedCast(list[2]), list[3] as Descriptor, uncheckedCast(list[4])) } } @@ -273,8 +271,7 @@ data class RestrictedType(override val name: String, override fun newInstance(described: Any?): RestrictedType { val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list") - @Suppress("UNCHECKED_CAST") - return RestrictedType(list[0] as String, list[1] as? String, list[2] as List, list[3] as String, list[4] as Descriptor, list[5] as List) + return RestrictedType(list[0] as String, list[1] as? String, uncheckedCast(list[2]), list[3] as String, list[4] as Descriptor, uncheckedCast(list[5])) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt index 0077285e77..fe05093168 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt @@ -2,6 +2,7 @@ package net.corda.nodeapi.internal.serialization.amqp import com.google.common.primitives.Primitives import com.google.common.reflect.TypeResolver +import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.CordaSerializable import net.corda.nodeapi.internal.serialization.carpenter.* @@ -262,8 +263,7 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) { return customSerializer } else { // Make a subclass serializer for the subclass and return that... - @Suppress("UNCHECKED_CAST") - return CustomSerializer.SubClass(clazz, customSerializer as CustomSerializer) + return CustomSerializer.SubClass(clazz, uncheckedCast(customSerializer)) } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/EnumSetSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/EnumSetSerializer.kt index 29527d21dd..5f86857ffe 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/EnumSetSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/EnumSetSerializer.kt @@ -1,11 +1,11 @@ package net.corda.nodeapi.internal.serialization.amqp.custom +import net.corda.core.internal.uncheckedCast import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer import net.corda.nodeapi.internal.serialization.amqp.MapSerializer import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory import java.util.* -@Suppress("UNCHECKED_CAST") /** * A serializer that writes out an [EnumSet] as a type, plus list of instances in the set. */ @@ -16,7 +16,7 @@ class EnumSetSerializer(factory: SerializerFactory) : CustomSerializer.Proxy): Class<*> { return if (set.isEmpty()) { - EnumSet.complementOf(set as EnumSet).first().javaClass + EnumSet.complementOf(uncheckedCast, EnumSet>(set)).first().javaClass } else { set.first().javaClass } @@ -24,9 +24,9 @@ class EnumSetSerializer(factory: SerializerFactory) : CustomSerializer.Proxy { return if (proxy.elements.isEmpty()) { - EnumSet.noneOf(proxy.clazz as Class) + EnumSet.noneOf(uncheckedCast, Class>(proxy.clazz)) } else { - EnumSet.copyOf(proxy.elements as List) + EnumSet.copyOf(uncheckedCast, List>(proxy.elements)) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt index d47bc1bf52..9f727e09c7 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt @@ -237,10 +237,9 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader } private fun ClassWriter.generateGetters(schema: Schema) { - @Suppress("UNCHECKED_CAST") - for ((name, type) in (schema.fields as Map)) { + for ((name, type) in schema.fields) { visitMethod(ACC_PUBLIC, "get" + name.capitalize(), "()" + type.descriptor, null, null).apply { - type.addNullabilityAnnotation(this) + (type as ClassField).addNullabilityAnnotation(this) visitCode() visitVarInsn(ALOAD, 0) // Load 'this' visitFieldInsn(GETFIELD, schema.jvmName, name, type.descriptor) @@ -258,8 +257,7 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader } private fun ClassWriter.generateAbstractGetters(schema: Schema) { - @Suppress("UNCHECKED_CAST") - for ((name, field) in (schema.fields as Map)) { + for ((name, field) in schema.fields) { val opcodes = ACC_ABSTRACT + ACC_PUBLIC // abstract method doesn't have any implementation so just end visitMethod(opcodes, "get" + name.capitalize(), "()${field.descriptor}", null, null).visitEnd() @@ -363,9 +361,8 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader // Assign the fields from parameters. var slot = 1 + superclassFields.size - @Suppress("UNCHECKED_CAST") - for ((name, field) in (schema.fields as Map)) { - field.nullTest(this, slot) + for ((name, field) in schema.fields) { + (field as ClassField).nullTest(this, slot) visitVarInsn(ALOAD, 0) // Load 'this' onto the stack slot += load(slot, field) // Load the contents of the parameter onto the stack. diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterTest.kt index 913ea207c1..0c1c610c5a 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterTest.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.serialization.carpenter +import net.corda.core.internal.uncheckedCast import net.corda.nodeapi.internal.serialization.AllWhitelist import org.junit.Test import java.beans.Introspector @@ -323,7 +324,6 @@ class ClassCarpenterTest { } @Test - @Suppress("UNCHECKED_CAST") fun `int array`() { val className = "iEnjoyPotato" val schema = ClassSchema( @@ -356,7 +356,6 @@ class ClassCarpenterTest { } @Test - @Suppress("UNCHECKED_CAST") fun `integer array`() { val className = "iEnjoyFlan" val schema = ClassSchema( @@ -366,16 +365,15 @@ class ClassCarpenterTest { val clazz = cc.build(schema) val i = clazz.constructors[0].newInstance(arrayOf(1, 2, 3)) as SimpleFieldAccess - val arr = clazz.getMethod("getA").invoke(i) + val arr: Array = uncheckedCast(clazz.getMethod("getA").invoke(i)) - assertEquals(1, (arr as Array)[0]) + assertEquals(1, arr[0]) assertEquals(2, arr[1]) assertEquals(3, arr[2]) assertEquals("$className{a=[1, 2, 3]}", i.toString()) } @Test - @Suppress("UNCHECKED_CAST") fun `int array with ints`() { val className = "iEnjoyCrumble" val schema = ClassSchema( @@ -395,7 +393,6 @@ class ClassCarpenterTest { } @Test - @Suppress("UNCHECKED_CAST") fun `multiple int arrays`() { val className = "iEnjoyJam" val schema = ClassSchema( @@ -417,7 +414,6 @@ class ClassCarpenterTest { } @Test - @Suppress("UNCHECKED_CAST") fun `string array`() { val className = "iEnjoyToast" val schema = ClassSchema( @@ -427,7 +423,7 @@ class ClassCarpenterTest { val clazz = cc.build(schema) val i = clazz.constructors[0].newInstance(arrayOf("toast", "butter", "jam")) - val arr = clazz.getMethod("getA").invoke(i) as Array + val arr: Array = uncheckedCast(clazz.getMethod("getA").invoke(i)) assertEquals("toast", arr[0]) assertEquals("butter", arr[1]) @@ -435,7 +431,6 @@ class ClassCarpenterTest { } @Test - @Suppress("UNCHECKED_CAST") fun `string arrays`() { val className = "iEnjoyToast" val schema = ClassSchema( @@ -452,8 +447,8 @@ class ClassCarpenterTest { "and on the side", arrayOf("some pickles", "some fries")) - val arr1 = clazz.getMethod("getA").invoke(i) as Array - val arr2 = clazz.getMethod("getC").invoke(i) as Array + val arr1: Array = uncheckedCast(clazz.getMethod("getA").invoke(i)) + val arr2: Array = uncheckedCast(clazz.getMethod("getC").invoke(i)) assertEquals("bread", arr1[0]) assertEquals("spread", arr1[1]) 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 0c6b9cec56..50e9ac4866 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -18,6 +18,7 @@ import net.corda.core.internal.concurrent.doneFuture import net.corda.core.internal.concurrent.flatMap import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.toX509CertHolder +import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.RPCOps import net.corda.core.messaging.SingleMessageRecipient @@ -322,11 +323,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } else { log.warn(deprecatedFlowConstructorMessage(initiatedFlow)) } - @Suppress("UNCHECKED_CAST") - { flowSession: FlowSession -> partyCtor.newInstance(flowSession.counterparty) as F } + { flowSession: FlowSession -> uncheckedCast(partyCtor.newInstance(flowSession.counterparty)) } } else { - @Suppress("UNCHECKED_CAST") - { flowSession: FlowSession -> flowSessionCtor.newInstance(flowSession) as F } + { flowSession: FlowSession -> uncheckedCast(flowSessionCtor.newInstance(flowSession)) } } val initiatingFlow = initiatedFlow.requireAnnotation().value.java val (version, classWithAnnotation) = initiatingFlow.flowVersionAndInitiatingClass 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 1e2625ffb4..5492f24c1d 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 @@ -8,6 +8,7 @@ import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.VisibleForTesting +import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.DataFeed import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.messaging.StateMachineTransactionMapping @@ -132,8 +133,7 @@ interface ServiceHubInternal : ServiceHub { flowInitiator: FlowInitiator, vararg args: Any?): FlowStateMachineImpl { val logicRef = FlowLogicRefFactoryImpl.createForRPC(logicType, *args) - @Suppress("UNCHECKED_CAST") - val logic = FlowLogicRefFactoryImpl.toFlowLogic(logicRef) as FlowLogic + val logic: FlowLogic = uncheckedCast(FlowLogicRefFactoryImpl.toFlowLogic(logicRef)) return startFlow(logic, flowInitiator, ourIdentity = null) } 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 784776820c..32c866da19 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 @@ -11,6 +11,7 @@ import net.corda.core.internal.ThreadBox import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.div import net.corda.core.internal.noneOrSingle +import net.corda.core.internal.uncheckedCast import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkMapCache.MapChange @@ -56,7 +57,6 @@ import java.math.BigInteger import java.security.KeyStore import java.security.KeyStoreException import java.security.Principal -import java.security.cert.X509Certificate import java.util.* import java.util.concurrent.Executor import java.util.concurrent.ScheduledExecutorService @@ -478,8 +478,7 @@ private class VerifyingNettyConnector(configuration: MutableMap, override fun createConnection(): Connection? { val connection = super.createConnection() as? NettyConnection if (sslEnabled && connection != null) { - @Suppress("UNCHECKED_CAST") - val expectedLegalNames = (configuration[ArtemisTcpTransport.VERIFY_PEER_LEGAL_NAME] ?: emptySet()) as Set + val expectedLegalNames: Set = uncheckedCast(configuration[ArtemisTcpTransport.VERIFY_PEER_LEGAL_NAME] ?: emptySet()) try { val session = connection.channel .pipeline() @@ -608,12 +607,11 @@ class NodeLoginModule : LoginModule { private lateinit var verifierCertCheck: CertificateChainCheckPolicy.Check private val principals = ArrayList() - @Suppress("UNCHECKED_CAST") override fun initialize(subject: Subject, callbackHandler: CallbackHandler, sharedState: Map, options: Map) { this.subject = subject this.callbackHandler = callbackHandler userService = options[RPCUserService::class.java.name] as RPCUserService - val certChainChecks = options[CERT_CHAIN_CHECKS_OPTION_NAME] as Map + val certChainChecks: Map = uncheckedCast(options[CERT_CHAIN_CHECKS_OPTION_NAME]) peerCertCheck = certChainChecks[PEER_ROLE]!! nodeCertCheck = certChainChecks[NODE_ROLE]!! verifierCertCheck = certChainChecks[VERIFIER_ROLE]!! diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt b/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt index 062919084e..b05dc10c02 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt @@ -4,6 +4,7 @@ import com.google.common.util.concurrent.ListenableFuture import net.corda.core.concurrent.CordaFuture import net.corda.core.identity.CordaX500Name import net.corda.core.internal.concurrent.openFuture +import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.services.PartyInfo @@ -161,8 +162,7 @@ fun MessagingService.onNext(topic: String, sessionId: Long): CordaFutu val messageFuture = openFuture() runOnNextMessage(topic, sessionId) { message -> messageFuture.capture { - @Suppress("UNCHECKED_CAST") - message.data.deserialize() as M + uncheckedCast(message.data.deserialize()) } } return messageFuture 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 1556ef31c3..b3103f979e 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,12 +12,9 @@ import net.corda.core.crypto.random63BitValue import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate -import net.corda.core.internal.FlowStateMachine -import net.corda.core.internal.abbreviate +import net.corda.core.internal.* import net.corda.core.internal.concurrent.OpenFuture import net.corda.core.internal.concurrent.openFuture -import net.corda.core.internal.isRegularFile -import net.corda.core.internal.staticField import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.* import net.corda.node.services.api.FlowAppAuditEvent @@ -281,7 +278,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, } // TODO Dummy implementation of access to application specific audit logging - override fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map): Unit { + override fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map) { val flowAuditEvent = FlowAppAuditEvent( serviceHub.clock.instant(), flowInitiator, @@ -430,8 +427,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, val session = receiveRequest.session val receiveType = receiveRequest.receiveType if (receiveType.isInstance(message)) { - @Suppress("UNCHECKED_CAST") - return this as ReceivedSessionMessage + return uncheckedCast(this) } else if (message is SessionEnd) { openSessions.values.remove(session) if (message is ErrorSessionEnd) { @@ -519,7 +515,6 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, } } -@Suppress("UNCHECKED_CAST") val Class>.flowVersionAndInitiatingClass: Pair>> get() { var current: Class<*> = this var found: Pair>>? = null @@ -528,7 +523,7 @@ val Class>.flowVersionAndInitiatingClass: Pair 0) { "Flow versions have to be greater or equal to 1" } - found = annotation.version to (current as Class>) + found = annotation.version to uncheckedCast(current) } current = current.superclass ?: return found diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index dc2a8342cb..2382614752 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -14,9 +14,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.random63BitValue import net.corda.core.flows.* import net.corda.core.identity.Party -import net.corda.core.internal.ThreadBox -import net.corda.core.internal.bufferUntilSubscribed -import net.corda.core.internal.castIfPossible +import net.corda.core.internal.* import net.corda.core.messaging.DataFeed import net.corda.core.serialization.SerializationDefaults.CHECKPOINT_CONTEXT import net.corda.core.serialization.SerializationDefaults.SERIALIZATION_FACTORY @@ -149,10 +147,9 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, /** Returns a list of all state machines executing the given flow logic at the top level (subflows do not count) */ fun

, T> findStateMachines(flowClass: Class

): List>> { - @Suppress("UNCHECKED_CAST") return mutex.locked { stateMachines.keys.mapNotNull { - flowClass.castIfPossible(it.logic)?.let { it to (it.stateMachine as FlowStateMachineImpl).resultFuture } + flowClass.castIfPossible(it.logic)?.let { it to uncheckedCast, FlowStateMachineImpl>(it.stateMachine).resultFuture } } } } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/PathManager.kt b/node/src/main/kotlin/net/corda/node/services/transactions/PathManager.kt index 7a031540f9..20e69d5aae 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/PathManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/PathManager.kt @@ -1,5 +1,6 @@ package net.corda.node.services.transactions +import net.corda.core.internal.uncheckedCast import net.corda.nodeapi.internal.addShutdownHook import java.io.Closeable import java.nio.file.Path @@ -31,8 +32,7 @@ open class PathManager>(path: Path) : Closeable { fun handle(): T { handleCounter.incrementAndGet() - @Suppress("UNCHECKED_CAST") - return this as T + return uncheckedCast(this) } override fun close() { 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 575d1cbfc9..30c32220f7 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 @@ -3,6 +3,7 @@ package net.corda.node.services.vault import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateRef import net.corda.core.identity.AbstractParty +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.* @@ -111,8 +112,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class { - @Suppress("UNCHECKED_CAST") - val literal = columnPredicate.rightLiteral as Comparable? + val literal: Comparable? = uncheckedCast(columnPredicate.rightLiteral) @Suppress("UNCHECKED_CAST") column as Path?> when (columnPredicate.operator) { @@ -139,10 +139,8 @@ class HibernateQueryCriteriaParser(val contractStateType: Class { @Suppress("UNCHECKED_CAST") column as Path?> - @Suppress("UNCHECKED_CAST") - val fromLiteral = columnPredicate.rightFromLiteral as Comparable? - @Suppress("UNCHECKED_CAST") - val toLiteral = columnPredicate.rightToLiteral as Comparable? + val fromLiteral: Comparable? = uncheckedCast(columnPredicate.rightFromLiteral) + val toLiteral: Comparable? = uncheckedCast(columnPredicate.rightToLiteral) criteriaBuilder.between(column, fromLiteral, toLiteral) } is ColumnPredicate.NullExpression -> { diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt index 27afbc2040..3501c2db34 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt @@ -7,6 +7,7 @@ import net.corda.core.contracts.TransactionState import net.corda.core.crypto.SecureHash import net.corda.core.internal.ThreadBox import net.corda.core.internal.bufferUntilSubscribed +import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.DataFeed import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultQueryException @@ -153,8 +154,7 @@ class HibernateVaultQueryImpl(val hibernateConfig: HibernateConfiguration, override fun _trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class): DataFeed, Vault.Update> { return mutex.locked { val snapshotResults = _queryBy(criteria, paging, sorting, contractStateType) - @Suppress("UNCHECKED_CAST") - val updates = vault.updatesPublisher.bufferUntilSubscribed().filter { it.containsType(contractStateType, snapshotResults.stateTypes) } as Observable> + val updates: Observable> = uncheckedCast(vault.updatesPublisher.bufferUntilSubscribed().filter { it.containsType(contractStateType, snapshotResults.stateTypes) }) DataFeed(snapshotResults, updates) } } @@ -180,8 +180,7 @@ class HibernateVaultQueryImpl(val hibernateConfig: HibernateConfiguration, val contractInterfaceToConcreteTypes = mutableMapOf>() distinctTypes.forEach { type -> - @Suppress("UNCHECKED_CAST") - val concreteType = Class.forName(type) as Class + val concreteType: Class = uncheckedCast(Class.forName(type)) val contractInterfaces = deriveContractInterfaces(concreteType) contractInterfaces.map { val contractInterface = contractInterfaceToConcreteTypes.getOrPut(it.name, { mutableSetOf() }) @@ -196,9 +195,8 @@ class HibernateVaultQueryImpl(val hibernateConfig: HibernateConfiguration, val myInterfaces: MutableSet> = mutableSetOf() clazz.interfaces.forEach { if (!it.equals(ContractState::class.java)) { - @Suppress("UNCHECKED_CAST") - myInterfaces.add(it as Class) - myInterfaces.addAll(deriveContractInterfaces(it)) + myInterfaces.add(uncheckedCast(it)) + myInterfaces.addAll(deriveContractInterfaces(uncheckedCast(it))) } } return myInterfaces diff --git a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt b/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt index 668236515a..35e695d78e 100644 --- a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt +++ b/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt @@ -233,8 +233,7 @@ object InteractiveShell { return } - @Suppress("UNCHECKED_CAST") - val clazz = matches.single() as Class> + val clazz: Class> = uncheckedCast(matches.single()) try { // TODO Flow invocation should use startFlowDynamic. val fsm = runFlowFromString({ node.services.startFlow(it, FlowInitiator.Shell) }, inputData, clazz) @@ -434,8 +433,6 @@ object InteractiveShell { } } - // Kotlin bug: USELESS_CAST warning is generated below but the IDE won't let us remove it. - @Suppress("USELESS_CAST", "UNCHECKED_CAST") private fun maybeFollow(response: Any?, printerFun: (Any?) -> String, toStream: PrintWriter): OpenFuture? { // Match on a couple of common patterns for "important" observables. It's tough to do this in a generic // way because observables can be embedded anywhere in the object graph, and can emit other arbitrary @@ -454,7 +451,7 @@ object InteractiveShell { } ?: return null val subscriber = PrintingSubscriber(printerFun, toStream) - (observable as Observable).subscribe(subscriber) + uncheckedCast(observable).subscribe(subscriber) return subscriber.future } diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt index e7d1074362..f2cb795f36 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt @@ -5,7 +5,7 @@ import net.corda.core.contracts.* import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.* import net.corda.core.identity.Party -import net.corda.core.node.NodeInfo +import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder @@ -46,7 +46,6 @@ object FixingFlow { @Suspendable override fun assembleSharedTX(handshake: TwoPartyDealFlow.Handshake): Triple, List> { - @Suppress("UNCHECKED_CAST") val fixOf = deal.nextFixingOf()!! // TODO Do we need/want to substitute in new public keys for the Parties? @@ -91,9 +90,8 @@ object FixingFlow { class Floater(override val otherSideSession: FlowSession, override val payload: FixingSession, override val progressTracker: ProgressTracker = TwoPartyDealFlow.Primary.tracker()) : TwoPartyDealFlow.Primary() { - @Suppress("UNCHECKED_CAST") private val dealToFix: StateAndRef by transient { - val state = serviceHub.loadState(payload.ref) as TransactionState + val state: TransactionState = uncheckedCast(serviceHub.loadState(payload.ref)) StateAndRef(state, payload.ref) } diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt index 4642412d9b..687ccbe857 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt @@ -11,6 +11,7 @@ import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachine +import net.corda.core.internal.uncheckedCast import net.corda.core.node.services.queryBy import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction @@ -155,9 +156,8 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten val acceptDealFlows: Observable = node2.internals.registerInitiatedFlow(AcceptDealFlow::class.java) - @Suppress("UNCHECKED_CAST") val acceptorTxFuture = acceptDealFlows.toFuture().toCompletableFuture().thenCompose { - (it.stateMachine as FlowStateMachine).resultFuture.toCompletableFuture() + uncheckedCast, FlowStateMachine>(it.stateMachine).resultFuture.toCompletableFuture() } showProgressFor(listOf(node1, node2)) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/RPCDriver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/RPCDriver.kt index 8bb2bd9a0c..4aff94842f 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/RPCDriver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/RPCDriver.kt @@ -10,6 +10,7 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.internal.concurrent.fork import net.corda.core.internal.concurrent.map import net.corda.core.internal.div +import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.RPCOps import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.RPCUserService @@ -507,8 +508,7 @@ class RandomRpcUser { @JvmStatic fun main(args: Array) { require(args.size == 4) - @Suppress("UNCHECKED_CAST") - val rpcClass = Class.forName(args[0]) as Class + val rpcClass: Class = uncheckedCast(Class.forName(args[0])) val hostAndPort = NetworkHostAndPort.parse(args[1]) val username = args[2] val password = args[3] diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index 5a55586924..bedda6486b 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -1,4 +1,4 @@ -@file:Suppress("UNUSED_PARAMETER", "UNCHECKED_CAST") +@file:Suppress("UNUSED_PARAMETER") @file:JvmName("CoreTestUtils") package net.corda.testing diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/Expect.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/Expect.kt index 1128186c67..0db4754f4c 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/Expect.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/Expect.kt @@ -1,6 +1,7 @@ package net.corda.testing import com.google.common.util.concurrent.SettableFuture +import net.corda.core.internal.uncheckedCast import net.corda.core.utilities.getOrThrow import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -213,9 +214,8 @@ private sealed class ExpectComposeState { class Single(val single: ExpectCompose.Single) : ExpectComposeState() { override fun nextState(event: E): Pair<() -> Unit, ExpectComposeState>? = if (single.expect.clazz.isAssignableFrom(event.javaClass)) { - @Suppress("UNCHECKED_CAST") - val coercedEvent = event as T - if (single.expect.match(event)) { + val coercedEvent: T = uncheckedCast(event) + if (single.expect.match(coercedEvent)) { Pair({ single.expect.expectClosure(coercedEvent) }, Finished()) } else { null @@ -285,8 +285,7 @@ private sealed class ExpectComposeState { is ExpectCompose.Single -> { // This coercion should not be needed but kotlin can't reason about existential type variables(T) // so here we're coercing T into E (even though T is invariant). - @Suppress("UNCHECKED_CAST") - Single(expectCompose as ExpectCompose.Single) + Single(uncheckedCast(expectCompose)) } is ExpectCompose.Sequential -> { if (expectCompose.sequence.size > 0) { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt index 94eb3a7a12..d51d5e7fe5 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt @@ -16,6 +16,7 @@ import net.corda.core.internal.div import net.corda.core.internal.write import net.corda.core.serialization.SerializeAsToken import net.corda.client.jackson.JacksonSupport +import net.corda.core.internal.uncheckedCast import net.corda.node.services.statemachine.FlowStackSnapshotFactory import java.nio.file.Path import java.time.Instant @@ -134,11 +135,10 @@ class FlowStackSnapshotFactoryImpl : FlowStackSnapshotFactory { } -private inline fun R.getField(name: String): A { +private inline fun R.getField(name: String): A { val field = R::class.java.getDeclaredField(name) field.isAccessible = true - @Suppress("UNCHECKED_CAST") - return field.get(this) as A + return uncheckedCast(field.get(this)) } private fun getFiberStack(fiber: Fiber<*>): Stack { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/Measure.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/Measure.kt index d3a6f9f65d..102cc0f413 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/Measure.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/Measure.kt @@ -1,5 +1,6 @@ package net.corda.testing +import net.corda.core.internal.uncheckedCast import kotlin.reflect.KCallable import kotlin.reflect.jvm.reflect @@ -9,18 +10,14 @@ import kotlin.reflect.jvm.reflect * different combinations of parameters. */ -@Suppress("UNCHECKED_CAST") -fun measure(a: Iterable, f: (A) -> R) = - measure(listOf(a), f.reflect()!!) { (f as ((Any?)->R))(it[0]) } -@Suppress("UNCHECKED_CAST") -fun measure(a: Iterable, b: Iterable, f: (A, B) -> R) = - measure(listOf(a, b), f.reflect()!!) { (f as ((Any?,Any?)->R))(it[0], it[1]) } -@Suppress("UNCHECKED_CAST") -fun measure(a: Iterable, b: Iterable, c: Iterable, f: (A, B, C) -> R) = - measure(listOf(a, b, c), f.reflect()!!) { (f as ((Any?,Any?,Any?)->R))(it[0], it[1], it[2]) } -@Suppress("UNCHECKED_CAST") -fun measure(a: Iterable, b: Iterable, c: Iterable, d: Iterable, f: (A, B, C, D) -> R) = - measure(listOf(a, b, c, d), f.reflect()!!) { (f as ((Any?,Any?,Any?,Any?)->R))(it[0], it[1], it[2], it[3]) } +fun measure(a: Iterable, f: (A) -> R) = + measure(listOf(a), f.reflect()!!) { f(uncheckedCast(it[0])) } +fun measure(a: Iterable, b: Iterable, f: (A, B) -> R) = + measure(listOf(a, b), f.reflect()!!) { f(uncheckedCast(it[0]), uncheckedCast(it[1])) } +fun measure(a: Iterable, b: Iterable, c: Iterable, f: (A, B, C) -> R) = + measure(listOf(a, b, c), f.reflect()!!) { f(uncheckedCast(it[0]), uncheckedCast(it[1]), uncheckedCast(it[2])) } +fun measure(a: Iterable, b: Iterable, c: Iterable, d: Iterable, f: (A, B, C, D) -> R) = + measure(listOf(a, b, c, d), f.reflect()!!) { f(uncheckedCast(it[0]), uncheckedCast(it[1]), uncheckedCast(it[2]), uncheckedCast(it[3])) } private fun measure(paramIterables: List>, kCallable: KCallable, call: (Array) -> R): Iterable> { val kParameters = kCallable.parameters diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt index 84d0512a05..6db7662380 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt @@ -5,6 +5,7 @@ import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.* import net.corda.core.crypto.NullKeys.NULL_SIGNATURE import net.corda.core.identity.Party +import net.corda.core.internal.uncheckedCast import net.corda.core.node.ServiceHub import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder @@ -188,8 +189,8 @@ data class TestLedgerDSLInterpreter private constructor( nonVerifiedTransactionWithLocations[stateRef.txhash] ?: throw TransactionResolutionException(stateRef.txhash) val output = transactionWithLocation.transaction.outputs[stateRef.index] - return if (S::class.java.isAssignableFrom(output.data.javaClass)) @Suppress("UNCHECKED_CAST") { - output as TransactionState + return if (S::class.java.isAssignableFrom(output.data.javaClass)) { + uncheckedCast(output) } else { throw TypeMismatch(requested = S::class.java, actual = output.data.javaClass) } @@ -313,8 +314,7 @@ data class TestLedgerDSLInterpreter private constructor( } else if (!clazz.isAssignableFrom(stateAndRef.state.data.javaClass)) { throw TypeMismatch(requested = clazz, actual = stateAndRef.state.data.javaClass) } else { - @Suppress("UNCHECKED_CAST") - return stateAndRef as StateAndRef + return uncheckedCast(stateAndRef) } } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/User.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/User.kt index aadbeb7e06..c55f3bc5d3 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/User.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/User.kt @@ -2,14 +2,14 @@ package net.corda.demobench.model +import net.corda.core.internal.uncheckedCast import net.corda.nodeapi.User import java.util.* -@Suppress("UNCHECKED_CAST") fun toUser(map: Map) = User( map.getOrElse("username", { "none" }) as String, map.getOrElse("password", { "none" }) as String, - LinkedHashSet(map.getOrElse("permissions", { emptyList() }) as Collection) + LinkedHashSet(uncheckedCast>(map.getOrElse("permissions", { emptyList() }))) ) fun user(name: String) = User(name, "letmein", setOf("ALL")) diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/model/SettingsModel.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/model/SettingsModel.kt index 5df84b94fd..19bba7b639 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/model/SettingsModel.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/model/SettingsModel.kt @@ -7,6 +7,7 @@ import javafx.beans.property.SimpleObjectProperty import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.internal.exists +import net.corda.core.internal.uncheckedCast import tornadofx.* import java.nio.file.Files import java.nio.file.Path @@ -52,14 +53,13 @@ class SettingsModel(path: Path = Paths.get("conf")) : Component(), Observable { // Save all changes in memory to properties file. fun commit() = Files.newOutputStream(path).use { config.store(it, "") } - @Suppress("UNCHECKED_CAST") private operator fun Properties.getValue(receiver: Any, metadata: KProperty<*>): T { return when (metadata.returnType.javaType) { - String::class.java -> string(metadata.name, "") as T - Int::class.java -> string(metadata.name, "0").toInt() as T - Boolean::class.java -> boolean(metadata.name) as T - Currency::class.java -> Currency.getInstance(string(metadata.name, "USD")) as T - Path::class.java -> Paths.get(string(metadata.name, "")).toAbsolutePath() as T + String::class.java -> uncheckedCast(string(metadata.name, "")) + Int::class.java -> uncheckedCast(string(metadata.name, "0").toInt()) + Boolean::class.java -> uncheckedCast(boolean(metadata.name)) + Currency::class.java -> uncheckedCast(Currency.getInstance(string(metadata.name, "USD"))) + Path::class.java -> uncheckedCast(Paths.get(string(metadata.name, "")).toAbsolutePath()) else -> throw IllegalArgumentException("Unsupported type ${metadata.returnType}") } } diff --git a/webserver/src/main/kotlin/net/corda/webserver/converters/Converters.kt b/webserver/src/main/kotlin/net/corda/webserver/converters/Converters.kt index ca47c80173..f0e20a037e 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/converters/Converters.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/converters/Converters.kt @@ -1,6 +1,7 @@ package net.corda.webserver.converters import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.uncheckedCast import java.lang.reflect.Type import javax.ws.rs.ext.ParamConverter import javax.ws.rs.ext.ParamConverterProvider @@ -15,8 +16,7 @@ object CordaX500NameConverter : ParamConverter { object CordaConverterProvider : ParamConverterProvider { override fun getConverter(rawType: Class, genericType: Type?, annotations: Array?): ParamConverter? { if (rawType == CordaX500Name::class.java) { - @Suppress("UNCHECKED_CAST") - return CordaX500NameConverter as ParamConverter + return uncheckedCast(CordaX500NameConverter) } return null } From d1891faa4d0f6a5149b5d6e88380d4bed9cf5a17 Mon Sep 17 00:00:00 2001 From: mkit Date: Wed, 27 Sep 2017 13:17:55 +0100 Subject: [PATCH 018/185] FlowSnapshot serialization error (#1671) * Converting SubList to list due to the Kryo lack of support * Adding test * Addressing review comments --- .../corda/testing/FlowStackSnapshotTest.kt | 55 +++++++++++++++++-- .../net/corda/testing/FlowStackSnapshot.kt | 3 +- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt index 0aa4bec5e4..33d682f2b4 100644 --- a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt @@ -2,23 +2,25 @@ package net.corda.testing import co.paralleluniverse.fibers.Suspendable import net.corda.client.jackson.JacksonSupport -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowStackSnapshot -import net.corda.core.flows.StartableByRPC -import net.corda.core.flows.StateMachineRunId +import net.corda.core.flows.* import net.corda.core.internal.div import net.corda.core.internal.list import net.corda.core.internal.read import net.corda.core.messaging.startFlow import net.corda.core.serialization.CordaSerializable import net.corda.node.services.FlowPermissions.Companion.startFlowPermission +import net.corda.node.services.network.NetworkMapService +import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.User +import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.driver.driver +import net.corda.testing.node.MockNetwork import org.junit.Ignore import org.junit.Test import java.nio.file.Path import java.time.LocalDate import kotlin.test.assertEquals +import kotlin.test.assertNull import kotlin.test.assertTrue @CordaSerializable @@ -178,6 +180,31 @@ class MultiplePersistingSideEffectFlow(val persistCallCount: Int) : FlowLogic() { + + @Suspendable + override fun call() { + val flowStackSnapshot = flowStackSnapshot() + val mySession = initiateFlow(ourIdentity) + mySession.sendAndReceive("Ping") + } +} + +@InitiatedBy(FlowStackSnapshotSerializationTestingFlow::class) +class DummyFlow(private val otherSideSession: FlowSession) : FlowLogic() { + + @Suspendable + override fun call() { + val message = otherSideSession.receive() + otherSideSession.send("$message Pong") + } +} + fun readFlowStackSnapshotFromDir(baseDir: Path, flowId: StateMachineRunId): FlowStackSnapshot { val snapshotFile = flowSnapshotDir(baseDir, flowId) / "flowStackSnapshot.json" return snapshotFile.read { @@ -263,6 +290,26 @@ class FlowStackSnapshotTest { } } + @Test + fun `flowStackSnapshot object is serializable`() { + val mockNet = MockNetwork(threadPerNode = true) + val notaryService = ServiceInfo(ValidatingNotaryService.type) + val notaryNode = mockNet.createNode( + legalName = DUMMY_NOTARY.name, + overrideServices = mapOf(notaryService to DUMMY_NOTARY_KEY), + advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), notaryService)) + val node = mockNet.createPartyNode(notaryNode.network.myAddress) + node.internals.registerInitiatedFlow(DummyFlow::class.java) + node.services.startFlow(FlowStackSnapshotSerializationTestingFlow()).resultFuture.get() + val thrown = try { + mockNet.stopNodes() + null + } catch (exception: Exception) { + exception + } + assertNull(thrown) + } + @Test fun `persistFlowStackSnapshot stack traces are aligned with stack objects`() { driver(startNodesInProcess = true) { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt index d51d5e7fe5..d79568d693 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt @@ -55,7 +55,8 @@ class FlowStackSnapshotFactoryImpl : FlowStackSnapshotFactory { val objectStack = getObjectStack(stack).toList() val frameOffsets = getFrameOffsets(stack) val frameObjects = frameOffsets.map { (frameOffset, frameSize) -> - objectStack.subList(frameOffset + 1, frameOffset + frameSize + 1) + // We need to convert the sublist to a list due to the Kryo lack of support when serializing + objectStack.subList(frameOffset + 1, frameOffset + frameSize + 1).toList() } // We drop the first element as it is corda internal call irrelevant from the perspective of a CordApp developer val relevantStackTrace = removeConstructorStackTraceElements(stackTrace).drop(1) From 241f8435551ec11a40f91f80e3520d669d386921 Mon Sep 17 00:00:00 2001 From: josecoll Date: Wed, 27 Sep 2017 13:33:23 +0100 Subject: [PATCH 019/185] Unification of VaultQuery And VaultService APIs (into single VaultService interface) to simplify node bootstrapping and usability. (#1677) --- .../corda/core/node/CordaPluginRegistry.kt | 6 - .../kotlin/net/corda/core/node/ServiceHub.kt | 5 +- .../core/node/services/VaultQueryService.kt | 142 ----------- .../corda/core/node/services/VaultService.kt | 137 ++++++++++- .../core/flows/ContractUpgradeFlowTest.kt | 4 +- docs/source/api-vault-query.rst | 4 +- .../java/net/corda/docs/FlowCookbookJava.java | 2 +- .../kotlin/net/corda/docs/FlowCookbook.kt | 2 +- .../docs/WorkflowTransactionBuildTutorial.kt | 2 +- .../WorkflowTransactionBuildTutorialTest.kt | 2 +- .../source/tutorial-building-transactions.rst | 4 +- .../corda/finance/contracts/GetBalances.kt | 4 +- .../net/corda/finance/flows/CashExitFlow.kt | 2 +- .../finance/contracts/asset/CashTests.kt | 2 +- .../finance/flows/CashPaymentFlowTests.kt | 10 +- .../net/corda/node/internal/AbstractNode.kt | 13 +- .../corda/node/internal/CordaRPCOpsImpl.kt | 4 +- .../services/vault/HibernateVaultQueryImpl.kt | 204 ---------------- .../node/services/vault/NodeVaultService.kt | 187 +++++++++++++- .../services/vault/VaultQueryJavaTests.java | 95 +++---- .../net/corda/node/CordaRPCOpsImplTest.kt | 2 +- .../events/NodeSchedulerServiceTest.kt | 2 +- .../services/events/ScheduledFlowTests.kt | 14 +- .../persistence/DBTransactionStorageTests.kt | 2 +- .../statemachine/FlowFrameworkTests.kt | 4 +- .../services/vault/NodeVaultServiceTest.kt | 112 ++++----- .../node/services/vault/VaultQueryTests.kt | 231 +++++++++--------- .../node/services/vault/VaultWithCashTest.kt | 56 ++--- .../corda/netmap/simulation/IRSSimulation.kt | 2 +- .../kotlin/net/corda/vega/flows/SimmFlow.kt | 12 +- .../net/corda/vega/flows/SimmRevaluation.kt | 2 +- .../net/corda/traderdemo/flow/SellerFlow.kt | 2 +- .../node/testing/MockServiceHubInternal.kt | 3 - .../net/corda/testing/node/MockServices.kt | 6 +- 34 files changed, 589 insertions(+), 692 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt b/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt index a74d36d3de..600f548a80 100644 --- a/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt +++ b/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt @@ -1,12 +1,6 @@ package net.corda.core.node -import net.corda.core.contracts.ContractState -import net.corda.core.messaging.CordaRPCOps -import net.corda.core.node.services.VaultQueryService -import net.corda.core.schemas.MappedSchema -import net.corda.core.schemas.QueryableState import net.corda.core.serialization.SerializationCustomization -import java.util.function.Function /** * Implement this interface on a class advertised in a META-INF/services/net.corda.core.node.CordaPluginRegistry file diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index f4c0f205cd..5d23e76dab 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -64,11 +64,10 @@ interface ServiceHub : ServicesForResolution { /** * The vault service lets you observe, soft lock and add notes to states that involve you or are relevant to your - * node in some way. + * node in some way. Additionally you may query and track states that correspond to various criteria. */ val vaultService: VaultService - /** The vault query service lets you select and track states that correspond to various criteria. */ - val vaultQueryService: VaultQueryService + /** * The key management service is responsible for storing and using private keys to sign things. An * implementation of this may, for example, call out to a hardware security module that enforces various diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultQueryService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultQueryService.kt index deace9212f..e69de29bb2 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultQueryService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultQueryService.kt @@ -1,142 +0,0 @@ -package net.corda.core.node.services - -import net.corda.core.contracts.ContractState -import net.corda.core.flows.FlowException -import net.corda.core.messaging.DataFeed -import net.corda.core.node.services.vault.PageSpecification -import net.corda.core.node.services.vault.QueryCriteria -import net.corda.core.node.services.vault.Sort - -/** - * The vault query service lets you select and track states that correspond to various criteria. - */ -interface VaultQueryService { - // DOCSTART VaultQueryAPI - /** - * Generic vault query function which takes a [QueryCriteria] object to define filters, - * optional [PageSpecification] and optional [Sort] modification criteria (default unsorted), - * and returns a [Vault.Page] object containing the following: - * 1. states as a List of (page number and size defined by [PageSpecification]) - * 2. states metadata as a List of [Vault.StateMetadata] held in the Vault States table. - * 3. total number of results available if [PageSpecification] supplied (otherwise returns -1) - * 4. status types used in this query: UNCONSUMED, CONSUMED, ALL - * 5. other results (aggregate functions with/without using value groups) - * - * @throws VaultQueryException if the query cannot be executed for any reason - * (missing criteria or parsing error, paging errors, unsupported query, underlying database error) - * - * Notes - * If no [PageSpecification] is provided, a maximum of [DEFAULT_PAGE_SIZE] results will be returned. - * API users must specify a [PageSpecification] if they are expecting more than [DEFAULT_PAGE_SIZE] results, - * otherwise a [VaultQueryException] will be thrown alerting to this condition. - * It is the responsibility of the API user to request further pages and/or specify a more suitable [PageSpecification]. - */ - @Throws(VaultQueryException::class) - fun _queryBy(criteria: QueryCriteria, - paging: PageSpecification, - sorting: Sort, - contractStateType: Class): Vault.Page - - /** - * Generic vault query function which takes a [QueryCriteria] object to define filters, - * optional [PageSpecification] and optional [Sort] modification criteria (default unsorted), - * and returns a [Vault.PageAndUpdates] object containing - * 1) a snapshot as a [Vault.Page] (described previously in [queryBy]) - * 2) an [Observable] of [Vault.Update] - * - * @throws VaultQueryException if the query cannot be executed for any reason - * - * Notes: the snapshot part of the query adheres to the same behaviour as the [queryBy] function. - * the [QueryCriteria] applies to both snapshot and deltas (streaming updates). - */ - @Throws(VaultQueryException::class) - fun _trackBy(criteria: QueryCriteria, - paging: PageSpecification, - sorting: Sort, - contractStateType: Class): DataFeed, Vault.Update> - // DOCEND VaultQueryAPI - - // Note: cannot apply @JvmOverloads to interfaces nor interface implementations - // Java Helpers - fun queryBy(contractStateType: Class): Vault.Page { - return _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractStateType) - } - - fun queryBy(contractStateType: Class, criteria: QueryCriteria): Vault.Page { - return _queryBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType) - } - - fun queryBy(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification): Vault.Page { - return _queryBy(criteria, paging, Sort(emptySet()), contractStateType) - } - - fun queryBy(contractStateType: Class, criteria: QueryCriteria, sorting: Sort): Vault.Page { - return _queryBy(criteria, PageSpecification(), sorting, contractStateType) - } - - fun queryBy(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.Page { - return _queryBy(criteria, paging, sorting, contractStateType) - } - - fun trackBy(contractStateType: Class): DataFeed, Vault.Update> { - return _trackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractStateType) - } - - fun trackBy(contractStateType: Class, criteria: QueryCriteria): DataFeed, Vault.Update> { - return _trackBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType) - } - - fun trackBy(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification): DataFeed, Vault.Update> { - return _trackBy(criteria, paging, Sort(emptySet()), contractStateType) - } - - fun trackBy(contractStateType: Class, criteria: QueryCriteria, sorting: Sort): DataFeed, Vault.Update> { - return _trackBy(criteria, PageSpecification(), sorting, contractStateType) - } - - fun trackBy(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): DataFeed, Vault.Update> { - return _trackBy(criteria, paging, sorting, contractStateType) - } -} - -inline fun VaultQueryService.queryBy(): Vault.Page { - return _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), T::class.java) -} - -inline fun VaultQueryService.queryBy(criteria: QueryCriteria): Vault.Page { - return _queryBy(criteria, PageSpecification(), Sort(emptySet()), T::class.java) -} - -inline fun VaultQueryService.queryBy(criteria: QueryCriteria, paging: PageSpecification): Vault.Page { - return _queryBy(criteria, paging, Sort(emptySet()), T::class.java) -} - -inline fun VaultQueryService.queryBy(criteria: QueryCriteria, sorting: Sort): Vault.Page { - return _queryBy(criteria, PageSpecification(), sorting, T::class.java) -} - -inline fun VaultQueryService.queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.Page { - return _queryBy(criteria, paging, sorting, T::class.java) -} - -inline fun VaultQueryService.trackBy(): DataFeed, Vault.Update> { - return _trackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), T::class.java) -} - -inline fun VaultQueryService.trackBy(criteria: QueryCriteria): DataFeed, Vault.Update> { - return _trackBy(criteria, PageSpecification(), Sort(emptySet()), T::class.java) -} - -inline fun VaultQueryService.trackBy(criteria: QueryCriteria, paging: PageSpecification): DataFeed, Vault.Update> { - return _trackBy(criteria, paging, Sort(emptySet()), T::class.java) -} - -inline fun VaultQueryService.trackBy(criteria: QueryCriteria, sorting: Sort): DataFeed, Vault.Update> { - return _trackBy(criteria, PageSpecification(), sorting, T::class.java) -} - -inline fun VaultQueryService.trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): DataFeed, Vault.Update> { - return _trackBy(criteria, paging, sorting, T::class.java) -} - -class VaultQueryException(description: String) : FlowException(description) \ No newline at end of file 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 a2353a7b99..a16243a7db 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 @@ -6,7 +6,10 @@ import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowException import net.corda.core.identity.AbstractParty +import net.corda.core.messaging.DataFeed +import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.node.services.vault.Sort import net.corda.core.serialization.CordaSerializable import net.corda.core.toFuture import net.corda.core.utilities.NonEmptySet @@ -166,11 +169,6 @@ interface VaultService { */ val updates: Observable> - /** - * Enable creation of observables of updates. - */ - val updatesPublisher: PublishSubject> - /** * Provide a [CordaFuture] for when a [StateRef] is consumed, which can be very useful in building tests. */ @@ -221,7 +219,7 @@ interface VaultService { // DOCEND SoftLockAPI /** - * Helper function to combine using [VaultQueryService] calls to determine spendable states and soft locking them. + * Helper function to determine spendable states and soft locking them. * Currently performance will be worse than for the hand optimised version in `Cash.unconsumedCashStatesForSpending` * However, this is fully generic and can operate with custom [FungibleAsset] states. * @param lockId The [FlowLogic.runId.uuid] of the current flow used to soft lock the states. @@ -241,8 +239,135 @@ interface VaultService { amount: Amount, contractStateType: Class): List> + // DOCSTART VaultQueryAPI + /** + * Generic vault query function which takes a [QueryCriteria] object to define filters, + * optional [PageSpecification] and optional [Sort] modification criteria (default unsorted), + * and returns a [Vault.Page] object containing the following: + * 1. states as a List of (page number and size defined by [PageSpecification]) + * 2. states metadata as a List of [Vault.StateMetadata] held in the Vault States table. + * 3. total number of results available if [PageSpecification] supplied (otherwise returns -1) + * 4. status types used in this query: UNCONSUMED, CONSUMED, ALL + * 5. other results (aggregate functions with/without using value groups) + * + * @throws VaultQueryException if the query cannot be executed for any reason + * (missing criteria or parsing error, paging errors, unsupported query, underlying database error) + * + * Notes + * If no [PageSpecification] is provided, a maximum of [DEFAULT_PAGE_SIZE] results will be returned. + * API users must specify a [PageSpecification] if they are expecting more than [DEFAULT_PAGE_SIZE] results, + * otherwise a [VaultQueryException] will be thrown alerting to this condition. + * It is the responsibility of the API user to request further pages and/or specify a more suitable [PageSpecification]. + */ + @Throws(VaultQueryException::class) + fun _queryBy(criteria: QueryCriteria, + paging: PageSpecification, + sorting: Sort, + contractStateType: Class): Vault.Page + + /** + * Generic vault query function which takes a [QueryCriteria] object to define filters, + * optional [PageSpecification] and optional [Sort] modification criteria (default unsorted), + * and returns a [Vault.PageAndUpdates] object containing + * 1) a snapshot as a [Vault.Page] (described previously in [queryBy]) + * 2) an [Observable] of [Vault.Update] + * + * @throws VaultQueryException if the query cannot be executed for any reason + * + * Notes: the snapshot part of the query adheres to the same behaviour as the [queryBy] function. + * the [QueryCriteria] applies to both snapshot and deltas (streaming updates). + */ + @Throws(VaultQueryException::class) + fun _trackBy(criteria: QueryCriteria, + paging: PageSpecification, + sorting: Sort, + contractStateType: Class): DataFeed, Vault.Update> + // DOCEND VaultQueryAPI + + // Note: cannot apply @JvmOverloads to interfaces nor interface implementations + // Java Helpers + fun queryBy(contractStateType: Class): Vault.Page { + return _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractStateType) + } + + fun queryBy(contractStateType: Class, criteria: QueryCriteria): Vault.Page { + return _queryBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType) + } + + fun queryBy(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification): Vault.Page { + return _queryBy(criteria, paging, Sort(emptySet()), contractStateType) + } + + fun queryBy(contractStateType: Class, criteria: QueryCriteria, sorting: Sort): Vault.Page { + return _queryBy(criteria, PageSpecification(), sorting, contractStateType) + } + + fun queryBy(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.Page { + return _queryBy(criteria, paging, sorting, contractStateType) + } + + fun trackBy(contractStateType: Class): DataFeed, Vault.Update> { + return _trackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractStateType) + } + + fun trackBy(contractStateType: Class, criteria: QueryCriteria): DataFeed, Vault.Update> { + return _trackBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType) + } + + fun trackBy(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification): DataFeed, Vault.Update> { + return _trackBy(criteria, paging, Sort(emptySet()), contractStateType) + } + + fun trackBy(contractStateType: Class, criteria: QueryCriteria, sorting: Sort): DataFeed, Vault.Update> { + return _trackBy(criteria, PageSpecification(), sorting, contractStateType) + } + + fun trackBy(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): DataFeed, Vault.Update> { + return _trackBy(criteria, paging, sorting, contractStateType) + } } +inline fun VaultService.queryBy(): Vault.Page { + return _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), T::class.java) +} + +inline fun VaultService.queryBy(criteria: QueryCriteria): Vault.Page { + return _queryBy(criteria, PageSpecification(), Sort(emptySet()), T::class.java) +} + +inline fun VaultService.queryBy(criteria: QueryCriteria, paging: PageSpecification): Vault.Page { + return _queryBy(criteria, paging, Sort(emptySet()), T::class.java) +} + +inline fun VaultService.queryBy(criteria: QueryCriteria, sorting: Sort): Vault.Page { + return _queryBy(criteria, PageSpecification(), sorting, T::class.java) +} + +inline fun VaultService.queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.Page { + return _queryBy(criteria, paging, sorting, T::class.java) +} + +inline fun VaultService.trackBy(): DataFeed, Vault.Update> { + return _trackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), T::class.java) +} + +inline fun VaultService.trackBy(criteria: QueryCriteria): DataFeed, Vault.Update> { + return _trackBy(criteria, PageSpecification(), Sort(emptySet()), T::class.java) +} + +inline fun VaultService.trackBy(criteria: QueryCriteria, paging: PageSpecification): DataFeed, Vault.Update> { + return _trackBy(criteria, paging, Sort(emptySet()), T::class.java) +} + +inline fun VaultService.trackBy(criteria: QueryCriteria, sorting: Sort): DataFeed, Vault.Update> { + return _trackBy(criteria, PageSpecification(), sorting, T::class.java) +} + +inline fun VaultService.trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): DataFeed, Vault.Update> { + return _trackBy(criteria, paging, sorting, T::class.java) +} + +class VaultQueryException(description: String) : FlowException(description) class StatesNotAvailableException(override val message: String?, override val cause: Throwable? = null) : FlowException(message, cause) { override fun toString() = "Soft locking error: $message" diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index 4ef934798b..aeca91feb4 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -219,14 +219,14 @@ class ContractUpgradeFlowTest { val stx = result.getOrThrow().stx val anonymisedRecipient = result.get().recipient!! val stateAndRef = stx.tx.outRef(0) - val baseState = a.database.transaction { a.services.vaultQueryService.queryBy().states.single() } + val baseState = a.database.transaction { a.services.vaultService.queryBy().states.single() } assertTrue(baseState.state.data is Cash.State, "Contract state is old version.") // Starts contract upgrade flow. val upgradeResult = a.services.startFlow(ContractUpgradeFlow.Initiate(stateAndRef, CashV2::class.java)).resultFuture mockNet.runNetwork() upgradeResult.getOrThrow() // Get contract state from the vault. - val firstState = a.database.transaction { a.services.vaultQueryService.queryBy().states.single() } + val firstState = a.database.transaction { a.services.vaultService.queryBy().states.single() } assertTrue(firstState.state.data is CashV2.State, "Contract state is upgraded to the new version.") assertEquals(Amount(1000000, USD).`issued by`(chosenIdentity.ref(1)), (firstState.state.data as CashV2.State).amount, "Upgraded cash contain the correct amount.") assertEquals>(listOf(anonymisedRecipient), (firstState.state.data as CashV2.State).owners, "Upgraded cash belongs to the right owner.") diff --git a/docs/source/api-vault-query.rst b/docs/source/api-vault-query.rst index 9f19f1087a..0b9944948d 100644 --- a/docs/source/api-vault-query.rst +++ b/docs/source/api-vault-query.rst @@ -10,9 +10,9 @@ Corda provides a number of flexible query mechanisms for accessing the Vault: - custom JPA_/JPQL_ queries - custom 3rd party Data Access frameworks such as `Spring Data `_ -The majority of query requirements can be satisfied by using the Vault Query API, which is exposed via the ``VaultQueryService`` for use directly by flows: +The majority of query requirements can be satisfied by using the Vault Query API, which is exposed via the ``VaultService`` for use directly by flows: -.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/node/services/VaultQueryService.kt +.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/node/services/VaultService.kt :language: kotlin :start-after: DOCSTART VaultQueryAPI :end-before: DOCEND VaultQueryAPI 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/FlowCookbookJava.java index bfa1b329d4..3777536aae 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/FlowCookbookJava.java @@ -223,7 +223,7 @@ public class FlowCookbookJava { // For example, we would extract any unconsumed ``DummyState``s // from our vault as follows: VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED); - Page results = getServiceHub().getVaultQueryService().queryBy(DummyState.class, criteria); + Page results = getServiceHub().getVaultService().queryBy(DummyState.class, criteria); List> dummyStates = results.getStates(); // For a full list of the available ways of extracting states from 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/FlowCookbook.kt index 3e720145bf..4b0864d714 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/FlowCookbook.kt @@ -206,7 +206,7 @@ object FlowCookbook { // For example, we would extract any unconsumed ``DummyState``s // from our vault as follows: val criteria: VaultQueryCriteria = VaultQueryCriteria() // default is UNCONSUMED - val results: Page = serviceHub.vaultQueryService.queryBy(criteria) + val results: Page = serviceHub.vaultService.queryBy(criteria) val dummyStates: List> = results.states // For a full list of the available ways of extracting states from 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/WorkflowTransactionBuildTutorial.kt index 2532577f9a..9b31523586 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/WorkflowTransactionBuildTutorial.kt @@ -129,7 +129,7 @@ class SubmitCompletionFlow(private val ref: StateRef, private val verdict: Workf override fun call(): StateAndRef { // DOCSTART 1 val criteria = VaultQueryCriteria(stateRefs = listOf(ref)) - val latestRecord = serviceHub.vaultQueryService.queryBy(criteria).states.single() + val latestRecord = serviceHub.vaultService.queryBy(criteria).states.single() // DOCEND 1 // Check the protocol hasn't already been run 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 d625c1483e..831e8db3fb 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 @@ -27,7 +27,7 @@ class WorkflowTransactionBuildTutorialTest { // Helper method to locate the latest Vault version of a LinearState private inline fun ServiceHub.latest(ref: UniqueIdentifier): StateAndRef { - val linearHeads = vaultQueryService.queryBy(QueryCriteria.LinearStateQueryCriteria(uuid = listOf(ref.id))) + val linearHeads = vaultService.queryBy(QueryCriteria.LinearStateQueryCriteria(uuid = listOf(ref.id))) return linearHeads.states.single() } diff --git a/docs/source/tutorial-building-transactions.rst b/docs/source/tutorial-building-transactions.rst index 18734ca89f..b8c25bf04a 100644 --- a/docs/source/tutorial-building-transactions.rst +++ b/docs/source/tutorial-building-transactions.rst @@ -87,7 +87,7 @@ One of the first steps to forming a transaction is gathering the set of input references. This process will clearly vary according to the nature of the business process being captured by the smart contract and the parameterised details of the request. However, it will generally involve -searching the Vault via the ``VaultQueryService`` interface on the +searching the Vault via the ``VaultService`` interface on the ``ServiceHub`` to locate the input states. To give a few more specific details consider two simplified real world @@ -161,7 +161,7 @@ in the right workflow state over the RPC interface. The RPC will then initiate the relevant flow using ``StateRef``, or ``linearId`` values as 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 ``VaultQueryService`` as follows: +the ``VaultService`` as follows: .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt :language: kotlin 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 2e28208a6f..d96b5b5c09 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt @@ -59,7 +59,7 @@ fun CordaRPCOps.getCashBalance(currency: Currency): Amount { } fun ServiceHub.getCashBalance(currency: Currency): Amount { - val results = this.vaultQueryService.queryBy>(generateCashSumCriteria(currency)) + val results = this.vaultService.queryBy>(generateCashSumCriteria(currency)) return rowsToAmount(currency, results) } @@ -69,7 +69,7 @@ fun CordaRPCOps.getCashBalances(): Map> { } fun ServiceHub.getCashBalances(): Map> { - val sums = this.vaultQueryService.queryBy>(generateCashSumsCriteria()).otherResults + val sums = this.vaultService.queryBy>(generateCashSumsCriteria()).otherResults return rowsToBalances(sums) } diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt index 763f0d4cff..f0084fb169 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt @@ -59,7 +59,7 @@ class CashExitFlow(private val amount: Amount, } // Work out who the owners of the burnt states were (specify page size so we don't silently drop any if > DEFAULT_PAGE_SIZE) - val inputStates = serviceHub.vaultQueryService.queryBy(VaultQueryCriteria(stateRefs = builder.inputStates()), + val inputStates = serviceHub.vaultService.queryBy(VaultQueryCriteria(stateRefs = builder.inputStates()), PageSpecification(pageNumber = DEFAULT_PAGE_NUM, pageSize = builder.inputStates().size)).states // TODO: Is it safe to drop participants we don't know how to contact? Does not knowing how to contact them diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt index b9b7bd7091..f30507ec3f 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt @@ -71,7 +71,7 @@ class CashTests : TestDependencyInjectionBase() { ownedBy = OUR_IDENTITY_1, issuedBy = MINI_CORP.ref(1), issuerServices = miniCorpServices) } database.transaction { - vaultStatesUnconsumed = miniCorpServices.vaultQueryService.queryBy().states + vaultStatesUnconsumed = miniCorpServices.vaultService.queryBy().states } resetTestSerialization() } diff --git a/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt b/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt index 5fe62e9d3c..5a26f1196f 100644 --- a/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt @@ -10,14 +10,10 @@ import net.corda.finance.DOLLARS import net.corda.finance.`issued by` import net.corda.finance.contracts.asset.Cash import net.corda.node.internal.StartedNode -import net.corda.testing.chooseIdentity -import net.corda.testing.expect -import net.corda.testing.expectEvents -import net.corda.testing.getDefaultNotary +import net.corda.testing.* import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode -import net.corda.testing.setCordappPackages import org.junit.After import org.junit.Before import org.junit.Test @@ -61,8 +57,8 @@ class CashPaymentFlowTests { bankOfCordaNode.database.transaction { // Register for vault updates val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) - val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultQueryService.trackBy(criteria) - val (_, vaultUpdatesBankClient) = notaryNode.services.vaultQueryService.trackBy(criteria) + val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultService.trackBy(criteria) + val (_, vaultUpdatesBankClient) = notaryNode.services.vaultService.trackBy(criteria) val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expectedPayment, payTo)).resultFuture 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 50e9ac4866..6ba72a75d6 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -35,8 +35,8 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.debug import net.corda.node.internal.classloading.requireAnnotation import net.corda.node.internal.cordapp.CordappLoader -import net.corda.node.services.ContractUpgradeHandler import net.corda.node.internal.cordapp.CordappProviderImpl +import net.corda.node.services.ContractUpgradeHandler import net.corda.node.services.FinalityHandler import net.corda.node.services.NotaryChangeHandler import net.corda.node.services.api.* @@ -63,7 +63,6 @@ import net.corda.node.services.statemachine.appName import net.corda.node.services.statemachine.flowVersionAndInitiatingClass import net.corda.node.services.transactions.* import net.corda.node.services.upgrade.ContractUpgradeServiceImpl -import net.corda.node.services.vault.HibernateVaultQueryImpl import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.VaultSoftLockManager import net.corda.node.utilities.* @@ -83,11 +82,9 @@ import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import java.sql.Connection import java.time.Clock -import java.util.* import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ExecutorService import java.util.concurrent.TimeUnit.SECONDS -import kotlin.collections.ArrayList import kotlin.collections.set import kotlin.reflect.KClass import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair @@ -389,7 +386,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, network = makeMessagingService(legalIdentity) info = makeInfo(legalIdentity) - val tokenizableServices = mutableListOf(attachments, network, services.vaultService, services.vaultQueryService, + val tokenizableServices = mutableListOf(attachments, network, services.vaultService, services.keyManagementService, services.identityService, platformClock, services.schedulerService, services.auditService, services.monitoringService, services.networkMapCache, services.schemaService, services.transactionVerifierService, services.validatedTransactions, services.contractUpgradeService, @@ -692,11 +689,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, override val transactionVerifierService by lazy { makeTransactionVerifierService() } override val schemaService by lazy { NodeSchemaService() } override val networkMapCache by lazy { PersistentNetworkMapCache(this) } - override val vaultService by lazy { NodeVaultService(this) } + override val vaultService by lazy { NodeVaultService(this, database.hibernateConfig) } override val contractUpgradeService by lazy { ContractUpgradeServiceImpl() } - override val vaultQueryService by lazy { - HibernateVaultQueryImpl(database.hibernateConfig, vaultService) - } + // 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. 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 ae8afce323..c902e8964a 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -56,7 +56,7 @@ class CordaRPCOpsImpl( sorting: Sort, contractStateType: Class): Vault.Page { return database.transaction { - services.vaultQueryService._queryBy(criteria, paging, sorting, contractStateType) + services.vaultService._queryBy(criteria, paging, sorting, contractStateType) } } @@ -66,7 +66,7 @@ class CordaRPCOpsImpl( sorting: Sort, contractStateType: Class): DataFeed, Vault.Update> { return database.transaction { - services.vaultQueryService._trackBy(criteria, paging, sorting, contractStateType) + services.vaultService._trackBy(criteria, paging, sorting, contractStateType) } } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt index 3501c2db34..e69de29bb2 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt @@ -1,204 +0,0 @@ -package net.corda.node.services.vault - -import net.corda.core.contracts.ContractState -import net.corda.core.contracts.StateAndRef -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionState -import net.corda.core.crypto.SecureHash -import net.corda.core.internal.ThreadBox -import net.corda.core.internal.bufferUntilSubscribed -import net.corda.core.internal.uncheckedCast -import net.corda.core.messaging.DataFeed -import net.corda.core.node.services.Vault -import net.corda.core.node.services.VaultQueryException -import net.corda.core.node.services.VaultQueryService -import net.corda.core.node.services.VaultService -import net.corda.core.node.services.vault.* -import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria -import net.corda.core.serialization.SerializationDefaults.STORAGE_CONTEXT -import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.serialization.deserialize -import net.corda.core.utilities.debug -import net.corda.core.utilities.loggerFor -import net.corda.core.utilities.trace -import net.corda.node.services.persistence.HibernateConfiguration -import net.corda.node.utilities.DatabaseTransactionManager -import org.hibernate.Session -import rx.Observable -import java.lang.Exception -import java.util.* -import javax.persistence.Tuple - -class HibernateVaultQueryImpl(val hibernateConfig: HibernateConfiguration, - val vault: VaultService) : SingletonSerializeAsToken(), VaultQueryService { - companion object { - val log = loggerFor() - } - - private var sessionFactory = hibernateConfig.sessionFactoryForRegisteredSchemas() - private var criteriaBuilder = sessionFactory.criteriaBuilder - - /** - * Maintain a list of contract state interfaces to concrete types stored in the vault - * for usage in generic queries of type queryBy or queryBy> - */ - private val contractStateTypeMappings = bootstrapContractStateTypes() - - init { - vault.rawUpdates.subscribe { update -> - update.produced.forEach { - val concreteType = it.state.data.javaClass - log.trace { "State update of type: $concreteType" } - val seen = contractStateTypeMappings.any { it.value.contains(concreteType.name) } - if (!seen) { - val contractInterfaces = deriveContractInterfaces(concreteType) - contractInterfaces.map { - val contractInterface = contractStateTypeMappings.getOrPut(it.name, { mutableSetOf() }) - contractInterface.add(concreteType.name) - } - } - } - } - } - - @Throws(VaultQueryException::class) - override fun _queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class): Vault.Page { - log.info("Vault Query for contract type: $contractStateType, criteria: $criteria, pagination: $paging, sorting: $sorting") - - // refresh to include any schemas registered after initial VQ service initialisation - sessionFactory = hibernateConfig.sessionFactoryForRegisteredSchemas() - criteriaBuilder = sessionFactory.criteriaBuilder - - // calculate total results where a page specification has been defined - var totalStates = -1L - if (!paging.isDefault) { - val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } - val countCriteria = VaultCustomQueryCriteria(count, Vault.StateStatus.ALL) - val results = queryBy(contractStateType, criteria.and(countCriteria)) - totalStates = results.otherResults[0] as Long - } - - val session = getSession() - - session.use { - val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java) - val queryRootVaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) - - // TODO: revisit (use single instance of parser for all queries) - val criteriaParser = HibernateQueryCriteriaParser(contractStateType, contractStateTypeMappings, criteriaBuilder, criteriaQuery, queryRootVaultStates) - - try { - // parse criteria and build where predicates - criteriaParser.parse(criteria, sorting) - - // prepare query for execution - val query = session.createQuery(criteriaQuery) - - // pagination checks - if (!paging.isDefault) { - // pagination - if (paging.pageNumber < DEFAULT_PAGE_NUM) throw VaultQueryException("Page specification: invalid page number ${paging.pageNumber} [page numbers start from $DEFAULT_PAGE_NUM]") - if (paging.pageSize < 1) throw VaultQueryException("Page specification: invalid page size ${paging.pageSize} [must be a value between 1 and $MAX_PAGE_SIZE]") - } - - query.firstResult = (paging.pageNumber - 1) * paging.pageSize - query.maxResults = paging.pageSize + 1 // detection too many results - - // execution - val results = query.resultList - - // final pagination check (fail-fast on too many results when no pagination specified) - if (paging.isDefault && results.size > DEFAULT_PAGE_SIZE) - throw VaultQueryException("Please specify a `PageSpecification` as there are more results [${results.size}] than the default page size [$DEFAULT_PAGE_SIZE]") - - val statesAndRefs: MutableList> = mutableListOf() - val statesMeta: MutableList = mutableListOf() - val otherResults: MutableList = mutableListOf() - - results.asSequence() - .forEachIndexed { index, result -> - if (result[0] is VaultSchemaV1.VaultStates) { - if (!paging.isDefault && index == paging.pageSize) // skip last result if paged - return@forEachIndexed - val vaultState = result[0] as VaultSchemaV1.VaultStates - val stateRef = StateRef(SecureHash.parse(vaultState.stateRef!!.txId!!), vaultState.stateRef!!.index!!) - val state = vaultState.contractState.deserialize>(context = STORAGE_CONTEXT) - statesMeta.add(Vault.StateMetadata(stateRef, - vaultState.contractStateClassName, - vaultState.recordedTime, - vaultState.consumedTime, - vaultState.stateStatus, - vaultState.notary, - vaultState.lockId, - vaultState.lockUpdateTime)) - statesAndRefs.add(StateAndRef(state, stateRef)) - } - else { - // TODO: improve typing of returned other results - log.debug { "OtherResults: ${Arrays.toString(result.toArray())}" } - otherResults.addAll(result.toArray().asList()) - } - } - - return Vault.Page(states = statesAndRefs, statesMetadata = statesMeta, stateTypes = criteriaParser.stateTypes, totalStatesAvailable = totalStates, otherResults = otherResults) - } catch (e: Exception) { - log.error(e.message) - throw e.cause ?: e - } - } - } - - private val mutex = ThreadBox({ vault.updatesPublisher }) - - @Throws(VaultQueryException::class) - override fun _trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class): DataFeed, Vault.Update> { - return mutex.locked { - val snapshotResults = _queryBy(criteria, paging, sorting, contractStateType) - val updates: Observable> = uncheckedCast(vault.updatesPublisher.bufferUntilSubscribed().filter { it.containsType(contractStateType, snapshotResults.stateTypes) }) - DataFeed(snapshotResults, updates) - } - } - - private fun getSession(): Session { - return sessionFactory.withOptions(). - connection(DatabaseTransactionManager.current().connection). - openSession() - } - - /** - * Derive list from existing vault states and then incrementally update using vault observables - */ - fun bootstrapContractStateTypes(): MutableMap> { - val criteria = criteriaBuilder.createQuery(String::class.java) - val vaultStates = criteria.from(VaultSchemaV1.VaultStates::class.java) - criteria.select(vaultStates.get("contractStateClassName")).distinct(true) - val session = getSession() - session.use { - val query = session.createQuery(criteria) - val results = query.resultList - val distinctTypes = results.map { it } - - val contractInterfaceToConcreteTypes = mutableMapOf>() - distinctTypes.forEach { type -> - val concreteType: Class = uncheckedCast(Class.forName(type)) - val contractInterfaces = deriveContractInterfaces(concreteType) - contractInterfaces.map { - val contractInterface = contractInterfaceToConcreteTypes.getOrPut(it.name, { mutableSetOf() }) - contractInterface.add(concreteType.name) - } - } - return contractInterfaceToConcreteTypes - } - } - - private fun deriveContractInterfaces(clazz: Class): Set> { - val myInterfaces: MutableSet> = mutableSetOf() - clazz.interfaces.forEach { - if (!it.equals(ContractState::class.java)) { - myInterfaces.add(uncheckedCast(it)) - myInterfaces.addAll(deriveContractInterfaces(uncheckedCast(it))) - } - } - return myInterfaces - } -} \ No newline at end of file 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 196dca299f..15796f15eb 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 @@ -6,14 +6,15 @@ import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.internal.ThreadBox import net.corda.core.internal.VisibleForTesting +import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.internal.tee +import net.corda.core.messaging.DataFeed import net.corda.core.node.ServiceHub import net.corda.core.node.services.StatesNotAvailableException import net.corda.core.node.services.Vault +import net.corda.core.node.services.VaultQueryException import net.corda.core.node.services.VaultService -import net.corda.core.node.services.vault.QueryCriteria -import net.corda.core.node.services.vault.Sort -import net.corda.core.node.services.vault.SortAttribute +import net.corda.core.node.services.vault.* import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.SerializationDefaults.STORAGE_CONTEXT import net.corda.core.serialization.SingletonSerializeAsToken @@ -23,15 +24,18 @@ import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.NotaryChangeWireTransaction import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.* +import net.corda.node.services.persistence.HibernateConfiguration import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.utilities.DatabaseTransactionManager import net.corda.node.utilities.bufferUntilDatabaseCommit import net.corda.node.utilities.wrapWithDatabaseTransaction +import org.hibernate.Session import rx.Observable import rx.subjects.PublishSubject import java.security.PublicKey import java.time.Instant import java.util.* +import javax.persistence.Tuple /** * Currently, the node vault service is a very simple RDBMS backed implementation. It will change significantly when @@ -43,7 +47,7 @@ import java.util.* * TODO: keep an audit trail with time stamps of previously unconsumed states "as of" a particular point in time. * TODO: have transaction storage do some caching. */ -class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsToken(), VaultService { +class NodeVaultService(private val services: ServiceHub, private val hibernateConfig: HibernateConfiguration) : SingletonSerializeAsToken(), VaultService { private companion object { val log = loggerFor() @@ -102,9 +106,6 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT override val updates: Observable> get() = mutex.locked { _updatesInDbTx } - override val updatesPublisher: PublishSubject> - get() = mutex.locked { _updatesPublisher } - /** * Splits the provided [txns] into batches of [WireTransaction] and [NotaryChangeWireTransaction]. * This is required because the batches get aggregated into single updates, and we want to be able to @@ -347,7 +348,7 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT val enrichedCriteria = QueryCriteria.VaultQueryCriteria( contractStateTypes = setOf(contractStateType), softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId))) - val results = services.vaultQueryService.queryBy(contractStateType, enrichedCriteria.and(eligibleStatesQuery), sorter) + val results = queryBy(contractStateType, enrichedCriteria.and(eligibleStatesQuery), sorter) var claimedAmount = 0L val claimedStates = mutableListOf>() @@ -368,8 +369,6 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT return claimedStates } - - @VisibleForTesting internal fun isRelevant(state: ContractState, myKeys: Set): Boolean { val keysToCheck = when (state) { @@ -378,4 +377,172 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT } return keysToCheck.any { it in myKeys } } + + private var sessionFactory = hibernateConfig.sessionFactoryForRegisteredSchemas() + private var criteriaBuilder = sessionFactory.criteriaBuilder + + /** + * Maintain a list of contract state interfaces to concrete types stored in the vault + * for usage in generic queries of type queryBy or queryBy> + */ + private val contractStateTypeMappings = bootstrapContractStateTypes() + + init { + rawUpdates.subscribe { update -> + update.produced.forEach { + val concreteType = it.state.data.javaClass + log.trace { "State update of type: $concreteType" } + val seen = contractStateTypeMappings.any { it.value.contains(concreteType.name) } + if (!seen) { + val contractInterfaces = deriveContractInterfaces(concreteType) + contractInterfaces.map { + val contractInterface = contractStateTypeMappings.getOrPut(it.name, { mutableSetOf() }) + contractInterface.add(concreteType.name) + } + } + } + } + } + + @Throws(VaultQueryException::class) + override fun _queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class): Vault.Page { + log.info("Vault Query for contract type: $contractStateType, criteria: $criteria, pagination: $paging, sorting: $sorting") + + // refresh to include any schemas registered after initial VQ service initialisation + sessionFactory = hibernateConfig.sessionFactoryForRegisteredSchemas() + criteriaBuilder = sessionFactory.criteriaBuilder + + // calculate total results where a page specification has been defined + var totalStates = -1L + if (!paging.isDefault) { + val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } + val countCriteria = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.ALL) + val results = queryBy(contractStateType, criteria.and(countCriteria)) + totalStates = results.otherResults[0] as Long + } + + val session = getSession() + + session.use { + val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java) + val queryRootVaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + + // TODO: revisit (use single instance of parser for all queries) + val criteriaParser = HibernateQueryCriteriaParser(contractStateType, contractStateTypeMappings, criteriaBuilder, criteriaQuery, queryRootVaultStates) + + try { + // parse criteria and build where predicates + criteriaParser.parse(criteria, sorting) + + // prepare query for execution + val query = session.createQuery(criteriaQuery) + + // pagination checks + if (!paging.isDefault) { + // pagination + if (paging.pageNumber < DEFAULT_PAGE_NUM) throw VaultQueryException("Page specification: invalid page number ${paging.pageNumber} [page numbers start from ${DEFAULT_PAGE_NUM}]") + if (paging.pageSize < 1) throw VaultQueryException("Page specification: invalid page size ${paging.pageSize} [must be a value between 1 and ${MAX_PAGE_SIZE}]") + } + + query.firstResult = (paging.pageNumber - 1) * paging.pageSize + query.maxResults = paging.pageSize + 1 // detection too many results + + // execution + val results = query.resultList + + // final pagination check (fail-fast on too many results when no pagination specified) + if (paging.isDefault && results.size > DEFAULT_PAGE_SIZE) + throw VaultQueryException("Please specify a `PageSpecification` as there are more results [${results.size}] than the default page size [${DEFAULT_PAGE_SIZE}]") + + val statesAndRefs: MutableList> = mutableListOf() + val statesMeta: MutableList = mutableListOf() + val otherResults: MutableList = mutableListOf() + + results.asSequence() + .forEachIndexed { index, result -> + if (result[0] is VaultSchemaV1.VaultStates) { + if (!paging.isDefault && index == paging.pageSize) // skip last result if paged + return@forEachIndexed + val vaultState = result[0] as VaultSchemaV1.VaultStates + val stateRef = StateRef(SecureHash.parse(vaultState.stateRef!!.txId!!), vaultState.stateRef!!.index!!) + val state = vaultState.contractState.deserialize>(context = STORAGE_CONTEXT) + statesMeta.add(Vault.StateMetadata(stateRef, + vaultState.contractStateClassName, + vaultState.recordedTime, + vaultState.consumedTime, + vaultState.stateStatus, + vaultState.notary, + vaultState.lockId, + vaultState.lockUpdateTime)) + statesAndRefs.add(StateAndRef(state, stateRef)) + } + else { + // TODO: improve typing of returned other results + log.debug { "OtherResults: ${Arrays.toString(result.toArray())}" } + otherResults.addAll(result.toArray().asList()) + } + } + + return Vault.Page(states = statesAndRefs, statesMetadata = statesMeta, stateTypes = criteriaParser.stateTypes, totalStatesAvailable = totalStates, otherResults = otherResults) + } catch (e: java.lang.Exception) { + log.error(e.message) + throw e.cause ?: e + } + } + } + + @Throws(VaultQueryException::class) + override fun _trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class): DataFeed, Vault.Update> { + return mutex.locked { + val snapshotResults = _queryBy(criteria, paging, sorting, contractStateType) + @Suppress("UNCHECKED_CAST") + val updates = _updatesPublisher.bufferUntilSubscribed().filter { it.containsType(contractStateType, snapshotResults.stateTypes) } as Observable> + DataFeed(snapshotResults, updates) + } + } + + private fun getSession(): Session { + return sessionFactory.withOptions(). + connection(DatabaseTransactionManager.current().connection). + openSession() + } + + /** + * Derive list from existing vault states and then incrementally update using vault observables + */ + private fun bootstrapContractStateTypes(): MutableMap> { + val criteria = criteriaBuilder.createQuery(String::class.java) + val vaultStates = criteria.from(VaultSchemaV1.VaultStates::class.java) + criteria.select(vaultStates.get("contractStateClassName")).distinct(true) + val session = getSession() + session.use { + val query = session.createQuery(criteria) + val results = query.resultList + val distinctTypes = results.map { it } + + val contractInterfaceToConcreteTypes = mutableMapOf>() + distinctTypes.forEach { type -> + @Suppress("UNCHECKED_CAST") + val concreteType = Class.forName(type) as Class + val contractInterfaces = deriveContractInterfaces(concreteType) + contractInterfaces.map { + val contractInterface = contractInterfaceToConcreteTypes.getOrPut(it.name, { mutableSetOf() }) + contractInterface.add(concreteType.name) + } + } + return contractInterfaceToConcreteTypes + } + } + + private fun deriveContractInterfaces(clazz: Class): Set> { + val myInterfaces: MutableSet> = mutableSetOf() + clazz.interfaces.forEach { + if (!it.equals(ContractState::class.java)) { + @Suppress("UNCHECKED_CAST") + myInterfaces.add(it as Class) + myInterfaces.addAll(deriveContractInterfaces(it)) + } + } + return myInterfaces + } } diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index f963189d1f..6a38dde99a 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -1,63 +1,44 @@ package net.corda.node.services.vault; -import com.google.common.collect.ImmutableSet; -import kotlin.Pair; -import kotlin.Triple; +import com.google.common.collect.*; +import kotlin.*; import net.corda.core.contracts.*; -import net.corda.core.identity.AbstractParty; -import net.corda.core.messaging.DataFeed; -import net.corda.core.node.services.IdentityService; -import net.corda.core.node.services.Vault; -import net.corda.core.node.services.VaultQueryException; -import net.corda.core.node.services.VaultQueryService; +import net.corda.core.identity.*; +import net.corda.core.messaging.*; +import net.corda.core.node.services.*; import net.corda.core.node.services.vault.*; -import net.corda.core.node.services.vault.QueryCriteria.LinearStateQueryCriteria; -import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria; -import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria; -import net.corda.core.schemas.MappedSchema; -import net.corda.core.utilities.EncodingUtils; -import net.corda.core.utilities.OpaqueBytes; -import net.corda.finance.contracts.DealState; -import net.corda.finance.contracts.asset.Cash; -import net.corda.finance.contracts.asset.CashUtilities; -import net.corda.finance.schemas.CashSchemaV1; -import net.corda.node.utilities.CordaPersistence; -import net.corda.node.utilities.DatabaseTransaction; -import net.corda.testing.TestConstants; -import net.corda.testing.TestDependencyInjectionBase; -import net.corda.testing.contracts.DummyLinearContract; -import net.corda.testing.contracts.VaultFiller; -import net.corda.testing.node.MockServices; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +import net.corda.core.node.services.vault.QueryCriteria.*; +import net.corda.core.schemas.*; +import net.corda.core.utilities.*; +import net.corda.finance.contracts.*; +import net.corda.finance.contracts.asset.*; +import net.corda.finance.schemas.*; +import net.corda.node.utilities.*; +import net.corda.testing.*; +import net.corda.testing.contracts.*; +import net.corda.testing.node.*; +import org.junit.*; import rx.Observable; -import java.io.IOException; -import java.lang.reflect.Field; -import java.security.KeyPair; +import java.io.*; +import java.lang.reflect.*; +import java.security.*; import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; +import java.util.stream.*; -import static net.corda.core.node.services.vault.QueryCriteriaUtils.DEFAULT_PAGE_NUM; -import static net.corda.core.node.services.vault.QueryCriteriaUtils.MAX_PAGE_SIZE; -import static net.corda.core.utilities.ByteArrays.toHexString; -import static net.corda.finance.contracts.asset.CashUtilities.getDUMMY_CASH_ISSUER; -import static net.corda.finance.contracts.asset.CashUtilities.getDUMMY_CASH_ISSUER_KEY; +import static net.corda.core.node.services.vault.QueryCriteriaUtils.*; +import static net.corda.core.utilities.ByteArrays.*; +import static net.corda.finance.contracts.asset.CashUtilities.*; import static net.corda.testing.CoreTestUtils.*; -import static net.corda.testing.TestConstants.getDUMMY_NOTARY; -import static net.corda.testing.TestConstants.getDUMMY_NOTARY_KEY; -import static net.corda.testing.node.MockServices.makeTestDatabaseAndMockServices; -import static net.corda.testing.node.MockServices.makeTestIdentityService; -import static org.assertj.core.api.Assertions.assertThat; +import static net.corda.testing.TestConstants.*; +import static net.corda.testing.node.MockServices.*; +import static org.assertj.core.api.Assertions.*; public class VaultQueryJavaTests extends TestDependencyInjectionBase { private MockServices services; private MockServices issuerServices; - private VaultQueryService vaultQuerySvc; + private VaultService vaultService; private CordaPersistence database; @Before @@ -73,7 +54,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { issuerServices = new MockServices(getDUMMY_CASH_ISSUER_KEY(), getBOC_KEY()); database = databaseAndServices.getFirst(); services = databaseAndServices.getSecond(); - vaultQuerySvc = services.getVaultQueryService(); + vaultService = services.getVaultService(); } @After @@ -98,7 +79,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { }); database.transaction(tx -> { // DOCSTART VaultJavaQueryExample0 - Vault.Page results = vaultQuerySvc.queryBy(LinearState.class); + Vault.Page results = vaultService.queryBy(LinearState.class); // DOCEND VaultJavaQueryExample0 assertThat(results.getStates()).hasSize(3); @@ -121,7 +102,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { SortAttribute.Standard sortAttribute = new SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID); Sort sorting = new Sort(Collections.singletonList(new Sort.SortColumn(sortAttribute, Sort.Direction.ASC))); VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, null, stateRefs); - Vault.Page results = vaultQuerySvc.queryBy(DummyLinearContract.State.class, criteria, sorting); + Vault.Page results = vaultService.queryBy(DummyLinearContract.State.class, criteria, sorting); assertThat(results.getStates()).hasSize(2); @@ -156,7 +137,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { database.transaction(tx -> { // DOCSTART VaultJavaQueryExample1 VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.CONSUMED); - Vault.Page results = vaultQuerySvc.queryBy(Cash.State.class, criteria); + Vault.Page results = vaultService.queryBy(Cash.State.class, criteria); // DOCEND VaultJavaQueryExample1 assertThat(results.getStates()).hasSize(3); @@ -202,7 +183,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { PageSpecification pageSpec = new PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE); Sort.SortColumn sortByUid = new Sort.SortColumn(new SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC); Sort sorting = new Sort(ImmutableSet.of(sortByUid)); - Vault.Page results = vaultQuerySvc.queryBy(LinearState.class, compositeCriteria2, pageSpec, sorting); + Vault.Page results = vaultService.queryBy(LinearState.class, compositeCriteria2, pageSpec, sorting); // DOCEND VaultJavaQueryExample2 assertThat(results.getStates()).hasSize(4); @@ -243,7 +224,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { QueryCriteria criteria = generalCriteria.and(customCriteria1).and(customCriteria2); - Vault.Page results = vaultQuerySvc.queryBy(Cash.State.class, criteria); + Vault.Page results = vaultService.queryBy(Cash.State.class, criteria); // DOCEND VaultJavaQueryExample3 assertThat(results.getStates()).hasSize(2); @@ -279,7 +260,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { Set> contractStateTypes = new HashSet(Collections.singletonList(Cash.State.class)); VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, contractStateTypes); - DataFeed, Vault.Update> results = vaultQuerySvc.trackBy(ContractState.class, criteria); + DataFeed, Vault.Update> results = vaultService.trackBy(ContractState.class, criteria); Vault.Page snapshot = results.getSnapshot(); Observable> updates = results.getUpdates(); @@ -318,7 +299,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { PageSpecification pageSpec = new PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE); Sort.SortColumn sortByUid = new Sort.SortColumn(new SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC); Sort sorting = new Sort(ImmutableSet.of(sortByUid)); - DataFeed, Vault.Update> results = vaultQuerySvc.trackBy(ContractState.class, compositeCriteria, pageSpec, sorting); + DataFeed, Vault.Update> results = vaultService.trackBy(ContractState.class, compositeCriteria, pageSpec, sorting); Vault.Page snapshot = results.getSnapshot(); // DOCEND VaultJavaQueryExample5 @@ -364,7 +345,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { QueryCriteria avgCriteria = new VaultCustomQueryCriteria(Builder.avg(pennies)); QueryCriteria criteria = sumCriteria.and(countCriteria).and(maxCriteria).and(minCriteria).and(avgCriteria); - Vault.Page results = vaultQuerySvc.queryBy(Cash.State.class, criteria); + Vault.Page results = vaultService.queryBy(Cash.State.class, criteria); // DOCEND VaultJavaQueryExample21 assertThat(results.getOtherResults()).hasSize(5); @@ -413,7 +394,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { QueryCriteria avgCriteria = new VaultCustomQueryCriteria(Builder.avg(pennies, Collections.singletonList(currency))); QueryCriteria criteria = sumCriteria.and(countCriteria).and(maxCriteria).and(minCriteria).and(avgCriteria); - Vault.Page results = vaultQuerySvc.queryBy(Cash.State.class, criteria); + Vault.Page results = vaultService.queryBy(Cash.State.class, criteria); // DOCEND VaultJavaQueryExample22 assertThat(results.getOtherResults()).hasSize(27); @@ -480,7 +461,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { QueryCriteria sumCriteria = new VaultCustomQueryCriteria(Builder.sum(pennies, Arrays.asList(issuerParty, currency), Sort.Direction.DESC)); - Vault.Page results = vaultQuerySvc.queryBy(Cash.State.class, sumCriteria); + Vault.Page results = vaultService.queryBy(Cash.State.class, sumCriteria); // DOCEND VaultJavaQueryExample23 assertThat(results.getOtherResults()).hasSize(12); diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index f713bf0f5e..ecd2e604d0 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -100,7 +100,7 @@ class CordaRPCOpsImplTest { // Check the monitoring service wallet is empty aliceNode.database.transaction { - assertFalse(aliceNode.services.vaultQueryService.queryBy().totalStatesAvailable > 0) + assertFalse(aliceNode.services.vaultService.queryBy().totalStatesAvailable > 0) } // Tell the monitoring service node to issue some cash diff --git a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt index 33202b4874..0b7466dddf 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt @@ -96,7 +96,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { overrideClock = testClock, keyManagement = kms, network = mockMessagingService), TestReference { - override val vaultService: VaultService = NodeVaultService(this) + override val vaultService: VaultService = NodeVaultService(this, database.hibernateConfig) override val testReference = this@NodeSchedulerServiceTest override val cordappProvider: CordappProviderImpl = CordappProviderImpl(CordappLoader.createWithTestPackages()).start(attachments) } diff --git a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt index bfd6557377..072fd74a05 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt @@ -5,7 +5,7 @@ import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.* import net.corda.core.flows.* import net.corda.core.identity.Party -import net.corda.core.node.services.VaultQueryService +import net.corda.core.node.services.VaultService import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.DEFAULT_PAGE_NUM import net.corda.core.node.services.vault.PageSpecification @@ -128,10 +128,10 @@ class ScheduledFlowTests { nodeA.services.startFlow(InsertInitialStateFlow(nodeB.info.chooseIdentity())) mockNet.waitQuiescent() val stateFromA = nodeA.database.transaction { - nodeA.services.vaultQueryService.queryBy().states.single() + nodeA.services.vaultService.queryBy().states.single() } val stateFromB = nodeB.database.transaction { - nodeB.services.vaultQueryService.queryBy().states.single() + nodeB.services.vaultService.queryBy().states.single() } assertEquals(1, countScheduledFlows) assertEquals("Must be same copy on both nodes", stateFromA, stateFromB) @@ -153,10 +153,10 @@ class ScheduledFlowTests { // Convert the states into maps to make error reporting easier val statesFromA: List> = nodeA.database.transaction { - queryStatesWithPaging(nodeA.services.vaultQueryService) + queryStatesWithPaging(nodeA.services.vaultService) } val statesFromB: List> = nodeB.database.transaction { - queryStatesWithPaging(nodeB.services.vaultQueryService) + queryStatesWithPaging(nodeB.services.vaultService) } assertEquals("Expect all states to be present",2 * N, statesFromA.count()) statesFromA.forEach { ref -> @@ -179,13 +179,13 @@ class ScheduledFlowTests { * * @return states ordered by the transaction ID. */ - private fun queryStatesWithPaging(vaultQueryService: VaultQueryService): List> { + private fun queryStatesWithPaging(vaultService: VaultService): List> { // DOCSTART VaultQueryExamplePaging var pageNumber = DEFAULT_PAGE_NUM val states = mutableListOf>() do { val pageSpec = PageSpecification(pageSize = PAGE_SIZE, pageNumber = pageNumber) - val results = vaultQueryService.queryBy(VaultQueryCriteria(), pageSpec, SORTING) + val results = vaultService.queryBy(VaultQueryCriteria(), pageSpec, SORTING) states.addAll(results.states) pageNumber++ } while ((pageSpec.pageSize * (pageNumber)) <= results.totalStatesAvailable) 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 ec6a134f13..4ea017461d 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 @@ -51,7 +51,7 @@ class DBTransactionStorageTests : TestDependencyInjectionBase() { services = object : MockServices(BOB_KEY) { override val vaultService: VaultService get() { - val vaultService = NodeVaultService(this) + val vaultService = NodeVaultService(this, database.hibernateConfig) hibernatePersister = HibernateObserver(vaultService.rawUpdates, database.hibernateConfig) return vaultService } diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 43869643f7..a08230d9ff 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -1024,9 +1024,9 @@ class FlowFrameworkTests { override fun call(): List> { val otherPartySession = initiateFlow(otherParty) otherPartySession.send(stx) - // hold onto reference here to force checkpoint of vaultQueryService and thus + // hold onto reference here to force checkpoint of vaultService and thus // prove it is registered as a tokenizableService in the node - val vaultQuerySvc = serviceHub.vaultQueryService + val vaultQuerySvc = serviceHub.vaultService waitForLedgerCommit(stx.id) return vaultQuerySvc.queryBy().states } 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 b5c9dc19d7..2880435862 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 @@ -47,8 +47,7 @@ import kotlin.test.assertTrue class NodeVaultServiceTest : TestDependencyInjectionBase() { lateinit var services: MockServices lateinit var issuerServices: MockServices - val vaultSvc: VaultService get() = services.vaultService - val vaultQuery: VaultQueryService get() = services.vaultQueryService + val vaultService: VaultService get() = services.vaultService lateinit var database: CordaPersistence @Before @@ -94,11 +93,10 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 3, 3, Random(0L)) } database.transaction { - val w1 = vaultQuery.queryBy().states + val w1 = vaultService.queryBy().states assertThat(w1).hasSize(3) - val originalVault = vaultSvc - val originalVaultQuery = vaultQuery + val originalVault = vaultService val services2 = object : MockServices() { override val vaultService: NodeVaultService get() = originalVault as NodeVaultService override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { @@ -107,11 +105,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { vaultService.notify(stx.tx) } } - - override val vaultQueryService: VaultQueryService get() = originalVaultQuery } - val w2 = services2.vaultQueryService.queryBy().states + val w2 = services2.vaultService.queryBy().states assertThat(w2).hasSize(3) } } @@ -122,10 +118,10 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 3, 3, Random(0L)) } database.transaction { - val w1 = vaultQuery.queryBy().states + val w1 = vaultService.queryBy().states assertThat(w1).hasSize(3) - val states = vaultQuery.queryBy(VaultQueryCriteria(stateRefs = listOf(w1[1].ref, w1[2].ref))).states + val states = vaultService.queryBy(VaultQueryCriteria(stateRefs = listOf(w1[1].ref, w1[2].ref))).states assertThat(states).hasSize(2) } } @@ -137,34 +133,34 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { } database.transaction { - val unconsumedStates = vaultQuery.queryBy().states + val unconsumedStates = vaultService.queryBy().states assertThat(unconsumedStates).hasSize(3) val stateRefsToSoftLock = NonEmptySet.of(unconsumedStates[1].ref, unconsumedStates[2].ref) // soft lock two of the three states val softLockId = UUID.randomUUID() - vaultSvc.softLockReserve(softLockId, stateRefsToSoftLock) + vaultService.softLockReserve(softLockId, stateRefsToSoftLock) // all softlocked states val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY)) - assertThat(vaultQuery.queryBy(criteriaLocked).states).hasSize(2) + assertThat(vaultService.queryBy(criteriaLocked).states).hasSize(2) // my softlocked states val criteriaByLockId = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.SPECIFIED, listOf(softLockId))) - assertThat(vaultQuery.queryBy(criteriaByLockId).states).hasSize(2) + assertThat(vaultService.queryBy(criteriaByLockId).states).hasSize(2) // excluding softlocked states - val unlockedStates1 = vaultQuery.queryBy(VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.UNLOCKED_ONLY))).states + val unlockedStates1 = vaultService.queryBy(VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.UNLOCKED_ONLY))).states assertThat(unlockedStates1).hasSize(1) // soft lock release one of the states explicitly - vaultSvc.softLockRelease(softLockId, NonEmptySet.of(unconsumedStates[1].ref)) - val unlockedStates2 = vaultQuery.queryBy(VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.UNLOCKED_ONLY))).states + vaultService.softLockRelease(softLockId, NonEmptySet.of(unconsumedStates[1].ref)) + val unlockedStates2 = vaultService.queryBy(VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.UNLOCKED_ONLY))).states assertThat(unlockedStates2).hasSize(2) // soft lock release the rest by id - vaultSvc.softLockRelease(softLockId) - val unlockedStates = vaultQuery.queryBy(VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.UNLOCKED_ONLY))).states + vaultService.softLockRelease(softLockId) + val unlockedStates = vaultService.queryBy(VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.UNLOCKED_ONLY))).states assertThat(unlockedStates).hasSize(3) // should be back to original states @@ -195,8 +191,8 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { backgroundExecutor.submit { try { database.transaction { - vaultSvc.softLockReserve(softLockId1, stateRefsToSoftLock) - assertThat(vaultQuery.queryBy(criteriaByLockId1).states).hasSize(3) + vaultService.softLockReserve(softLockId1, stateRefsToSoftLock) + assertThat(vaultService.queryBy(criteriaByLockId1).states).hasSize(3) } println("SOFT LOCK STATES #1 succeeded") } catch(e: Throwable) { @@ -211,8 +207,8 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { try { Thread.sleep(100) // let 1st thread soft lock them 1st database.transaction { - vaultSvc.softLockReserve(softLockId2, stateRefsToSoftLock) - assertThat(vaultQuery.queryBy(criteriaByLockId2).states).hasSize(3) + vaultService.softLockReserve(softLockId2, stateRefsToSoftLock) + assertThat(vaultService.queryBy(criteriaByLockId2).states).hasSize(3) } println("SOFT LOCK STATES #2 succeeded") } catch(e: Throwable) { @@ -224,10 +220,10 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { countDown.await() database.transaction { - val lockStatesId1 = vaultQuery.queryBy(criteriaByLockId1).states + val lockStatesId1 = vaultService.queryBy(criteriaByLockId1).states println("SOFT LOCK #1 final states: $lockStatesId1") assertThat(lockStatesId1.size).isIn(0, 3) - val lockStatesId2 = vaultQuery.queryBy(criteriaByLockId2).states + val lockStatesId2 = vaultService.queryBy(criteriaByLockId2).states println("SOFT LOCK #2 final states: $lockStatesId2") assertThat(lockStatesId2.size).isIn(0, 3) } @@ -248,15 +244,15 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { // lock 1st state with LockId1 database.transaction { - vaultSvc.softLockReserve(softLockId1, NonEmptySet.of(stateRefsToSoftLock.first())) + vaultService.softLockReserve(softLockId1, NonEmptySet.of(stateRefsToSoftLock.first())) val criteriaByLockId1 = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.SPECIFIED, listOf(softLockId1))) - assertThat(vaultQuery.queryBy(criteriaByLockId1).states).hasSize(1) + assertThat(vaultService.queryBy(criteriaByLockId1).states).hasSize(1) } // attempt to lock all 3 states with LockId2 database.transaction { assertThatExceptionOfType(StatesNotAvailableException::class.java).isThrownBy( - { vaultSvc.softLockReserve(softLockId2, stateRefsToSoftLock.toNonEmptySet()) } + { vaultService.softLockReserve(softLockId2, stateRefsToSoftLock.toNonEmptySet()) } ).withMessageContaining("only 2 rows available").withNoCause() } } @@ -276,14 +272,14 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { // lock states with LockId1 database.transaction { - vaultSvc.softLockReserve(softLockId1, stateRefsToSoftLock) - assertThat(vaultQuery.queryBy(criteriaByLockId1).states).hasSize(3) + vaultService.softLockReserve(softLockId1, stateRefsToSoftLock) + assertThat(vaultService.queryBy(criteriaByLockId1).states).hasSize(3) } // attempt to relock same states with LockId1 database.transaction { - vaultSvc.softLockReserve(softLockId1, stateRefsToSoftLock) - assertThat(vaultQuery.queryBy(criteriaByLockId1).states).hasSize(3) + vaultService.softLockReserve(softLockId1, stateRefsToSoftLock) + assertThat(vaultService.queryBy(criteriaByLockId1).states).hasSize(3) } } @@ -303,14 +299,14 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { // lock states with LockId1 database.transaction { - vaultSvc.softLockReserve(softLockId1, NonEmptySet.of(stateRefsToSoftLock.first())) - assertThat(vaultQuery.queryBy(criteriaByLockId1).states).hasSize(1) + vaultService.softLockReserve(softLockId1, NonEmptySet.of(stateRefsToSoftLock.first())) + assertThat(vaultService.queryBy(criteriaByLockId1).states).hasSize(1) } // attempt to lock all states with LockId1 (including previously already locked one) database.transaction { - vaultSvc.softLockReserve(softLockId1, stateRefsToSoftLock.toNonEmptySet()) - assertThat(vaultQuery.queryBy(criteriaByLockId1).states).hasSize(3) + vaultService.softLockReserve(softLockId1, stateRefsToSoftLock.toNonEmptySet()) + assertThat(vaultService.queryBy(criteriaByLockId1).states).hasSize(3) } } @@ -321,15 +317,15 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { } database.transaction { - val unconsumedStates = vaultQuery.queryBy().states + val unconsumedStates = vaultService.queryBy().states assertThat(unconsumedStates).hasSize(1) - val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(100.DOLLARS) + val spendableStatesUSD = vaultService.unconsumedCashStatesForSpending(100.DOLLARS) spendableStatesUSD.forEach(::println) assertThat(spendableStatesUSD).hasSize(1) assertThat(spendableStatesUSD[0].state.data.amount.quantity).isEqualTo(100L * 100) val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY)) - assertThat(vaultQuery.queryBy(criteriaLocked).states).hasSize(1) + assertThat(vaultService.queryBy(criteriaLocked).states).hasSize(1) } } @@ -340,7 +336,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1))) } database.transaction { - val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(200.DOLLARS, + val spendableStatesUSD = vaultService.unconsumedCashStatesForSpending(200.DOLLARS, onlyFromIssuerParties = setOf(DUMMY_CASH_ISSUER.party, BOC)) spendableStatesUSD.forEach(::println) assertThat(spendableStatesUSD).hasSize(2) @@ -359,10 +355,10 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(3)), ref = OpaqueBytes.of(3)) } database.transaction { - val unconsumedStates = vaultQuery.queryBy().states + val unconsumedStates = vaultService.queryBy().states assertThat(unconsumedStates).hasSize(4) - val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(200.DOLLARS, + val spendableStatesUSD = vaultService.unconsumedCashStatesForSpending(200.DOLLARS, onlyFromIssuerParties = setOf(BOC), withIssuerRefs = setOf(OpaqueBytes.of(1), OpaqueBytes.of(2))) assertThat(spendableStatesUSD).hasSize(2) assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.party).isEqualTo(BOC) @@ -378,14 +374,14 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L)) } database.transaction { - val unconsumedStates = vaultQuery.queryBy().states + val unconsumedStates = vaultService.queryBy().states assertThat(unconsumedStates).hasSize(1) - val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(110.DOLLARS) + val spendableStatesUSD = vaultService.unconsumedCashStatesForSpending(110.DOLLARS) spendableStatesUSD.forEach(::println) assertThat(spendableStatesUSD).hasSize(0) val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY)) - assertThat(vaultQuery.queryBy(criteriaLocked).states).hasSize(0) + assertThat(vaultService.queryBy(criteriaLocked).states).hasSize(0) } } @@ -395,15 +391,15 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 2, 2, Random(0L)) } database.transaction { - val unconsumedStates = vaultQuery.queryBy().states + val unconsumedStates = vaultService.queryBy().states assertThat(unconsumedStates).hasSize(2) - val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(1.DOLLARS) + val spendableStatesUSD = vaultService.unconsumedCashStatesForSpending(1.DOLLARS) spendableStatesUSD.forEach(::println) assertThat(spendableStatesUSD).hasSize(1) assertThat(spendableStatesUSD[0].state.data.amount.quantity).isGreaterThanOrEqualTo(100L) val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY)) - assertThat(vaultQuery.queryBy(criteriaLocked).states).hasSize(1) + assertThat(vaultService.queryBy(criteriaLocked).states).hasSize(1) } } @@ -416,18 +412,18 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { } database.transaction { var unlockedStates = 30 - val allStates = vaultQuery.queryBy().states + val allStates = vaultService.queryBy().states assertThat(allStates).hasSize(unlockedStates) var lockedCount = 0 for (i in 1..5) { val lockId = UUID.randomUUID() - val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(20.DOLLARS, lockId = lockId) + val spendableStatesUSD = vaultService.unconsumedCashStatesForSpending(20.DOLLARS, lockId = lockId) spendableStatesUSD.forEach(::println) assertThat(spendableStatesUSD.size <= unlockedStates) unlockedStates -= spendableStatesUSD.size val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.SPECIFIED, listOf(lockId))) - val lockedStates = vaultQuery.queryBy(criteriaLocked).states + val lockedStates = vaultService.queryBy(criteriaLocked).states if (spendableStatesUSD.isNotEmpty()) { assertEquals(spendableStatesUSD.size, lockedStates.size) val lockedTotal = lockedStates.map { it.state.data }.sumCash() @@ -438,7 +434,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { } } val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY)) - assertThat(vaultQuery.queryBy(criteriaLocked).states).hasSize(lockedCount) + assertThat(vaultService.queryBy(criteriaLocked).states).hasSize(lockedCount) } } @@ -457,10 +453,10 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { services.recordTransactions(usefulTX) - vaultSvc.addNoteToTransaction(usefulTX.id, "USD Sample Note 1") - vaultSvc.addNoteToTransaction(usefulTX.id, "USD Sample Note 2") - vaultSvc.addNoteToTransaction(usefulTX.id, "USD Sample Note 3") - assertEquals(3, vaultSvc.getTransactionNotes(usefulTX.id).count()) + vaultService.addNoteToTransaction(usefulTX.id, "USD Sample Note 1") + vaultService.addNoteToTransaction(usefulTX.id, "USD Sample Note 2") + vaultService.addNoteToTransaction(usefulTX.id, "USD Sample Note 3") + assertEquals(3, vaultService.getTransactionNotes(usefulTX.id).count()) // Issue more Money (GBP) val anotherBuilder = TransactionBuilder(null).apply { @@ -470,8 +466,8 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { services.recordTransactions(anotherTX) - vaultSvc.addNoteToTransaction(anotherTX.id, "GBP Sample Note 1") - assertEquals(1, vaultSvc.getTransactionNotes(anotherTX.id).count()) + vaultService.addNoteToTransaction(anotherTX.id, "GBP Sample Note 1") + assertEquals(1, vaultService.getTransactionNotes(anotherTX.id).count()) } } 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 b6aabe44c6..74c799fc57 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 @@ -48,8 +48,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { private lateinit var services: MockServices private lateinit var notaryServices: MockServices - private val vaultSvc: VaultService get() = services.vaultService - private val vaultQuerySvc: VaultQueryService get() = services.vaultQueryService + private val vaultService: VaultService get() = services.vaultService private val identitySvc: IdentityService = makeTestIdentityService() private lateinit var database: CordaPersistence @@ -146,7 +145,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { // DOCSTART VaultQueryExample1 - val result = vaultQuerySvc.queryBy() + val result = vaultService.queryBy() /** * Query result returns a [Vault.Page] which contains: @@ -173,7 +172,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val criteria = VaultQueryCriteria() // default is UNCONSUMED - val result = vaultQuerySvc.queryBy(criteria) + val result = vaultService.queryBy(criteria) assertThat(result.states).hasSize(16) assertThat(result.statesMetadata).hasSize(16) @@ -191,7 +190,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val paging = PageSpecification(DEFAULT_PAGE_NUM, 10) database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val resultsBeforeConsume = vaultQuerySvc.queryBy(criteria, paging) + val resultsBeforeConsume = vaultService.queryBy(criteria, paging) assertThat(resultsBeforeConsume.states).hasSize(4) assertThat(resultsBeforeConsume.totalStatesAvailable).isEqualTo(4) } @@ -200,7 +199,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val consumedCriteria = VaultQueryCriteria(status = Vault.StateStatus.UNCONSUMED) - val resultsAfterConsume = vaultQuerySvc.queryBy(consumedCriteria, paging) + val resultsAfterConsume = vaultService.queryBy(consumedCriteria, paging) assertThat(resultsAfterConsume.states).hasSize(1) assertThat(resultsAfterConsume.totalStatesAvailable).isEqualTo(1) } @@ -214,7 +213,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { services.fillWithSomeTestDeals(listOf("123", "456", "789")) } database.transaction { - val result = vaultQuerySvc.queryBy() + val result = vaultService.queryBy() assertThat(result.states).hasSize(3) assertThat(result.statesMetadata).hasSize(3) @@ -230,7 +229,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val criteria = VaultQueryCriteria() // default is UNCONSUMED - val result = vaultQuerySvc.queryBy(criteria) + val result = vaultService.queryBy(criteria) assertThat(result.states).hasSize(3) assertThat(result.statesMetadata).hasSize(3) @@ -254,7 +253,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF) val criteria = VaultQueryCriteria() - val results = vaultQuerySvc.queryBy(criteria, Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC)))) + val results = vaultService.queryBy(criteria, Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC)))) // default StateRef sort is by index then txnId: // order by @@ -285,7 +284,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val sortBy = Sort(setOf(Sort.SortColumn(sortAttributeTxnId, Sort.Direction.ASC), Sort.SortColumn(sortAttributeIndex, Sort.Direction.ASC))) val criteria = VaultQueryCriteria() - val results = vaultQuerySvc.queryBy(criteria, sortBy) + val results = vaultService.queryBy(criteria, sortBy) results.statesMetadata.forEach { println(" ${it.ref}") @@ -309,7 +308,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { // DOCSTART VaultQueryExample2 val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID) val criteria = VaultQueryCriteria(stateRefs = listOf(stateRefs.first(), stateRefs.last())) - val results = vaultQuerySvc.queryBy(criteria, Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC)))) + val results = vaultService.queryBy(criteria, Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC)))) // DOCEND VaultQueryExample2 assertThat(results.states).hasSize(2) @@ -331,7 +330,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { // default State.Status is UNCONSUMED // DOCSTART VaultQueryExample3 val criteria = VaultQueryCriteria(contractStateTypes = setOf(Cash.State::class.java, DealState::class.java)) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) // DOCEND VaultQueryExample3 assertThat(results.states).hasSize(6) } @@ -351,7 +350,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(5) } } @@ -367,7 +366,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val paging = PageSpecification(DEFAULT_PAGE_NUM, 10) database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val resultsBeforeConsume = vaultQuerySvc.queryBy(criteria, paging) + val resultsBeforeConsume = vaultService.queryBy(criteria, paging) assertThat(resultsBeforeConsume.states).hasSize(4) assertThat(resultsBeforeConsume.totalStatesAvailable).isEqualTo(4) } @@ -376,7 +375,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val consumedCriteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) - val resultsAfterConsume = vaultQuerySvc.queryBy(consumedCriteria, paging) + val resultsAfterConsume = vaultService.queryBy(consumedCriteria, paging) assertThat(resultsAfterConsume.states).hasSize(3) assertThat(resultsAfterConsume.totalStatesAvailable).isEqualTo(3) } @@ -396,7 +395,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(17) } } @@ -409,7 +408,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) val paging = PageSpecification(DEFAULT_PAGE_NUM, 10) database.transaction { - val resultsBeforeConsume = vaultQuerySvc.queryBy(criteria, paging) + val resultsBeforeConsume = vaultService.queryBy(criteria, paging) assertThat(resultsBeforeConsume.states).hasSize(1) assertThat(resultsBeforeConsume.totalStatesAvailable).isEqualTo(1) } @@ -417,7 +416,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { services.consumeCash(50.DOLLARS, notary = DUMMY_NOTARY) // consumed 100 (spent), produced 50 (change) } database.transaction { - val resultsAfterConsume = vaultQuerySvc.queryBy(criteria, paging) + val resultsAfterConsume = vaultService.queryBy(criteria, paging) assertThat(resultsAfterConsume.states).hasSize(2) assertThat(resultsAfterConsume.totalStatesAvailable).isEqualTo(2) } @@ -433,7 +432,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { // DOCSTART VaultQueryExample4 val criteria = VaultQueryCriteria(notary = listOf(CASH_NOTARY)) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) // DOCEND VaultQueryExample4 assertThat(results.states).hasSize(3) } @@ -450,7 +449,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val criteria = LinearStateQueryCriteria(participants = listOf(BIG_CORP)) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(3) } } @@ -467,7 +466,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { // DOCSTART VaultQueryExample5 val criteria = LinearStateQueryCriteria(participants = listOf(BIG_CORP, MINI_CORP)) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) // DOCEND VaultQueryExample5 assertThat(results.states).hasSize(3) @@ -479,44 +478,44 @@ class VaultQueryTests : TestDependencyInjectionBase() { val (lockId1, lockId2) = database.transaction { val issuedStates = services.fillWithSomeTestCash(100.DOLLARS, notaryServices, CASH_NOTARY, 10, 10, Random(0L)).states.toList() - vaultSvc.softLockReserve(UUID.randomUUID(), NonEmptySet.of(issuedStates[1].ref, issuedStates[2].ref, issuedStates[3].ref)) + vaultService.softLockReserve(UUID.randomUUID(), NonEmptySet.of(issuedStates[1].ref, issuedStates[2].ref, issuedStates[3].ref)) val lockId1 = UUID.randomUUID() - vaultSvc.softLockReserve(lockId1, NonEmptySet.of(issuedStates[4].ref, issuedStates[5].ref)) + vaultService.softLockReserve(lockId1, NonEmptySet.of(issuedStates[4].ref, issuedStates[5].ref)) val lockId2 = UUID.randomUUID() - vaultSvc.softLockReserve(lockId2, NonEmptySet.of(issuedStates[6].ref)) + vaultService.softLockReserve(lockId2, NonEmptySet.of(issuedStates[6].ref)) Pair(lockId1, lockId2) } database.transaction { // excluding soft locked states val criteriaExclusive = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.UNLOCKED_ONLY)) - val resultsExclusive = vaultQuerySvc.queryBy(criteriaExclusive) + val resultsExclusive = vaultService.queryBy(criteriaExclusive) assertThat(resultsExclusive.states).hasSize(4) // only soft locked states val criteriaLockedOnly = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY)) - val resultsLockedOnly = vaultQuerySvc.queryBy(criteriaLockedOnly) + val resultsLockedOnly = vaultService.queryBy(criteriaLockedOnly) assertThat(resultsLockedOnly.states).hasSize(6) // soft locked states by single lock id val criteriaByLockId = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.SPECIFIED, listOf(lockId1))) - val resultsByLockId = vaultQuerySvc.queryBy(criteriaByLockId) + val resultsByLockId = vaultService.queryBy(criteriaByLockId) assertThat(resultsByLockId.states).hasSize(2) // soft locked states by multiple lock ids val criteriaByLockIds = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.SPECIFIED, listOf(lockId1, lockId2))) - val resultsByLockIds = vaultQuerySvc.queryBy(criteriaByLockIds) + val resultsByLockIds = vaultService.queryBy(criteriaByLockIds) assertThat(resultsByLockIds.states).hasSize(3) // unlocked and locked by `lockId2` val criteriaUnlockedAndByLockId = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId2))) - val resultsUnlockedAndByLockIds = vaultQuerySvc.queryBy(criteriaUnlockedAndByLockId) + val resultsUnlockedAndByLockIds = vaultService.queryBy(criteriaUnlockedAndByLockId) assertThat(resultsUnlockedAndByLockIds.states).hasSize(5) // missing lockId expectedEx.expect(IllegalArgumentException::class.java) expectedEx.expectMessage("Must specify one or more lockIds") val criteriaMissingLockId = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.UNLOCKED_AND_SPECIFIED)) - vaultQuerySvc.queryBy(criteriaMissingLockId) + vaultService.queryBy(criteriaMissingLockId) } } @@ -530,7 +529,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.equal(GBP.currencyCode) } val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(1) } } @@ -545,7 +544,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.notEqual(GBP.currencyCode) } val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(2) } } @@ -560,7 +559,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.greaterThan(1000L) } val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(1) } } @@ -575,7 +574,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.greaterThanOrEqual(1000L) } val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(2) } } @@ -590,7 +589,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.lessThan(1000L) } val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(1) } } @@ -605,7 +604,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.lessThanOrEqual(1000L) } val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(2) } } @@ -620,7 +619,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.between(500L, 1500L) } val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(1) } } @@ -636,7 +635,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val currencies = listOf(CHF.currencyCode, GBP.currencyCode) val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.`in`(currencies) } val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(2) } } @@ -652,7 +651,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val currencies = listOf(CHF.currencyCode, GBP.currencyCode) val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.notIn(currencies) } val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(1) } } @@ -667,7 +666,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.like("%BP") } // GPB val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(1) } } @@ -682,7 +681,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.notLike("%BP") } // GPB val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(2) } } @@ -697,7 +696,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::issuerParty.isNull() } val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(0) } } @@ -712,7 +711,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::issuerParty.notNull() } val criteria = VaultCustomQueryCriteria(logicalExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(3) } } @@ -743,7 +742,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val avg = builder { CashSchemaV1.PersistentCashState::pennies.avg() } val avgCriteria = VaultCustomQueryCriteria(avg) - val results = vaultQuerySvc.queryBy>(sumCriteria + val results = vaultService.queryBy>(sumCriteria .and(countCriteria) .and(maxCriteria) .and(minCriteria) @@ -782,7 +781,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val avg = builder { CashSchemaV1.PersistentCashState::pennies.avg(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } val avgCriteria = VaultCustomQueryCriteria(avg) - val results = vaultQuerySvc.queryBy>(sumCriteria + val results = vaultService.queryBy>(sumCriteria .and(maxCriteria) .and(minCriteria) .and(avgCriteria)) @@ -837,7 +836,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { orderBy = Sort.Direction.DESC) } - val results = vaultQuerySvc.queryBy>(VaultCustomQueryCriteria(sum)) + val results = vaultService.queryBy>(VaultCustomQueryCriteria(sum)) // DOCEND VaultQueryExample23 assertThat(results.otherResults).hasSize(12) @@ -871,15 +870,15 @@ class VaultQueryTests : TestDependencyInjectionBase() { // count fungible assets val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } val countCriteria = QueryCriteria.VaultCustomQueryCriteria(count) - val fungibleStateCount = vaultQuerySvc.queryBy>(countCriteria).otherResults.single() as Long + val fungibleStateCount = vaultService.queryBy>(countCriteria).otherResults.single() as Long assertThat(fungibleStateCount).isEqualTo(10L) // count linear states - val linearStateCount = vaultQuerySvc.queryBy(countCriteria).otherResults.single() as Long + val linearStateCount = vaultService.queryBy(countCriteria).otherResults.single() as Long assertThat(linearStateCount).isEqualTo(9L) // count deal states - val dealStateCount = vaultQuerySvc.queryBy(countCriteria).otherResults.single() as Long + val dealStateCount = vaultService.queryBy(countCriteria).otherResults.single() as Long assertThat(dealStateCount).isEqualTo(3L) } } @@ -900,15 +899,15 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { // count fungible assets val countCriteria = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.ALL) - val fungibleStateCount = vaultQuerySvc.queryBy>(countCriteria).otherResults.single() as Long + val fungibleStateCount = vaultService.queryBy>(countCriteria).otherResults.single() as Long assertThat(fungibleStateCount).isEqualTo(10L) // count linear states - val linearStateCount = vaultQuerySvc.queryBy(countCriteria).otherResults.single() as Long + val linearStateCount = vaultService.queryBy(countCriteria).otherResults.single() as Long assertThat(linearStateCount).isEqualTo(9L) // count deal states - val dealStateCount = vaultQuerySvc.queryBy(countCriteria).otherResults.single() as Long + val dealStateCount = vaultService.queryBy(countCriteria).otherResults.single() as Long assertThat(dealStateCount).isEqualTo(3L) } val cashUpdates = @@ -924,30 +923,30 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { // count fungible assets val countCriteriaUnconsumed = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.UNCONSUMED) - val fungibleStateCountUnconsumed = vaultQuerySvc.queryBy>(countCriteriaUnconsumed).otherResults.single() as Long + val fungibleStateCountUnconsumed = vaultService.queryBy>(countCriteriaUnconsumed).otherResults.single() as Long assertThat(fungibleStateCountUnconsumed.toInt()).isEqualTo(10 - cashUpdates.consumed.size + cashUpdates.produced.size) // count linear states - val linearStateCountUnconsumed = vaultQuerySvc.queryBy(countCriteriaUnconsumed).otherResults.single() as Long + val linearStateCountUnconsumed = vaultService.queryBy(countCriteriaUnconsumed).otherResults.single() as Long assertThat(linearStateCountUnconsumed).isEqualTo(5L) // count deal states - val dealStateCountUnconsumed = vaultQuerySvc.queryBy(countCriteriaUnconsumed).otherResults.single() as Long + val dealStateCountUnconsumed = vaultService.queryBy(countCriteriaUnconsumed).otherResults.single() as Long assertThat(dealStateCountUnconsumed).isEqualTo(2L) // CONSUMED states // count fungible assets val countCriteriaConsumed = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.CONSUMED) - val fungibleStateCountConsumed = vaultQuerySvc.queryBy>(countCriteriaConsumed).otherResults.single() as Long + val fungibleStateCountConsumed = vaultService.queryBy>(countCriteriaConsumed).otherResults.single() as Long assertThat(fungibleStateCountConsumed.toInt()).isEqualTo(cashUpdates.consumed.size) // count linear states - val linearStateCountConsumed = vaultQuerySvc.queryBy(countCriteriaConsumed).otherResults.single() as Long + val linearStateCountConsumed = vaultService.queryBy(countCriteriaConsumed).otherResults.single() as Long assertThat(linearStateCountConsumed).isEqualTo(4L) // count deal states - val dealStateCountConsumed = vaultQuerySvc.queryBy(countCriteriaConsumed).otherResults.single() as Long + val dealStateCountConsumed = vaultService.queryBy(countCriteriaConsumed).otherResults.single() as Long assertThat(dealStateCountConsumed).isEqualTo(1L) } } @@ -967,7 +966,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { QueryCriteria.TimeInstantType.RECORDED, ColumnPredicate.Between(start, end)) val criteria = VaultQueryCriteria(timeCondition = recordedBetweenExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) // DOCEND VaultQueryExample6 assertThat(results.states).hasSize(3) @@ -976,7 +975,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val recordedBetweenExpressionFuture = TimeCondition( QueryCriteria.TimeInstantType.RECORDED, ColumnPredicate.Between(startFuture, end)) val criteriaFuture = VaultQueryCriteria(timeCondition = recordedBetweenExpressionFuture) - assertThat(vaultQuerySvc.queryBy(criteriaFuture).states).isEmpty() + assertThat(vaultService.queryBy(criteriaFuture).states).isEmpty() } } @@ -996,7 +995,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { QueryCriteria.TimeInstantType.CONSUMED, ColumnPredicate.BinaryComparison(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, asOfDateTime)) val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED, timeCondition = consumedAfterExpression) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(3) } @@ -1012,7 +1011,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { // DOCSTART VaultQueryExample7 val pagingSpec = PageSpecification(DEFAULT_PAGE_NUM, 10) val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val results = vaultQuerySvc.queryBy(criteria, paging = pagingSpec) + val results = vaultService.queryBy(criteria, paging = pagingSpec) // DOCEND VaultQueryExample7 assertThat(results.states).hasSize(10) assertThat(results.totalStatesAvailable).isEqualTo(100) @@ -1031,7 +1030,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val pagingSpec = PageSpecification(10, 10) val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val results = vaultQuerySvc.queryBy(criteria, paging = pagingSpec) + val results = vaultService.queryBy(criteria, paging = pagingSpec) assertThat(results.states).hasSize(5) // should retrieve states 90..94 assertThat(results.totalStatesAvailable).isEqualTo(95) } @@ -1050,7 +1049,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val pagingSpec = PageSpecification(0, 10) val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - vaultQuerySvc.queryBy(criteria, paging = pagingSpec) + vaultService.queryBy(criteria, paging = pagingSpec) } } @@ -1067,7 +1066,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Suppress("EXPECTED_CONDITION") val pagingSpec = PageSpecification(DEFAULT_PAGE_NUM, @Suppress("INTEGER_OVERFLOW")MAX_PAGE_SIZE + 1) // overflow = -2147483648 val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - vaultQuerySvc.queryBy(criteria, paging = pagingSpec) + vaultService.queryBy(criteria, paging = pagingSpec) } } @@ -1082,7 +1081,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - vaultQuerySvc.queryBy(criteria) + vaultService.queryBy(criteria) } } @@ -1098,7 +1097,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val sortCol2 = Sort.SortColumn(SortAttribute.Standard(Sort.VaultStateAttribute.STATE_STATUS), Sort.Direction.ASC) val sortCol3 = Sort.SortColumn(SortAttribute.Standard(Sort.VaultStateAttribute.CONSUMED_TIME), Sort.Direction.DESC) val sorting = Sort(setOf(sortCol1, sortCol2, sortCol3)) - val result = vaultQuerySvc.queryBy(VaultQueryCriteria(status = Vault.StateStatus.ALL), sorting = sorting) + val result = vaultService.queryBy(VaultQueryCriteria(status = Vault.StateStatus.ALL), sorting = sorting) val states = result.states val metadata = result.statesMetadata @@ -1123,7 +1122,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { services.fillWithSomeTestLinearStates(10) } database.transaction { - val results = vaultQuerySvc.queryBy>() + val results = vaultService.queryBy>() assertThat(results.states).hasSize(4) } } @@ -1142,7 +1141,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) - val results = vaultQuerySvc.queryBy>(criteria) + val results = vaultService.queryBy>(criteria) assertThat(results.states).hasSize(2) } } @@ -1154,7 +1153,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { services.fillWithSomeTestLinearStates(10) } database.transaction { - val results = vaultQuerySvc.queryBy() + val results = vaultService.queryBy() assertThat(results.states).hasSize(3) } } @@ -1169,7 +1168,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } // should now have x2 CONSUMED + x2 UNCONSUMED (one spent + one change) database.transaction { - val results = vaultQuerySvc.queryBy(FungibleAssetQueryCriteria()) + val results = vaultService.queryBy(FungibleAssetQueryCriteria()) assertThat(results.statesMetadata).hasSize(2) assertThat(results.states).hasSize(2) } @@ -1192,7 +1191,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(2) } } @@ -1205,7 +1204,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { services.fillWithSomeTestDeals(listOf("123", "456", "789")) } database.transaction { - val results = vaultQuerySvc.queryBy() + val results = vaultService.queryBy() assertThat(results.states).hasSize(13) } } @@ -1224,7 +1223,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(3) } } @@ -1241,7 +1240,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { // DOCSTART VaultQueryExample8 val linearIds = issuedStates.states.map { it.state.data.linearId }.toList() val criteria = LinearStateQueryCriteria(linearId = listOf(linearIds.first(), linearIds.last())) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) // DOCEND VaultQueryExample8 assertThat(results.states).hasSize(2) } @@ -1259,7 +1258,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val linearIds = listOf(linearState1.states.first().state.data.linearId, linearState3.states.first().state.data.linearId) val criteria = LinearStateQueryCriteria(linearId = linearIds) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(2) } } @@ -1276,7 +1275,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val externalIds = listOf(linearState1.states.first().state.data.linearId.externalId!!, linearState3.states.first().state.data.linearId.externalId!!) val criteria = LinearStateQueryCriteria(externalId = externalIds) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(2) } } @@ -1297,7 +1296,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { // DOCSTART VaultQueryExample9 val linearStateCriteria = LinearStateQueryCriteria(linearId = listOf(linearId), status = Vault.StateStatus.ALL) val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val results = vaultQuerySvc.queryBy(linearStateCriteria and vaultCriteria) + val results = vaultService.queryBy(linearStateCriteria and vaultCriteria) // DOCEND VaultQueryExample9 assertThat(results.states).hasSize(4) } @@ -1320,7 +1319,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC))) - val results = vaultQuerySvc.queryBy(linearStateCriteria.and(vaultCriteria), sorting = sorting) + val results = vaultService.queryBy(linearStateCriteria.and(vaultCriteria), sorting = sorting) results.states.forEach { println("${it.state.data.linearId.id}") } assertThat(results.states).hasSize(8) } @@ -1337,7 +1336,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val vaultCriteria = VaultQueryCriteria() val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.EXTERNAL_ID), Sort.Direction.DESC))) - val results = vaultQuerySvc.queryBy((vaultCriteria), sorting = sorting) + val results = vaultService.queryBy((vaultCriteria), sorting = sorting) results.states.forEach { println(it.state.data.linearString) } assertThat(results.states).hasSize(6) } @@ -1358,7 +1357,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.EXTERNAL_ID), Sort.Direction.DESC))) - val results = vaultQuerySvc.queryBy(compositeCriteria, sorting = sorting) + val results = vaultService.queryBy(compositeCriteria, sorting = sorting) assertThat(results.statesMetadata).hasSize(4) assertThat(results.states).hasSize(4) } @@ -1375,7 +1374,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val vaultCriteria = VaultQueryCriteria() val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Custom(DummyLinearStateSchemaV1.PersistentDummyLinearState::class.java, "linearString"), Sort.Direction.DESC))) - val results = vaultQuerySvc.queryBy((vaultCriteria), sorting = sorting) + val results = vaultService.queryBy((vaultCriteria), sorting = sorting) results.states.forEach { println(it.state.data.linearString) } assertThat(results.states).hasSize(6) } @@ -1397,7 +1396,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val linearStateCriteria = LinearStateQueryCriteria(linearId = txns.states.map { it.state.data.linearId }, status = Vault.StateStatus.CONSUMED) val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC))) - val results = vaultQuerySvc.queryBy(linearStateCriteria.and(vaultCriteria), sorting = sorting) + val results = vaultService.queryBy(linearStateCriteria.and(vaultCriteria), sorting = sorting) assertThat(results.states).hasSize(3) } } @@ -1411,7 +1410,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { services.fillWithSomeTestDeals(listOf("123", "456", "789")) } database.transaction { - val results = vaultQuerySvc.queryBy() + val results = vaultService.queryBy() assertThat(results.states).hasSize(3) } } @@ -1424,7 +1423,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { // DOCSTART VaultQueryExample10 val criteria = LinearStateQueryCriteria(externalId = listOf("456", "789")) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) // DOCEND VaultQueryExample10 assertThat(results.states).hasSize(2) @@ -1439,11 +1438,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { services.fillWithSomeTestDeals(listOf("123", "789")) } database.transaction { - val all = vaultQuerySvc.queryBy() + val all = vaultService.queryBy() all.states.forEach { println(it.state) } val criteria = LinearStateQueryCriteria(externalId = listOf("456")) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) assertThat(results.states).hasSize(1) } } @@ -1459,7 +1458,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { // DOCSTART VaultQueryExample11 val criteria = LinearStateQueryCriteria(participants = parties) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) // DOCEND VaultQueryExample11 assertThat(results.states).hasSize(1) @@ -1481,7 +1480,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val criteria = FungibleAssetQueryCriteria(issuer = listOf(BOC), issuerRef = listOf(BOC.ref(1).reference, BOC.ref(2).reference)) - val results = vaultQuerySvc.queryBy>(criteria) + val results = vaultService.queryBy>(criteria) assertThat(results.states).hasSize(2) } } @@ -1509,7 +1508,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val criteria = FungibleAssetQueryCriteria(issuer = listOf(gbpCashIssuer.party, usdCashIssuer.party)) - val results = vaultQuerySvc.queryBy>(criteria) + val results = vaultService.queryBy>(criteria) assertThat(results.states).hasSize(2) } } @@ -1523,7 +1522,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val criteria = FungibleAssetQueryCriteria(owner = listOf(MEGA_CORP)) - val results = vaultQuerySvc.queryBy>(criteria) + val results = vaultService.queryBy>(criteria) assertThat(results.states).hasSize(1) // can only be 1 owner of a node (MEGA_CORP in this MockServices setup) } } @@ -1540,7 +1539,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { // DOCSTART VaultQueryExample5.2 val criteria = FungibleAssetQueryCriteria(owner = listOf(MEGA_CORP, BOC)) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultService.queryBy(criteria) // DOCEND VaultQueryExample5.2 assertThat(results.states).hasSize(2) // can only be 1 owner of a node (MEGA_CORP in this MockServices setup) @@ -1560,7 +1559,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { // DOCSTART VaultQueryExample12 val ccyIndex = builder { CashSchemaV1.PersistentCashState::currency.equal(USD.currencyCode) } val criteria = VaultCustomQueryCriteria(ccyIndex) - val results = vaultQuerySvc.queryBy>(criteria) + val results = vaultService.queryBy>(criteria) // DOCEND VaultQueryExample12 assertThat(results.states).hasSize(3) @@ -1580,7 +1579,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val ccyIndex = builder { CashSchemaV1.PersistentCashState::currency.equal(USD.currencyCode) } val ccyCriteria = VaultCustomQueryCriteria(ccyIndex) - val results = vaultQuerySvc.queryBy>(sumCriteria.and(ccyCriteria)) + val results = vaultService.queryBy>(sumCriteria.and(ccyCriteria)) assertThat(results.otherResults).hasSize(2) assertThat(results.otherResults[0]).isEqualTo(30000L) @@ -1601,7 +1600,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { val ccyIndex = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } val criteria = VaultCustomQueryCriteria(ccyIndex) - val results = vaultQuerySvc.queryBy>(criteria) + val results = vaultService.queryBy>(criteria) assertThat(results.otherResults).hasSize(6) assertThat(results.otherResults[0]).isEqualTo(110000L) @@ -1624,7 +1623,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { // DOCSTART VaultQueryExample13 val fungibleAssetCriteria = FungibleAssetQueryCriteria(quantity = builder { greaterThan(2500L) }) - val results = vaultQuerySvc.queryBy(fungibleAssetCriteria) + val results = vaultService.queryBy(fungibleAssetCriteria) // DOCEND VaultQueryExample13 assertThat(results.states).hasSize(4) // POUNDS, SWISS_FRANCS @@ -1642,7 +1641,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { // DOCSTART VaultQueryExample14 val criteria = FungibleAssetQueryCriteria(issuer = listOf(BOC)) - val results = vaultQuerySvc.queryBy>(criteria) + val results = vaultService.queryBy>(criteria) // DOCEND VaultQueryExample14 assertThat(results.states).hasSize(1) @@ -1661,7 +1660,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val ccyIndex = builder { CashSchemaV1.PersistentCashState::currency.equal(GBP.currencyCode) } val customCriteria = VaultCustomQueryCriteria(ccyIndex) val fungibleAssetCriteria = FungibleAssetQueryCriteria(quantity = builder { greaterThan(5000L) }) - val results = vaultQuerySvc.queryBy(fungibleAssetCriteria.and(customCriteria)) + val results = vaultService.queryBy(fungibleAssetCriteria.and(customCriteria)) assertThat(results.states).hasSize(1) // POUNDS > 50 } @@ -1700,7 +1699,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val ccyIndex = builder { CommercialPaperSchemaV1.PersistentCommercialPaperState::currency.equal(USD.currencyCode) } val criteria1 = QueryCriteria.VaultCustomQueryCriteria(ccyIndex) - val result = vaultQuerySvc.queryBy(criteria1) + val result = vaultService.queryBy(criteria1) Assertions.assertThat(result.states).hasSize(1) Assertions.assertThat(result.statesMetadata).hasSize(1) @@ -1746,7 +1745,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val criteria2 = QueryCriteria.VaultCustomQueryCriteria(maturityIndex) val criteria3 = QueryCriteria.VaultCustomQueryCriteria(faceValueIndex) - vaultQuerySvc.queryBy(criteria1.and(criteria3).and(criteria2)) + vaultService.queryBy(criteria1.and(criteria3).and(criteria2)) } Assertions.assertThat(result.states).hasSize(1) Assertions.assertThat(result.statesMetadata).hasSize(1) @@ -1766,7 +1765,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val criteria = VaultCustomQueryCriteria(logicalExpression) assertThatThrownBy { - vaultQuerySvc.queryBy(criteria) + vaultService.queryBy(criteria) }.isInstanceOf(VaultQueryException::class.java).hasMessageContaining("Please register the entity") } } @@ -1794,7 +1793,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val customCriteria2 = VaultCustomQueryCriteria(quantityIndex) val criteria = generalCriteria.and(customCriteria1.and(customCriteria2)) - vaultQuerySvc.queryBy(criteria) + vaultService.queryBy(criteria) } // DOCEND VaultQueryExample20 @@ -1819,7 +1818,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val recordedBetweenExpression = TimeCondition(TimeInstantType.RECORDED, builder { between(start, end) }) val basicCriteria = VaultQueryCriteria(timeCondition = recordedBetweenExpression) - val results = vaultQuerySvc.queryBy(basicCriteria) + val results = vaultService.queryBy(basicCriteria) assertThat(results.states).hasSize(1) } @@ -1837,7 +1836,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val externalIdCondition = builder { VaultSchemaV1.VaultLinearStates::externalId.equal("TEST2") } val externalIdCustomCriteria = VaultCustomQueryCriteria(externalIdCondition) - val results = vaultQuerySvc.queryBy(externalIdCustomCriteria) + val results = vaultService.queryBy(externalIdCustomCriteria) assertThat(results.states).hasSize(1) } @@ -1866,7 +1865,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val basicCriteria = VaultQueryCriteria(timeCondition = recordedBetweenExpression) val criteria = basicCriteria.and(customCriteria) - vaultQuerySvc.queryBy(criteria) + vaultService.queryBy(criteria) } assertThat(results.states).hasSize(1) @@ -1894,7 +1893,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val uuidCustomCriteria = VaultCustomQueryCriteria(uuidCondition) val criteria = externalIdCustomCriteria or uuidCustomCriteria - vaultQuerySvc.queryBy(criteria) + vaultService.queryBy(criteria) } assertThat(results.statesMetadata).hasSize(2) assertThat(results.states).hasSize(2) @@ -1911,7 +1910,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val linearStateCriteria = LinearStateQueryCriteria(participants = listOf(ALICE)) - val results = vaultQuerySvc.queryBy(linearStateCriteria) + val results = vaultService.queryBy(linearStateCriteria) assertThat(results.states).hasSize(1) assertThat(results.states[0].state.data.linearId.externalId).isEqualTo("TEST1") @@ -1931,7 +1930,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { val linearStateCriteria = LinearStateQueryCriteria(participants = listOf(ALICE, BOB, CHARLIE)) - val results = vaultQuerySvc.queryBy(linearStateCriteria) + val results = vaultService.queryBy(linearStateCriteria) assertThat(results.states).hasSize(1) assertThat(results.states[0].state.data.linearId.externalId).isEqualTo("TEST1") @@ -1952,7 +1951,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val externalIdCondition = VaultSchemaV1.VaultLinearStates::externalId.isNull() val externalIdCustomCriteria = VaultCustomQueryCriteria(externalIdCondition) - vaultQuerySvc.queryBy(externalIdCustomCriteria) + vaultService.queryBy(externalIdCustomCriteria) } assertThat(results.states).hasSize(1) } @@ -1972,7 +1971,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val externalIdCondition = VaultSchemaV1.VaultLinearStates::externalId.notNull() val externalIdCustomCriteria = VaultCustomQueryCriteria(externalIdCondition) - vaultQuerySvc.queryBy(externalIdCustomCriteria) + vaultService.queryBy(externalIdCustomCriteria) } assertThat(results.states).hasSize(2) } @@ -2001,7 +2000,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val sorter = Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC))) // Execute query - val results = services.vaultQueryService.queryBy>(baseCriteria and enrichedCriteria, sorter).states + val results = services.vaultService.queryBy>(baseCriteria and enrichedCriteria, sorter).states assertThat(results).hasSize(4) } } @@ -2015,7 +2014,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val updates = database.transaction { // DOCSTART VaultQueryExample15 - vaultQuerySvc.trackBy().updates // UNCONSUMED default + vaultService.trackBy().updates // UNCONSUMED default // DOCEND VaultQueryExample15 } val (linearStates, dealStates) = @@ -2057,7 +2056,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val updates = database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) - vaultQuerySvc.trackBy(criteria).updates + vaultService.trackBy(criteria).updates } val (linearStates, dealStates) = database.transaction { @@ -2103,7 +2102,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val updates = database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - vaultQuerySvc.trackBy(criteria).updates + vaultService.trackBy(criteria).updates } val (linearStates, dealStates) = database.transaction { @@ -2158,7 +2157,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val updates = database.transaction { // DOCSTART VaultQueryExample16 - val (snapshot, updates) = vaultQuerySvc.trackBy() + val (snapshot, updates) = vaultService.trackBy() // DOCEND VaultQueryExample16 assertThat(snapshot.states).hasSize(0) updates @@ -2207,7 +2206,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val updates = database.transaction { // DOCSTART VaultQueryExample17 - val (snapshot, updates) = vaultQuerySvc.trackBy() + val (snapshot, updates) = vaultService.trackBy() // DOCEND VaultQueryExample17 assertThat(snapshot.states).hasSize(0) updates diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index 9c969f0950..a1450132a5 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -1,11 +1,10 @@ -package net.corda.node.services.vault +package net.corda.node.services.vaultService import net.corda.core.contracts.ContractState import net.corda.core.contracts.LinearState import net.corda.core.contracts.UniqueIdentifier import net.corda.core.identity.AnonymousParty import net.corda.core.node.services.Vault -import net.corda.core.node.services.VaultQueryService import net.corda.core.node.services.VaultService import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.QueryCriteria @@ -37,8 +36,7 @@ import kotlin.test.assertEquals class VaultWithCashTest : TestDependencyInjectionBase() { lateinit var services: MockServices lateinit var issuerServices: MockServices - val vault: VaultService get() = services.vaultService - val vaultQuery: VaultQueryService get() = services.vaultQueryService + val vaultService: VaultService get() = services.vaultService lateinit var database: CordaPersistence lateinit var notaryServices: MockServices @@ -69,7 +67,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 3, 3, Random(0L), issuedBy = DUMMY_CASH_ISSUER) } database.transaction { - val w = vaultQuery.queryBy().states + val w = vaultService.queryBy().states assertEquals(3, w.size) val state = w[0].state.data @@ -141,8 +139,8 @@ class VaultWithCashTest : TestDependencyInjectionBase() { println("Cash balance: ${services.getCashBalance(USD)}") } database.transaction { - assertThat(vaultQuery.queryBy().states).hasSize(10) - assertThat(vaultQuery.queryBy(criteriaLocked).states).hasSize(0) + assertThat(vaultService.queryBy().states).hasSize(10) + assertThat(vaultService.queryBy(criteriaLocked).states).hasSize(0) } val backgroundExecutor = Executors.newFixedThreadPool(2) @@ -157,9 +155,9 @@ class VaultWithCashTest : TestDependencyInjectionBase() { val ptxn1 = notaryServices.signInitialTransaction(txn1Builder) val txn1 = services.addSignature(ptxn1, freshKey) println("txn1: ${txn1.id} spent ${((txn1.tx.outputs[0].data) as Cash.State).amount}") - val unconsumedStates1 = vaultQuery.queryBy() - val consumedStates1 = vaultQuery.queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)) - val lockedStates1 = vaultQuery.queryBy(criteriaLocked).states + val unconsumedStates1 = vaultService.queryBy() + val consumedStates1 = vaultService.queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)) + val lockedStates1 = vaultService.queryBy(criteriaLocked).states println("""txn1 states: UNCONSUMED: ${unconsumedStates1.totalStatesAvailable} : $unconsumedStates1, CONSUMED: ${consumedStates1.totalStatesAvailable} : $consumedStates1, @@ -167,9 +165,9 @@ class VaultWithCashTest : TestDependencyInjectionBase() { """) services.recordTransactions(txn1) println("txn1: Cash balance: ${services.getCashBalance(USD)}") - val unconsumedStates2 = vaultQuery.queryBy() - val consumedStates2 = vaultQuery.queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)) - val lockedStates2 = vaultQuery.queryBy(criteriaLocked).states + val unconsumedStates2 = vaultService.queryBy() + val consumedStates2 = vaultService.queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)) + val lockedStates2 = vaultService.queryBy(criteriaLocked).states println("""txn1 states: UNCONSUMED: ${unconsumedStates2.totalStatesAvailable} : $unconsumedStates2, CONSUMED: ${consumedStates2.totalStatesAvailable} : $consumedStates2, @@ -193,9 +191,9 @@ class VaultWithCashTest : TestDependencyInjectionBase() { val ptxn2 = notaryServices.signInitialTransaction(txn2Builder) val txn2 = services.addSignature(ptxn2, freshKey) println("txn2: ${txn2.id} spent ${((txn2.tx.outputs[0].data) as Cash.State).amount}") - val unconsumedStates1 = vaultQuery.queryBy() - val consumedStates1 = vaultQuery.queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)) - val lockedStates1 = vaultQuery.queryBy(criteriaLocked).states + val unconsumedStates1 = vaultService.queryBy() + val consumedStates1 = vaultService.queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)) + val lockedStates1 = vaultService.queryBy(criteriaLocked).states println("""txn2 states: UNCONSUMED: ${unconsumedStates1.totalStatesAvailable} : $unconsumedStates1, CONSUMED: ${consumedStates1.totalStatesAvailable} : $consumedStates1, @@ -203,9 +201,9 @@ class VaultWithCashTest : TestDependencyInjectionBase() { """) services.recordTransactions(txn2) println("txn2: Cash balance: ${services.getCashBalance(USD)}") - val unconsumedStates2 = vaultQuery.queryBy() - val consumedStates2 = vaultQuery.queryBy() - val lockedStates2 = vaultQuery.queryBy(criteriaLocked).states + val unconsumedStates2 = vaultService.queryBy() + val consumedStates2 = vaultService.queryBy() + val lockedStates2 = vaultService.queryBy(criteriaLocked).states println("""txn2 states: UNCONSUMED: ${unconsumedStates2.totalStatesAvailable} : $unconsumedStates2, CONSUMED: ${consumedStates2.totalStatesAvailable} : $consumedStates2, @@ -269,7 +267,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { dummyIssue } database.transaction { - assertThat(vaultQuery.queryBy().states).hasSize(1) + assertThat(vaultService.queryBy().states).hasSize(1) // Move the same state val dummyMoveBuilder = TransactionBuilder(notary = DUMMY_NOTARY) @@ -284,7 +282,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { services.recordTransactions(dummyMove) } database.transaction { - assertThat(vaultQuery.queryBy().states).hasSize(1) + assertThat(vaultService.queryBy().states).hasSize(1) } } @@ -298,14 +296,14 @@ class VaultWithCashTest : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.POUNDS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L)) } database.transaction { - val cash = vaultQuery.queryBy().states + val cash = vaultService.queryBy().states cash.forEach { println(it.state.data.amount) } } database.transaction { services.fillWithSomeTestDeals(listOf("123", "456", "789")) } database.transaction { - val deals = vaultQuery.queryBy().states + val deals = vaultService.queryBy().states deals.forEach { println(it.state.data.linearId.externalId!!) } } @@ -318,10 +316,10 @@ class VaultWithCashTest : TestDependencyInjectionBase() { services.recordTransactions(spendTX) } database.transaction { - val consumedStates = vaultQuery.queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)).states + val consumedStates = vaultService.queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)).states assertEquals(3, consumedStates.count()) - val unconsumedStates = vaultQuery.queryBy().states + val unconsumedStates = vaultService.queryBy().states assertEquals(7, unconsumedStates.count()) } } @@ -336,13 +334,13 @@ class VaultWithCashTest : TestDependencyInjectionBase() { } val deals = database.transaction { - vaultQuery.queryBy().states + vaultService.queryBy().states } database.transaction { services.fillWithSomeTestLinearStates(3) } database.transaction { - val linearStates = vaultQuery.queryBy().states + val linearStates = vaultService.queryBy().states linearStates.forEach { println(it.state.data.linearId) } // Create a txn consuming different contract types @@ -359,10 +357,10 @@ class VaultWithCashTest : TestDependencyInjectionBase() { services.recordTransactions(dummyMove) } database.transaction { - val consumedStates = vaultQuery.queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)).states + val consumedStates = vaultService.queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)).states assertEquals(2, consumedStates.count()) - val unconsumedStates = vaultQuery.queryBy().states + val unconsumedStates = vaultService.queryBy().states assertEquals(6, unconsumedStates.count()) } } diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt index 687ccbe857..af7aae480e 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt @@ -97,7 +97,7 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten val swaps = node1.database.transaction { - node1.services.vaultQueryService.queryBy().states + node1.services.vaultService.queryBy().states } val theDealRef: StateAndRef = swaps.single() diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt index 56c7001682..eb1198a881 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt @@ -80,7 +80,7 @@ object SimmFlow { notary = serviceHub.networkMapCache.notaryIdentities.first() // TODO We should pass the notary as a parameter to the flow, not leave it to random choice. val criteria = LinearStateQueryCriteria(participants = listOf(otherParty)) - val trades = serviceHub.vaultQueryService.queryBy(criteria).states + val trades = serviceHub.vaultService.queryBy(criteria).states val portfolio = Portfolio(trades, valuationDate) otherPartySession = initiateFlow(otherParty) @@ -89,7 +89,7 @@ object SimmFlow { } else { updatePortfolio(portfolio, existing) } - val portfolioStateRef = serviceHub.vaultQueryService.queryBy(criteria).states.first() + val portfolioStateRef = serviceHub.vaultService.queryBy(criteria).states.first() val state = updateValuation(portfolioStateRef) logger.info("SimmFlow done") @@ -126,7 +126,7 @@ object SimmFlow { private fun updateValuation(stateRef: StateAndRef): RevisionedState { logger.info("Agreeing valuations") val state = stateRef.state.data - val portfolio = serviceHub.vaultQueryService.queryBy(VaultQueryCriteria(stateRefs = state.portfolio)).states.toPortfolio() + val portfolio = serviceHub.vaultService.queryBy(VaultQueryCriteria(stateRefs = state.portfolio)).states.toPortfolio() val valuer = serviceHub.identityService.wellKnownPartyFromAnonymous(state.valuer) require(valuer != null) { "Valuer party must be known to this node" } @@ -211,7 +211,7 @@ object SimmFlow { @Suspendable override fun call() { val criteria = LinearStateQueryCriteria(participants = listOf(replyToSession.counterparty)) - val trades = serviceHub.vaultQueryService.queryBy(criteria).states + val trades = serviceHub.vaultService.queryBy(criteria).states val portfolio = Portfolio(trades) logger.info("SimmFlow receiver started") offer = replyToSession.receive().unwrap { it } @@ -220,7 +220,7 @@ object SimmFlow { } else { updatePortfolio(portfolio) } - val portfolioStateRef = serviceHub.vaultQueryService.queryBy(criteria).states.first() + val portfolioStateRef = serviceHub.vaultService.queryBy(criteria).states.first() updateValuation(portfolioStateRef) } @@ -327,7 +327,7 @@ object SimmFlow { @Suspendable private fun updateValuation(stateRef: StateAndRef) { - val portfolio = serviceHub.vaultQueryService.queryBy(VaultQueryCriteria(stateRefs = stateRef.state.data.portfolio)).states.toPortfolio() + val portfolio = serviceHub.vaultService.queryBy(VaultQueryCriteria(stateRefs = stateRef.state.data.portfolio)).states.toPortfolio() val valuer = serviceHub.identityService.wellKnownPartyFromAnonymous(stateRef.state.data.valuer) ?: throw IllegalStateException("Unknown valuer party ${stateRef.state.data.valuer}") val valuation = agreeValuation(portfolio, offer.valuationDate, valuer) subFlow(object : StateRevisionFlow.Receiver(replyToSession) { diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmRevaluation.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmRevaluation.kt index 8f607163ef..599e9d4345 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmRevaluation.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmRevaluation.kt @@ -20,7 +20,7 @@ object SimmRevaluation { class Initiator(private val curStateRef: StateRef, private val valuationDate: LocalDate) : FlowLogic() { @Suspendable override fun call() { - val stateAndRef = serviceHub.vaultQueryService.queryBy(VaultQueryCriteria(stateRefs = listOf(curStateRef))).states.single() + val stateAndRef = serviceHub.vaultService.queryBy(VaultQueryCriteria(stateRefs = listOf(curStateRef))).states.single() val curState = stateAndRef.state.data if (ourIdentity == curState.participants[0]) { val otherParty = serviceHub.identityService.wellKnownPartyFromAnonymous(curState.participants[1]) diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt index 890885227f..2a08170192 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt @@ -40,7 +40,7 @@ class SellerFlow(private val otherParty: Party, progressTracker.currentStep = SELF_ISSUING val cpOwner = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false) - val commercialPaper = serviceHub.vaultQueryService.queryBy(CommercialPaper.State::class.java).states.first() + val commercialPaper = serviceHub.vaultService.queryBy(CommercialPaper.State::class.java).states.first() progressTracker.currentStep = TRADING diff --git a/testing/node-driver/src/main/kotlin/net/corda/node/testing/MockServiceHubInternal.kt b/testing/node-driver/src/main/kotlin/net/corda/node/testing/MockServiceHubInternal.kt index b29cd52654..e3e54de6d4 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/node/testing/MockServiceHubInternal.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/node/testing/MockServiceHubInternal.kt @@ -35,7 +35,6 @@ open class MockServiceHubInternal( override val database: CordaPersistence, override val configuration: NodeConfiguration, val customVault: VaultService? = null, - val customVaultQuery: VaultQueryService? = null, val keyManagement: KeyManagementService? = null, val network: MessagingService? = null, val identity: IdentityService? = MOCK_IDENTITY_SERVICE, @@ -50,8 +49,6 @@ open class MockServiceHubInternal( val customTransactionVerifierService: TransactionVerifierService? = InMemoryTransactionVerifierService(2), override val cordappProvider: CordappProvider = CordappProviderImpl(CordappLoader.createDefault(Paths.get("."))).start(attachments) ) : ServiceHubInternal { - override val vaultQueryService: VaultQueryService - get() = customVaultQuery ?: throw UnsupportedOperationException() override val transactionVerifierService: TransactionVerifierService get() = customTransactionVerifierService ?: throw UnsupportedOperationException() override val vaultService: VaultService 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 73dc1f883d..7338a6c68f 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 @@ -24,7 +24,6 @@ import net.corda.node.services.persistence.InMemoryStateMachineRecordedTransacti import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.transactions.InMemoryTransactionVerifierService -import net.corda.node.services.vault.HibernateVaultQueryImpl import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase @@ -117,8 +116,6 @@ open class MockServices(cordappPackages: List = emptyList(), vararg val (vaultService as NodeVaultService).notifyAll(txs.map { it.tx }) } - override val vaultQueryService: VaultQueryService = HibernateVaultQueryImpl(database.hibernateConfig, vaultService) - override fun jdbcSession(): Connection = database.createSession() } } @@ -149,7 +146,6 @@ open class MockServices(cordappPackages: List = emptyList(), vararg val override val vaultService: VaultService get() = throw UnsupportedOperationException() override val contractUpgradeService: ContractUpgradeService get() = throw UnsupportedOperationException() - override val vaultQueryService: VaultQueryService get() = throw UnsupportedOperationException() override val networkMapCache: NetworkMapCache get() = throw UnsupportedOperationException() override val clock: Clock get() = Clock.systemUTC() override val myInfo: NodeInfo get() { @@ -163,7 +159,7 @@ open class MockServices(cordappPackages: List = emptyList(), vararg val lateinit var hibernatePersister: HibernateObserver fun makeVaultService(hibernateConfig: HibernateConfiguration = HibernateConfiguration( { NodeSchemaService() }, makeTestDatabaseProperties(), { identityService })): VaultService { - val vaultService = NodeVaultService(this) + val vaultService = NodeVaultService(this, hibernateConfig) hibernatePersister = HibernateObserver(vaultService.rawUpdates, hibernateConfig) return vaultService } From 7a55855afbaf91c2548dd32b50a1279b20ca7aea Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Tue, 26 Sep 2017 19:25:29 +0100 Subject: [PATCH 020/185] Fix validating notary flow to handle notary change transactions properly. Add a notary change test for checking longer chains involving both regular and notary change transactions. --- .../net/corda/docs/CustomNotaryTutorial.kt | 22 ++-- .../transactions/ValidatingNotaryFlow.kt | 26 +++-- .../corda/node/services/NotaryChangeTests.kt | 102 ++++++++++++------ 3 files changed, 101 insertions(+), 49 deletions(-) diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt index 3f56cb0a56..bf1e504bf3 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt @@ -1,13 +1,14 @@ package net.corda.docs import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.TransactionVerificationException import net.corda.core.flows.* import net.corda.core.node.ServiceHub import net.corda.core.node.services.CordaService import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.TrustedAuthorityNotaryService -import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionWithSignatures import net.corda.node.services.transactions.PersistentUniquenessProvider import java.security.PublicKey import java.security.SignatureException @@ -35,10 +36,17 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating override fun receiveAndVerifyTx(): TransactionParts { try { val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false)) - checkNotary(stx.notary) - checkSignatures(stx) - val wtx = stx.tx - return TransactionParts(wtx.id, wtx.inputs, wtx.timeWindow, wtx.notary) + val notary = stx.notary + checkNotary(notary) + var timeWindow: TimeWindow? = null + val transactionWithSignatures = if (stx.isNotaryChangeTransaction()) { + stx.resolveNotaryChangeTransaction(serviceHub) + } else { + timeWindow = stx.tx.timeWindow + stx + } + checkSignatures(transactionWithSignatures) + return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!) } catch (e: Exception) { throw when (e) { is TransactionVerificationException, @@ -48,9 +56,9 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating } } - private fun checkSignatures(stx: SignedTransaction) { + private fun checkSignatures(tx: TransactionWithSignatures) { try { - stx.verifySignaturesExcept(service.notaryIdentityKey) + tx.verifySignaturesExcept(service.notaryIdentityKey) } catch (e: SignatureException) { throw NotaryException(NotaryError.TransactionInvalid(e)) } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt index cf871f7f0f..2cee47aa89 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt @@ -1,10 +1,11 @@ package net.corda.node.services.transactions import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.TransactionVerificationException import net.corda.core.flows.* import net.corda.core.node.services.TrustedAuthorityNotaryService -import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionWithSignatures import java.security.SignatureException /** @@ -15,9 +16,8 @@ import java.security.SignatureException */ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthorityNotaryService) : NotaryFlow.Service(otherSideSession, service) { /** - * The received transaction is checked for contract-validity, which requires fully resolving it into a - * [TransactionForVerification], for which the caller also has to to reveal the whole transaction - * dependency chain. + * Fully resolves the received transaction and its dependencies, runs contract verification logic and checks that + * the transaction in question has all required signatures apart from the notary's. */ @Suspendable override fun receiveAndVerifyTx(): TransactionParts { @@ -25,9 +25,15 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false)) val notary = stx.notary checkNotary(notary) - checkSignatures(stx) - val wtx = stx.tx - return TransactionParts(wtx.id, wtx.inputs, wtx.timeWindow, notary!!) + var timeWindow: TimeWindow? = null + val transactionWithSignatures = if (stx.isNotaryChangeTransaction()) { + stx.resolveNotaryChangeTransaction(serviceHub) + } else { + timeWindow = stx.tx.timeWindow + stx + } + checkSignatures(transactionWithSignatures) + return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!) } catch (e: Exception) { throw when (e) { is TransactionVerificationException, @@ -37,10 +43,10 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor } } - private fun checkSignatures(stx: SignedTransaction) { + private fun checkSignatures(tx: TransactionWithSignatures) { try { - stx.verifySignaturesExcept(service.notaryIdentityKey) - } catch(e: SignatureException) { + tx.verifySignaturesExcept(service.notaryIdentityKey) + } catch (e: SignatureException) { throw NotaryException(NotaryError.TransactionInvalid(e)) } } diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index ea0106a076..024445b343 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -3,6 +3,7 @@ package net.corda.node.services import net.corda.core.contracts.* import net.corda.core.crypto.generateKeyPair import net.corda.core.flows.NotaryChangeFlow +import net.corda.core.flows.NotaryFlow import net.corda.core.flows.StateReplacementException import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party @@ -12,7 +13,7 @@ import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds import net.corda.node.internal.StartedNode import net.corda.node.services.network.NetworkMapService -import net.corda.node.services.transactions.SimpleNotaryService +import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.contracts.DummyContract @@ -32,8 +33,8 @@ class NotaryChangeTests { lateinit var newNotaryNode: StartedNode lateinit var clientNodeA: StartedNode lateinit var clientNodeB: StartedNode - lateinit var notaryNewId: Party - lateinit var notaryOldId: Party + lateinit var newNotaryParty: Party + lateinit var oldNotaryParty: Party @Before fun setUp() { @@ -41,15 +42,15 @@ class NotaryChangeTests { mockNet = MockNetwork() oldNotaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, - advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type))) + advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type))) clientNodeA = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress) clientNodeB = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress) - newNotaryNode = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress, advertisedServices = ServiceInfo(SimpleNotaryService.type)) + newNotaryNode = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress, advertisedServices = ServiceInfo(ValidatingNotaryService.type)) mockNet.registerIdentities() mockNet.runNetwork() // Clear network map registration messages oldNotaryNode.internals.ensureRegistered() - notaryNewId = newNotaryNode.info.legalIdentities[1] - notaryOldId = oldNotaryNode.info.legalIdentities[1] + newNotaryParty = newNotaryNode.info.legalIdentities[1] + oldNotaryParty = oldNotaryNode.info.legalIdentities[1] } @After @@ -60,21 +61,16 @@ class NotaryChangeTests { @Test fun `should change notary for a state with single participant`() { - val state = issueState(clientNodeA, oldNotaryNode, notaryOldId) - val newNotary = notaryNewId - val flow = NotaryChangeFlow(state, newNotary) - val future = clientNodeA.services.startFlow(flow) - - mockNet.runNetwork() - - val newState = future.resultFuture.getOrThrow() - assertEquals(newState.state.notary, newNotary) + val state = issueState(clientNodeA, oldNotaryParty) + assertEquals(state.state.notary, oldNotaryParty) + val newState = changeNotary(state, clientNodeA, newNotaryParty) + assertEquals(newState.state.notary, newNotaryParty) } @Test fun `should change notary for a state with multiple participants`() { - val state = issueMultiPartyState(clientNodeA, clientNodeB, oldNotaryNode, notaryOldId) - val newNotary = notaryNewId + val state = issueMultiPartyState(clientNodeA, clientNodeB, oldNotaryNode, oldNotaryParty) + val newNotary = newNotaryParty val flow = NotaryChangeFlow(state, newNotary) val future = clientNodeA.services.startFlow(flow) @@ -89,7 +85,7 @@ class NotaryChangeTests { @Test fun `should throw when a participant refuses to change Notary`() { - val state = issueMultiPartyState(clientNodeA, clientNodeB, oldNotaryNode, notaryOldId) + val state = issueMultiPartyState(clientNodeA, clientNodeB, oldNotaryNode, oldNotaryParty) val newEvilNotary = getTestPartyAndCertificate(CordaX500Name(organisation = "Evil R3", locality = "London", country = "GB"), generateKeyPair().public) val flow = NotaryChangeFlow(state, newEvilNotary.party) val future = clientNodeA.services.startFlow(flow) @@ -103,10 +99,10 @@ class NotaryChangeTests { @Test fun `should not break encumbrance links`() { - val issueTx = issueEncumberedState(clientNodeA, notaryOldId) + val issueTx = issueEncumberedState(clientNodeA, oldNotaryParty) val state = StateAndRef(issueTx.outputs.first(), StateRef(issueTx.id, 0)) - val newNotary = notaryNewId + val newNotary = newNotaryParty val flow = NotaryChangeFlow(state, newNotary) val future = clientNodeA.services.startFlow(flow) mockNet.runNetwork() @@ -135,6 +131,47 @@ class NotaryChangeTests { } } + @Test + fun `notary change and regular transactions are properly handled during resolution in longer chains`() { + val issued = issueState(clientNodeA, oldNotaryParty) + val moved = moveState(issued, clientNodeA, clientNodeB) + + // We don't to tx resolution when moving state to another node, so need to add the issue transaction manually + // to node B. The resolution process is tested later during notarisation. + clientNodeB.services.recordTransactions(clientNodeA.services.validatedTransactions.getTransaction(issued.ref.txhash)!!) + + val changedNotary = changeNotary(moved, clientNodeB, newNotaryParty) + val movedBack = moveState(changedNotary, clientNodeB, clientNodeA) + val changedNotaryBack = changeNotary(movedBack, clientNodeA, oldNotaryParty) + + assertEquals(issued.state, changedNotaryBack.state) + } + + private fun changeNotary(movedState: StateAndRef, node: StartedNode<*>, newNotary: Party): StateAndRef { + val flow = NotaryChangeFlow(movedState, newNotary) + val future = node.services.startFlow(flow) + mockNet.runNetwork() + + return future.resultFuture.getOrThrow() + } + + private fun moveState(state: StateAndRef, fromNode: StartedNode<*>, toNode: StartedNode<*>): StateAndRef { + val tx = DummyContract.move(state, toNode.info.chooseIdentity()) + val stx = fromNode.services.signInitialTransaction(tx) + + val notaryFlow = NotaryFlow.Client(stx) + val future = fromNode.services.startFlow(notaryFlow) + mockNet.runNetwork() + + val notarySignature = future.resultFuture.getOrThrow() + val finalTransaction = stx + notarySignature + + fromNode.services.recordTransactions(finalTransaction) + toNode.services.recordTransactions(finalTransaction) + + return finalTransaction.tx.outRef(0) + } + private fun issueEncumberedState(node: StartedNode<*>, notaryIdentity: Party): WireTransaction { val owner = node.info.chooseIdentity().ref(0) val stateA = DummyContract.SingleOwnerState(Random().nextInt(), owner.party) @@ -161,30 +198,31 @@ class NotaryChangeTests { // - The transaction type is not a notary change transaction at all. } -fun issueState(node: StartedNode<*>, notaryNode: StartedNode<*>, notaryIdentity: Party): StateAndRef<*> { +fun issueState(node: StartedNode<*>, notaryIdentity: Party): StateAndRef { val tx = DummyContract.generateInitial(Random().nextInt(), notaryIdentity, node.info.chooseIdentity().ref(0)) - val signedByNode = node.services.signInitialTransaction(tx) - val stx = notaryNode.services.addSignature(signedByNode, notaryIdentity.owningKey) + val stx = node.services.signInitialTransaction(tx) node.services.recordTransactions(stx) - return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) + return stx.tx.outRef(0) } fun issueMultiPartyState(nodeA: StartedNode<*>, nodeB: StartedNode<*>, notaryNode: StartedNode<*>, notaryIdentity: Party): StateAndRef { - val state = TransactionState(DummyContract.MultiOwnerState(0, - listOf(nodeA.info.chooseIdentity(), nodeB.info.chooseIdentity())), DummyContract.PROGRAM_ID, notaryIdentity) - val tx = TransactionBuilder(notary = notaryIdentity).withItems(state, dummyCommand()) + val participants = listOf(nodeA.info.chooseIdentity(), nodeB.info.chooseIdentity()) + val state = TransactionState( + DummyContract.MultiOwnerState(0, participants), + DummyContract.PROGRAM_ID, notaryIdentity) + val tx = TransactionBuilder(notary = notaryIdentity).withItems(state, dummyCommand(participants.first().owningKey)) val signedByA = nodeA.services.signInitialTransaction(tx) val signedByAB = nodeB.services.addSignature(signedByA) val stx = notaryNode.services.addSignature(signedByAB, notaryIdentity.owningKey) nodeA.services.recordTransactions(stx) nodeB.services.recordTransactions(stx) - return StateAndRef(state, StateRef(stx.id, 0)) + return stx.tx.outRef(0) } -fun issueInvalidState(node: StartedNode<*>, notary: Party): StateAndRef<*> { +fun issueInvalidState(node: StartedNode<*>, notary: Party): StateAndRef { val tx = DummyContract.generateInitial(Random().nextInt(), notary, node.info.chooseIdentity().ref(0)) tx.setTimeWindow(Instant.now(), 30.seconds) val stx = node.services.signInitialTransaction(tx) node.services.recordTransactions(stx) - return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) -} + return stx.tx.outRef(0) +} \ No newline at end of file From 0ce0baa235e868604148f40065ae1fcf6e27329b Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Wed, 27 Sep 2017 14:12:44 +0100 Subject: [PATCH 021/185] Removed initialiseSerialization param from CordaRPCClient, mirroring change in 1.0 branch --- .../net/corda/client/jfx/NodeMonitorModelTest.kt | 6 +++--- .../net/corda/client/jfx/model/NodeMonitorModel.kt | 7 +++---- .../corda/client/rpc/CordaRPCJavaClientTest.java | 13 +++++++------ .../net/corda/client/rpc/CordaRPCClientTest.kt | 4 ++-- .../kotlin/net/corda/client/rpc/CordaRPCClient.kt | 13 ++++--------- .../rpc/internal/KryoClientSerializationScheme.kt | 13 ++++++++++--- .../net/corda/services/messaging/MQSecurityTest.kt | 2 +- .../kotlin/net/corda/irs/IRSDemoTest.kt | 2 +- .../kotlin/net/corda/traderdemo/TraderDemoTest.kt | 8 +++----- .../main/kotlin/net/corda/testing/driver/Driver.kt | 4 ++-- .../net/corda/testing/SerializationTestHelpers.kt | 6 +++--- 11 files changed, 39 insertions(+), 39 deletions(-) diff --git a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt index bf5140fc36..ea0c88189c 100644 --- a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt +++ b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt @@ -27,9 +27,9 @@ import net.corda.finance.flows.CashExitFlow import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.nodeapi.internal.ServiceInfo import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User +import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.driver.driver import net.corda.testing.node.DriverBasedTest @@ -71,7 +71,7 @@ class NodeMonitorModelTest : DriverBasedTest() { vaultUpdates = monitor.vaultUpdates.bufferUntilSubscribed() networkMapUpdates = monitor.networkMap.bufferUntilSubscribed() - monitor.register(aliceNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password, initialiseSerialization = false) + monitor.register(aliceNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password) rpc = monitor.proxyObservable.value!! notaryParty = notaryHandle.nodeInfo.legalIdentities[1] @@ -79,7 +79,7 @@ class NodeMonitorModelTest : DriverBasedTest() { bobNode = bobNodeHandle.nodeInfo val monitorBob = NodeMonitorModel() stateMachineUpdatesBob = monitorBob.stateMachineUpdates.bufferUntilSubscribed() - monitorBob.register(bobNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password, initialiseSerialization = false) + monitorBob.register(bobNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password) rpcBob = monitorBob.proxyObservable.value!! runTest() } diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt index 3cde49536e..9bece5edcc 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt @@ -55,13 +55,12 @@ class NodeMonitorModel { * Register for updates to/from a given vault. * TODO provide an unsubscribe mechanism */ - fun register(nodeHostAndPort: NetworkHostAndPort, username: String, password: String, initialiseSerialization: Boolean = true) { + fun register(nodeHostAndPort: NetworkHostAndPort, username: String, password: String) { val client = CordaRPCClient( hostAndPort = nodeHostAndPort, - configuration = CordaRPCClientConfiguration.default.copy( + configuration = CordaRPCClientConfiguration.DEFAULT.copy( connectionMaxRetryInterval = 10.seconds - ), - initialiseSerialization = initialiseSerialization + ) ) val connection = client.start(username, password) val proxy = connection.proxy 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 1a6ce172cd..1862324c4f 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 @@ -8,12 +8,12 @@ import net.corda.core.utilities.OpaqueBytes; import net.corda.finance.flows.AbstractCashFlow; import net.corda.finance.flows.CashIssueFlow; import net.corda.finance.flows.CashPaymentFlow; -import net.corda.finance.schemas.*; +import net.corda.finance.schemas.CashSchemaV1; import net.corda.node.internal.Node; import net.corda.node.internal.StartedNode; import net.corda.node.services.transactions.ValidatingNotaryService; -import net.corda.nodeapi.internal.ServiceInfo; import net.corda.nodeapi.User; +import net.corda.nodeapi.internal.ServiceInfo; import net.corda.testing.CoreTestUtils; import net.corda.testing.node.NodeBasedTest; import org.junit.After; @@ -24,14 +24,15 @@ import java.io.IOException; import java.util.*; import java.util.concurrent.ExecutionException; -import static java.util.Collections.*; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; import static java.util.Objects.requireNonNull; import static kotlin.test.AssertionsKt.assertEquals; -import static net.corda.client.rpc.CordaRPCClientConfiguration.getDefault; import static net.corda.finance.Currencies.DOLLARS; import static net.corda.finance.contracts.GetBalances.getCashBalance; import static net.corda.node.services.FlowPermissions.startFlowPermission; -import static net.corda.testing.CoreTestUtils.*; +import static net.corda.testing.CoreTestUtils.setCordappPackages; +import static net.corda.testing.CoreTestUtils.unsetCordappPackages; import static net.corda.testing.TestConstants.getALICE; public class CordaRPCJavaClientTest extends NodeBasedTest { @@ -56,7 +57,7 @@ public class CordaRPCJavaClientTest extends NodeBasedTest { CordaFuture> nodeFuture = startNode(getALICE().getName(), 1, services, singletonList(rpcUser), emptyMap()); node = nodeFuture.get(); node.getInternals().registerCustomSchemas(Collections.singleton(CashSchemaV1.INSTANCE)); - client = new CordaRPCClient(requireNonNull(node.getInternals().getConfiguration().getRpcAddress()), getDefault(), false); + client = new CordaRPCClient(requireNonNull(node.getInternals().getConfiguration().getRpcAddress())); } @After diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt index b4c10fa808..32284a970f 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt @@ -19,9 +19,9 @@ import net.corda.finance.schemas.CashSchemaV1 import net.corda.node.internal.Node import net.corda.node.internal.StartedNode import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.nodeapi.internal.ServiceInfo import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.User +import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.ALICE import net.corda.testing.chooseIdentity import net.corda.testing.node.NodeBasedTest @@ -54,7 +54,7 @@ class CordaRPCClientTest : NodeBasedTest() { setCordappPackages("net.corda.finance.contracts") node = startNode(ALICE.name, rpcUsers = listOf(rpcUser), advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))).getOrThrow() node.internals.registerCustomSchemas(setOf(CashSchemaV1)) - client = CordaRPCClient(node.internals.configuration.rpcAddress!!, initialiseSerialization = false) + client = CordaRPCClient(node.internals.configuration.rpcAddress!!) } @After 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 635db3afea..2def3fbf20 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 @@ -34,8 +34,8 @@ data class CordaRPCClientConfiguration(val connectionMaxRetryInterval: Duration) /** * Returns the default configuration we recommend you use. */ - @JvmStatic - val default = CordaRPCClientConfiguration(connectionMaxRetryInterval = RPCClientConfiguration.default.connectionMaxRetryInterval) + @JvmField + val DEFAULT = CordaRPCClientConfiguration(connectionMaxRetryInterval = RPCClientConfiguration.default.connectionMaxRetryInterval) } } @@ -67,16 +67,11 @@ data class CordaRPCClientConfiguration(val connectionMaxRetryInterval: Duration) */ class CordaRPCClient @JvmOverloads constructor( hostAndPort: NetworkHostAndPort, - configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default, - initialiseSerialization: Boolean = true + configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT ) { init { - // Init serialization. It's plausible there are multiple clients in a single JVM, so be tolerant of - // others having registered first. // TODO: allow clients to have serialization factory etc injected and align with RPC protocol version? - if (initialiseSerialization) { - KryoClientSerializationScheme.initialiseSerialization() - } + KryoClientSerializationScheme.initialiseSerialization() } private val rpcClient = RPCClient( diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/KryoClientSerializationScheme.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/KryoClientSerializationScheme.kt index 66ddb33b1d..2275cd0d3f 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/KryoClientSerializationScheme.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/KryoClientSerializationScheme.kt @@ -5,6 +5,7 @@ import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationDefaults import net.corda.core.utilities.ByteSequence import net.corda.nodeapi.internal.serialization.* +import java.util.concurrent.atomic.AtomicBoolean class KryoClientSerializationScheme : AbstractKryoSerializationScheme() { override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean { @@ -23,7 +24,9 @@ class KryoClientSerializationScheme : AbstractKryoSerializationScheme() { override fun rpcServerKryoPool(context: SerializationContext): KryoPool = throw UnsupportedOperationException() companion object { + val isInitialised = AtomicBoolean(false) fun initialiseSerialization() { + if (!isInitialised.compareAndSet(false, true)) return try { SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply { registerScheme(KryoClientSerializationScheme()) @@ -31,10 +34,14 @@ class KryoClientSerializationScheme : AbstractKryoSerializationScheme() { } SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT SerializationDefaults.RPC_CLIENT_CONTEXT = KRYO_RPC_CLIENT_CONTEXT - } catch(e: IllegalStateException) { + } catch (e: IllegalStateException) { // Check that it's registered as we expect - check(SerializationDefaults.SERIALIZATION_FACTORY is SerializationFactoryImpl) { "RPC client encountered conflicting configuration of serialization subsystem." } - check((SerializationDefaults.SERIALIZATION_FACTORY as SerializationFactoryImpl).alreadyRegisteredSchemes.any { it is KryoClientSerializationScheme }) { "RPC client encountered conflicting configuration of serialization subsystem." } + val factory = SerializationDefaults.SERIALIZATION_FACTORY + val checkedFactory = factory as? SerializationFactoryImpl + ?: throw IllegalStateException("RPC client encountered conflicting configuration of serialization subsystem: $factory") + check(checkedFactory.alreadyRegisteredSchemes.any { it is KryoClientSerializationScheme }) { + "RPC client encountered conflicting configuration of serialization subsystem." + } } } } 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 af332fcf21..d6aaeb10a7 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 @@ -154,7 +154,7 @@ abstract class MQSecurityTest : NodeBasedTest() { } fun loginToRPC(target: NetworkHostAndPort, rpcUser: User): CordaRPCOps { - return CordaRPCClient(target, initialiseSerialization = false).start(rpcUser.username, rpcUser.password).proxy + return CordaRPCClient(target).start(rpcUser.username, rpcUser.password).proxy } fun loginToRPCAndGetClientQueue(): String { diff --git a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt index bc72cc65d7..4d3c92b7dc 100644 --- a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt +++ b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt @@ -98,7 +98,7 @@ class IRSDemoTest : IntegrationTestCategory { } private fun getFixingDateObservable(config: FullNodeConfiguration): Observable { - val client = CordaRPCClient(config.rpcAddress!!, initialiseSerialization = false) + val client = CordaRPCClient(config.rpcAddress!!) val proxy = client.start("user", "password").proxy val vaultUpdates = proxy.vaultTrackBy().updates diff --git a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt index 2625e43280..93034badfe 100644 --- a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt +++ b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt @@ -10,13 +10,11 @@ import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.schemas.CommercialPaperSchemaV1 import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService -import net.corda.nodeapi.internal.ServiceInfo import net.corda.nodeapi.User +import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.driver.poll import net.corda.testing.node.NodeBasedTest -import net.corda.testing.setCordappPackages -import net.corda.testing.unsetCordappPackages import net.corda.traderdemo.flow.BuyerFlow import net.corda.traderdemo.flow.CommercialPaperIssueFlow import net.corda.traderdemo.flow.SellerFlow @@ -56,11 +54,11 @@ class TraderDemoTest : NodeBasedTest() { nodeB.internals.registerCustomSchemas(setOf(CashSchemaV1, CommercialPaperSchemaV1)) val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map { - val client = CordaRPCClient(it.internals.configuration.rpcAddress!!, initialiseSerialization = false) + val client = CordaRPCClient(it.internals.configuration.rpcAddress!!) client.start(demoUser.username, demoUser.password).proxy } val nodeBankRpc = let { - val client = CordaRPCClient(bankNode.internals.configuration.rpcAddress!!, initialiseSerialization = false) + val client = CordaRPCClient(bankNode.internals.configuration.rpcAddress!!) client.start(bankUser.username, bankUser.password).proxy } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index 3366440b6e..c279e0aa4d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -221,7 +221,7 @@ sealed class NodeHandle { } } - fun rpcClientToNode(): CordaRPCClient = CordaRPCClient(configuration.rpcAddress!!, initialiseSerialization = false) + fun rpcClientToNode(): CordaRPCClient = CordaRPCClient(configuration.rpcAddress!!) /** * Stops the referenced node. @@ -635,7 +635,7 @@ class DriverDSL( private fun establishRpc(config: FullNodeConfiguration, processDeathFuture: CordaFuture): CordaFuture { val rpcAddress = config.rpcAddress!! - val client = CordaRPCClient(rpcAddress, initialiseSerialization = false) + val client = CordaRPCClient(rpcAddress) val connectionFuture = poll(executorService, "RPC connection") { try { client.start(config.rpcUsers[0].username, config.rpcUsers[0].password) diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt index 5da78a32af..73884f6cab 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt @@ -17,11 +17,11 @@ inline fun withTestSerialization(block: () -> T): T { } fun initialiseTestSerialization() { + // Stop the CordaRPCClient from trying to setup the defaults as we're about to do it now + KryoClientSerializationScheme.isInitialised.set(true) // Check that everything is configured for testing with mutable delegating instances. try { - check(SerializationDefaults.SERIALIZATION_FACTORY is TestSerializationFactory) { - "Found non-test serialization configuration: ${SerializationDefaults.SERIALIZATION_FACTORY}" - } + check(SerializationDefaults.SERIALIZATION_FACTORY is TestSerializationFactory) } catch(e: IllegalStateException) { SerializationDefaults.SERIALIZATION_FACTORY = TestSerializationFactory() } From 0c910640bbfad73e1ec190db869f891124e3b7d9 Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Wed, 27 Sep 2017 15:14:39 +0100 Subject: [PATCH 022/185] Misc documentation fixes (#1690) --- docs/source/tut-two-party-flow.rst | 27 ++++++------ docs/source/tutorial-test-dsl.rst | 68 +++++++++++++++--------------- docs/source/writing-cordapps.rst | 2 +- 3 files changed, 50 insertions(+), 47 deletions(-) diff --git a/docs/source/tut-two-party-flow.rst b/docs/source/tut-two-party-flow.rst index 441bd43c3b..9837f600c8 100644 --- a/docs/source/tut-two-party-flow.rst +++ b/docs/source/tut-two-party-flow.rst @@ -41,7 +41,8 @@ In ``IOUFlow.java``/``IOUFlow.kt``, update ``IOUFlow.call`` as follows: val signedTx = serviceHub.signInitialTransaction(txBuilder) // Obtaining the counterparty's signature - val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx, CollectSignaturesFlow.tracker())) + val otherSession = initiateFlow(otherParty) + val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx, setOf(otherSession), CollectSignaturesFlow.tracker())) // Finalising the transaction. subFlow(FinalityFlow(fullySignedTx)) @@ -52,6 +53,7 @@ In ``IOUFlow.java``/``IOUFlow.kt``, update ``IOUFlow.call`` as follows: import com.google.common.collect.ImmutableList; import java.security.PublicKey; + import java.util.Collections; import java.util.List; ... @@ -69,7 +71,8 @@ In ``IOUFlow.java``/``IOUFlow.kt``, update ``IOUFlow.call`` as follows: final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); // Obtaining the counterparty's signature - final SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(signedTx, CollectSignaturesFlow.Companion.tracker())); + final FlowSession otherSession = initiateFlow(otherParty) + final SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(signedTx, Collections.singleton(otherSession), CollectSignaturesFlow.Companion.tracker())); // Finalising the transaction. subFlow(new FinalityFlow(fullySignedTx)); @@ -98,10 +101,10 @@ In a new ``IOUFlowResponder.java`` file in Java, or within the ``App.kt`` file i ... @InitiatedBy(IOUFlow::class) - class IOUFlowResponder(val otherParty: Party) : FlowLogic() { + class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic() { @Suspendable override fun call() { - val signTransactionFlow = object : SignTransactionFlow(otherParty, SignTransactionFlow.tracker()) { + val signTransactionFlow = object : SignTransactionFlow(otherPartySession, SignTransactionFlow.tracker()) { override fun checkTransaction(stx: SignedTransaction) = requireThat { val output = stx.tx.outputs.single().data "This must be an IOU transaction." using (output is IOUState) @@ -123,9 +126,9 @@ In a new ``IOUFlowResponder.java`` file in Java, or within the ``App.kt`` file i import net.corda.core.contracts.ContractState; import net.corda.core.flows.FlowException; 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.core.identity.Party; import net.corda.core.transactions.SignedTransaction; import net.corda.core.utilities.ProgressTracker; @@ -133,18 +136,18 @@ In a new ``IOUFlowResponder.java`` file in Java, or within the ``App.kt`` file i @InitiatedBy(IOUFlow.class) public class IOUFlowResponder extends FlowLogic { - private final Party otherParty; + private final FlowSession otherPartySession; - public IOUFlowResponder(Party otherParty) { - this.otherParty = otherParty; + public IOUFlowResponder(FlowSession otherPartySession) { + this.otherPartySession = otherPartySession; } @Suspendable @Override public Void call() throws FlowException { - class signTxFlow extends SignTransactionFlow { - private signTxFlow(Party otherParty, ProgressTracker progressTracker) { - super(otherParty, progressTracker); + class SignTxFlow extends SignTransactionFlow { + private signTxFlow(FlowSession otherPartySession, ProgressTracker progressTracker) { + super(otherPartySession, progressTracker); } @Override @@ -159,7 +162,7 @@ In a new ``IOUFlowResponder.java`` file in Java, or within the ``App.kt`` file i } } - subFlow(new signTxFlow(otherParty, SignTransactionFlow.Companion.tracker())); + subFlow(new SignTxFlow(otherPartySession, SignTransactionFlow.Companion.tracker())); return null; } diff --git a/docs/source/tutorial-test-dsl.rst b/docs/source/tutorial-test-dsl.rst index 51aa2789c8..2eb870c781 100644 --- a/docs/source/tutorial-test-dsl.rst +++ b/docs/source/tutorial-test-dsl.rst @@ -249,7 +249,7 @@ We can continue to build the transaction until it ``verifies``: input(inState) command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } this `fails with` "the state is propagated" - output("alice's paper") { inState `owned by` ALICE_PUBKEY } + output(CommercialPaper.CP_PROGRAM_ID, "alice's paper") { inState `owned by` ALICE_PUBKEY } this.verifies() } } @@ -265,7 +265,7 @@ We can continue to build the transaction until it ``verifies``: tx.input(inState); tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); tx.failsWith("the state is propagated"); - tx.output("alice's paper", inState.withOwner(getALICE_PUBKEY())); + tx.output(CommercialPaper.CP_PROGRAM_ID, "alice's paper", inState.withOwner(getALICE_PUBKEY())); return tx.verifies(); }); return Unit.INSTANCE; @@ -289,7 +289,7 @@ What should we do if we wanted to test what happens when the wrong party signs t fun `simple issuance with tweak`() { ledger { transaction { - output("paper") { getPaper() } // Some CP is issued onto the ledger by MegaCorp. + output(CommercialPaper.CP_PROGRAM_ID, "paper") { getPaper() } // Some CP is issued onto the ledger by MegaCorp. tweak { command(DUMMY_PUBKEY_1) { CommercialPaper.Commands.Issue() } timestamp(TEST_TX_TIME) @@ -308,7 +308,7 @@ What should we do if we wanted to test what happens when the wrong party signs t public void simpleIssuanceWithTweak() { ledger(l -> { l.transaction(tx -> { - tx.output("paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp. + tx.output(CommercialPaper.CP_PROGRAM_ID, "paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp. tx.tweak(tw -> { tw.command(getDUMMY_PUBKEY_1(), new JavaCommercialPaper.Commands.Issue()); tw.timestamp(getTEST_TX_TIME()); @@ -337,7 +337,7 @@ ledger with a single transaction: @Test fun `simple issuance with tweak and top level transaction`() { transaction { - output("paper") { getPaper() } // Some CP is issued onto the ledger by MegaCorp. + output(CommercialPaper.CP_PROGRAM_ID, "paper") { getPaper() } // Some CP is issued onto the ledger by MegaCorp. tweak { command(DUMMY_PUBKEY_1) { CommercialPaper.Commands.Issue() } timestamp(TEST_TX_TIME) @@ -354,7 +354,7 @@ ledger with a single transaction: @Test public void simpleIssuanceWithTweakTopLevelTx() { transaction(tx -> { - tx.output("paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp. + tx.output(CommercialPaper.CP_PROGRAM_ID, "paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp. tx.tweak(tw -> { tw.command(getDUMMY_PUBKEY_1(), new JavaCommercialPaper.Commands.Issue()); tw.timestamp(getTEST_TX_TIME()); @@ -381,12 +381,12 @@ Now that we know how to define a single transaction, let's look at how to define ledger { unverifiedTransaction { - output("alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY) + output(Cash.CP_PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY) } // Some CP is issued onto the ledger by MegaCorp. transaction("Issuance") { - output("paper") { getPaper() } + output(CommercialPaper.CP_PROGRAM_ID, "paper") { getPaper() } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } timestamp(TEST_TX_TIME) this.verifies() @@ -396,8 +396,8 @@ Now that we know how to define a single transaction, let's look at how to define transaction("Trade") { input("paper") input("alice's $900") - output("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } - output("alice's paper") { "paper".output() `owned by` ALICE_PUBKEY } + output(Cash.CP_PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } + output(CommercialPaper.CP_PROGRAM_ID, "alice's paper") { "paper".output() `owned by` ALICE_PUBKEY } command(ALICE_PUBKEY) { Cash.Commands.Move() } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } this.verifies() @@ -412,14 +412,14 @@ Now that we know how to define a single transaction, let's look at how to define PartyAndReference issuer = getMEGA_CORP().ref(defaultRef); ledger(l -> { l.unverifiedTransaction(tx -> { - tx.output("alice's $900", + tx.output(Cash.CP_PROGRAM_ID, "alice's $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getALICE_PUBKEY(), null)); return Unit.INSTANCE; }); // Some CP is issued onto the ledger by MegaCorp. l.transaction("Issuance", tx -> { - tx.output("paper", getPaper()); + tx.output(CommercialPaper.CP_PROGRAM_ID, "paper", getPaper()); tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); tx.timestamp(getTEST_TX_TIME()); return tx.verifies(); @@ -428,9 +428,9 @@ Now that we know how to define a single transaction, let's look at how to define l.transaction("Trade", tx -> { tx.input("paper"); tx.input("alice's $900"); - tx.output("borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getMEGA_CORP_PUBKEY(), null)); + tx.output(Cash.CP_PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getMEGA_CORP_PUBKEY(), null)); JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); - tx.output("alice's paper", inputPaper.withOwner(getALICE_PUBKEY())); + tx.output(CommercialPaper.CP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(getALICE_PUBKEY())); tx.command(getALICE_PUBKEY(), new Cash.Commands.Move()); tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); return tx.verifies(); @@ -462,12 +462,12 @@ To do so let's create a simple example that uses the same input twice: val issuer = MEGA_CORP.ref(123) ledger { unverifiedTransaction { - output("alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY) + output(Cash.CP_PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY) } // Some CP is issued onto the ledger by MegaCorp. transaction("Issuance") { - output("paper") { getPaper() } + output(CommercialPaper.CP_PROGRAM_ID, "paper") { getPaper() } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } timestamp(TEST_TX_TIME) this.verifies() @@ -476,8 +476,8 @@ To do so let's create a simple example that uses the same input twice: transaction("Trade") { input("paper") input("alice's $900") - output("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } - output("alice's paper") { "paper".output() `owned by` ALICE_PUBKEY } + output(Cash.CP_PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } + output(CommercialPaper.CP_PROGRAM_ID, "alice's paper") { "paper".output() `owned by` ALICE_PUBKEY } command(ALICE_PUBKEY) { Cash.Commands.Move() } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } this.verifies() @@ -486,7 +486,7 @@ To do so let's create a simple example that uses the same input twice: transaction { input("paper") // We moved a paper to another pubkey. - output("bob's paper") { "paper".output() `owned by` BOB_PUBKEY } + output(CommercialPaper.CP_PROGRAM_ID, "bob's paper") { "paper".output() `owned by` BOB_PUBKEY } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } this.verifies() } @@ -502,14 +502,14 @@ To do so let's create a simple example that uses the same input twice: PartyAndReference issuer = getMEGA_CORP().ref(defaultRef); ledger(l -> { l.unverifiedTransaction(tx -> { - tx.output("alice's $900", + tx.output(Cash.CP_PROGRAM_ID, "alice's $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getALICE_PUBKEY(), null)); return Unit.INSTANCE; }); // Some CP is issued onto the ledger by MegaCorp. l.transaction("Issuance", tx -> { - tx.output("paper", getPaper()); + tx.output(CommercialPaper.CP_PROGRAM_ID, "paper", getPaper()); tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); tx.timestamp(getTEST_TX_TIME()); return tx.verifies(); @@ -518,9 +518,9 @@ To do so let's create a simple example that uses the same input twice: l.transaction("Trade", tx -> { tx.input("paper"); tx.input("alice's $900"); - tx.output("borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getMEGA_CORP_PUBKEY(), null)); + tx.output(Cash.CP_PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getMEGA_CORP_PUBKEY(), null)); JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); - tx.output("alice's paper", inputPaper.withOwner(getALICE_PUBKEY())); + tx.output(CommercialPaper.CP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(getALICE_PUBKEY())); tx.command(getALICE_PUBKEY(), new Cash.Commands.Move()); tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); return tx.verifies(); @@ -530,7 +530,7 @@ To do so let's create a simple example that uses the same input twice: tx.input("paper"); JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); // We moved a paper to other pubkey. - tx.output("bob's paper", inputPaper.withOwner(getBOB_PUBKEY())); + tx.output(CommercialPaper.CP_PROGRAM_ID, "bob's paper", inputPaper.withOwner(getBOB_PUBKEY())); tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); return tx.verifies(); }); @@ -551,12 +551,12 @@ verification (``this.fails()`` at the end). As in previous examples we can use ` val issuer = MEGA_CORP.ref(123) ledger { unverifiedTransaction { - output("alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY) + output(Cash.CP_PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY) } // Some CP is issued onto the ledger by MegaCorp. transaction("Issuance") { - output("paper") { getPaper() } + output(CommercialPaper.CP_PROGRAM_ID, "paper") { getPaper() } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } timestamp(TEST_TX_TIME) this.verifies() @@ -565,8 +565,8 @@ verification (``this.fails()`` at the end). As in previous examples we can use ` transaction("Trade") { input("paper") input("alice's $900") - output("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } - output("alice's paper") { "paper".output() `owned by` ALICE_PUBKEY } + output(Cash.CP_PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } + output(CommercialPaper.CP_PROGRAM_ID, "alice's paper") { "paper".output() `owned by` ALICE_PUBKEY } command(ALICE_PUBKEY) { Cash.Commands.Move() } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } this.verifies() @@ -576,7 +576,7 @@ verification (``this.fails()`` at the end). As in previous examples we can use ` transaction { input("paper") // We moved a paper to another pubkey. - output("bob's paper") { "paper".output() `owned by` BOB_PUBKEY } + output(CommercialPaper.CP_PROGRAM_ID, "bob's paper") { "paper".output() `owned by` BOB_PUBKEY } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } this.verifies() } @@ -594,14 +594,14 @@ verification (``this.fails()`` at the end). As in previous examples we can use ` PartyAndReference issuer = getMEGA_CORP().ref(defaultRef); ledger(l -> { l.unverifiedTransaction(tx -> { - tx.output("alice's $900", + tx.output(Cash.CP_PROGRAM_ID, "alice's $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getALICE_PUBKEY(), null)); return Unit.INSTANCE; }); // Some CP is issued onto the ledger by MegaCorp. l.transaction("Issuance", tx -> { - tx.output("paper", getPaper()); + tx.output(CommercialPaper.CP_PROGRAM_ID, "paper", getPaper()); tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); tx.timestamp(getTEST_TX_TIME()); return tx.verifies(); @@ -610,9 +610,9 @@ verification (``this.fails()`` at the end). As in previous examples we can use ` l.transaction("Trade", tx -> { tx.input("paper"); tx.input("alice's $900"); - tx.output("borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getMEGA_CORP_PUBKEY(), null)); + tx.output(Cash.CP_PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getMEGA_CORP_PUBKEY(), null)); JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); - tx.output("alice's paper", inputPaper.withOwner(getALICE_PUBKEY())); + tx.output(CommercialPaper.CP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(getALICE_PUBKEY())); tx.command(getALICE_PUBKEY(), new Cash.Commands.Move()); tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); return tx.verifies(); @@ -623,7 +623,7 @@ verification (``this.fails()`` at the end). As in previous examples we can use ` tx.input("paper"); JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); // We moved a paper to another pubkey. - tx.output("bob's paper", inputPaper.withOwner(getBOB_PUBKEY())); + tx.output(CommercialPaper.CP_PROGRAM_ID, "bob's paper", inputPaper.withOwner(getBOB_PUBKEY())); tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); return tx.verifies(); }); diff --git a/docs/source/writing-cordapps.rst b/docs/source/writing-cordapps.rst index 13c14f314e..9200cb8fe5 100644 --- a/docs/source/writing-cordapps.rst +++ b/docs/source/writing-cordapps.rst @@ -64,7 +64,7 @@ Defining plugins ---------------- Your CorDapp may need to define two types of plugins: -* ``CordaPluginRegistry`` subclasses, which define additional serializable classes and vault schemas +* ``CordaPluginRegistry`` subclasses, which define additional serializable classes * ``WebServerPluginRegistry`` subclasses, which define the APIs and static web content served by your CorDapp The fully-qualified class path of each ``CordaPluginRegistry`` subclass must then be added to the From 39160de0a36d4dd91d85692f5692dd0b830c9176 Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Wed, 27 Sep 2017 15:15:34 +0100 Subject: [PATCH 023/185] BIGINT fix for H2 coin selection. (#1659) --- .../src/main/kotlin/net/corda/finance/Currencies.kt | 13 ++++++++++++- .../asset/cash/selection/CashSelectionH2Impl.kt | 2 +- .../main/kotlin/net/corda/traderdemo/TraderDemo.kt | 6 +++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/finance/src/main/kotlin/net/corda/finance/Currencies.kt b/finance/src/main/kotlin/net/corda/finance/Currencies.kt index 5e1b997aed..906e01d8af 100644 --- a/finance/src/main/kotlin/net/corda/finance/Currencies.kt +++ b/finance/src/main/kotlin/net/corda/finance/Currencies.kt @@ -15,17 +15,28 @@ import java.util.* @JvmField val JPY: Currency = Currency.getInstance("JPY") @JvmField val RUB: Currency = Currency.getInstance("RUB") -fun AMOUNT(amount: Int, token: T): Amount = Amount.fromDecimal(BigDecimal.valueOf(amount.toLong()), token) +fun AMOUNT(amount: Int, token: T): Amount = AMOUNT(amount.toLong(), token) +fun AMOUNT(amount: Long, token: T): Amount = Amount.fromDecimal(BigDecimal.valueOf(amount), token) fun AMOUNT(amount: Double, token: T): Amount = Amount.fromDecimal(BigDecimal.valueOf(amount), token) fun DOLLARS(amount: Int): Amount = AMOUNT(amount, USD) +fun DOLLARS(amount: Long): Amount = AMOUNT(amount, USD) fun DOLLARS(amount: Double): Amount = AMOUNT(amount, USD) fun POUNDS(amount: Int): Amount = AMOUNT(amount, GBP) +fun POUNDS(amount: Long): Amount = AMOUNT(amount, GBP) +fun POUNDS(amount: Double): Amount = AMOUNT(amount, GBP) fun SWISS_FRANCS(amount: Int): Amount = AMOUNT(amount, CHF) +fun SWISS_FRANCS(amount: Long): Amount = AMOUNT(amount, CHF) +fun SWISS_FRANCS(amount: Double): Amount = AMOUNT(amount, CHF) val Int.DOLLARS: Amount get() = DOLLARS(this) +val Long.DOLLARS: Amount get() = DOLLARS(this) val Double.DOLLARS: Amount get() = DOLLARS(this) val Int.POUNDS: Amount get() = POUNDS(this) +val Long.POUNDS: Amount get() = POUNDS(this) +val Double.POUNDS: Amount get() = POUNDS(this) val Int.SWISS_FRANCS: Amount get() = SWISS_FRANCS(this) +val Long.SWISS_FRANCS: Amount get() = SWISS_FRANCS(this) +val Double.SWISS_FRANCS: Amount get() = SWISS_FRANCS(this) infix fun Currency.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) infix fun Amount.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) 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 fb3a84371b..7c18981857 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 @@ -77,7 +77,7 @@ class CashSelectionH2Impl : CashSelection { spendLock.withLock { val statement = services.jdbcSession().createStatement() try { - statement.execute("CALL SET(@t, 0);") + statement.execute("CALL SET(@t, CAST(0 AS BIGINT));") // we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null) // the softLockReserve update will detect whether we try to lock states locked by others diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt index 689d3b4e52..43c82f5565 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt @@ -44,17 +44,17 @@ private class TraderDemo { } // What happens next depends on the role. The buyer sits around waiting for a trade to start. The seller role - // will contact the buyer and actually make something happen. + // will contact the buyer and actually make something happen. We intentionally use large amounts here. val role = options.valueOf(roleArg)!! if (role == Role.BANK) { val bankHost = NetworkHostAndPort("localhost", bankRpcPort) CordaRPCClient(bankHost).use("demo", "demo") { - TraderDemoClientApi(it.proxy).runIssuer(1100.DOLLARS, buyerName, sellerName) + TraderDemoClientApi(it.proxy).runIssuer(1_100_000_000_000.DOLLARS, buyerName, sellerName) } } else { val sellerHost = NetworkHostAndPort("localhost", sellerRpcPort) CordaRPCClient(sellerHost).use("demo", "demo") { - TraderDemoClientApi(it.proxy).runSeller(1000.DOLLARS, buyerName) + TraderDemoClientApi(it.proxy).runSeller(1_000_000_000_000.DOLLARS, buyerName) } } } From 512de2690d9fd177cb1f3de79f01a35a91988374 Mon Sep 17 00:00:00 2001 From: josecoll Date: Wed, 27 Sep 2017 15:29:00 +0100 Subject: [PATCH 024/185] Added changelog entry. (#1691) --- docs/source/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index af1374367b..2f01167fb3 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -12,6 +12,9 @@ UNRELEASED Release 1.0 ----------- +* Unification of VaultQuery And VaultService APIs + Developers now only need to work with a single Vault Service API for all needs. + * Java 8 lambdas now work property with Kryo during check-pointing. * String constants have been marked as ``const`` type in Kotlin, eliminating cases where functions of the form From 72cff032e67285e30d5d148d19c2881eb6ebbaf0 Mon Sep 17 00:00:00 2001 From: Andras Slemmer <0slemi0@gmail.com> Date: Wed, 27 Sep 2017 15:33:09 +0100 Subject: [PATCH 025/185] FlowSession docs (#1660) --- .../net/corda/core/flows/FlowSession.kt | 17 +- docs/source/api-flows.rst | 155 +++++++++++++++--- .../java/net/corda/docs/FlowCookbookJava.java | 7 + .../kotlin/net/corda/docs/FlowCookbook.kt | 14 +- docs/source/flow-state-machines.rst | 14 +- 5 files changed, 171 insertions(+), 36 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt b/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt index 9c2e5425d6..74bd247a78 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt @@ -5,7 +5,18 @@ import net.corda.core.identity.Party import net.corda.core.utilities.UntrustworthyData /** - * To port existing flows: + * + * A [FlowSession] is a handle on a communication sequence between two flows. It is used to send and receive messages + * between flows. + * + * There are two ways of obtaining such a session: + * + * 1. Calling [FlowLogic.initiateFlow]. This will create a [FlowSession] object on which the first send/receive + * operation will attempt to kick off a corresponding [InitiatedBy] flow on the counterparty's node. + * 2. As constructor parameter to [InitiatedBy] flows. This session is the one corresponding to the initiating flow and + * may be used for replies. + * + * To port flows using the old Party-based API: * * Look for [Deprecated] usages of send/receive/sendAndReceive/getFlowInfo. * @@ -31,6 +42,10 @@ import net.corda.core.utilities.UntrustworthyData * otherSideSession.send(something) */ abstract class FlowSession { + /** + * The [Party] on the other side of this session. In the case of a session created by [FlowLogic.initiateFlow] + * [counterparty] is the same Party as the one passed to that function. + */ abstract val counterparty: Party /** diff --git a/docs/source/api-flows.rst b/docs/source/api-flows.rst index 4c1ef7fd2b..99cddf74b2 100644 --- a/docs/source/api-flows.rst +++ b/docs/source/api-flows.rst @@ -113,9 +113,8 @@ subclass's constructor can take any number of arguments of any type. The generic FlowLogic annotations --------------------- -Any flow that you wish to start either directly via RPC or as a subflow must be annotated with the -``@InitiatingFlow`` annotation. Additionally, if you wish to start the flow via RPC, you must annotate it with the -``@StartableByRPC`` annotation: +Any flow from which you want to initiate other flows must be annotated with the ``@InitiatingFlow`` annotation. +Additionally, if you wish to start the flow via RPC, you must annotate it with the ``@StartableByRPC`` annotation: .. container:: codeset @@ -139,7 +138,7 @@ Meanwhile, any flow that responds to a message from another flow must be annotat .. sourcecode:: kotlin @InitiatedBy(Initiator::class) - class Responder(val otherParty: Party) : FlowLogic() { } + class Responder(val otherSideSession: FlowSession) : FlowLogic() { } .. sourcecode:: java @@ -270,18 +269,50 @@ Finally, we can use the map to identify nodes providing a specific service (e.g. Communication between parties ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -``FlowLogic`` instances communicate using three functions: -* ``send(otherParty: Party, payload: Any)`` - * Sends the ``payload`` object to the ``otherParty`` -* ``receive(receiveType: Class, otherParty: Party)`` - * Receives an object of type ``receiveType`` from the ``otherParty`` -* ``sendAndReceive(receiveType: Class, otherParty: Party, payload: Any)`` - * Sends the ``payload`` object to the ``otherParty``, and receives an object of type ``receiveType`` back +In order to create a communication session between your initiator flow and the receiver flow you must call +``initiateFlow(party: Party): FlowSession`` + +``FlowSession`` instances in turn provide three functions: + +* ``send(payload: Any)`` + * Sends the ``payload`` object +* ``receive(receiveType: Class): R`` + * Receives an object of type ``receiveType`` +* ``sendAndReceive(receiveType: Class, payload: Any): R`` + * Sends the ``payload`` object and receives an object of type ``receiveType`` back + + +InitiateFlow +~~~~~~~~~~~~ + +``initiateFlow`` creates a communication session with the passed in ``Party``. + + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART initiateFlow + :end-before: DOCEND initiateFlow + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART initiateFlow + :end-before: DOCEND initiateFlow + :dedent: 12 + +Note that at the time of call to this function no actual communication is done, this is deferred to the first +send/receive, at which point the counterparty will either: + +1. Ignore the message if they are not registered to respond to messages from this flow. +2. Start the flow they have registered to respond to this flow. Send ~~~~ -We can send arbitrary data to a counterparty: + +Once we have a ``FlowSession`` object we can send arbitrary data to a counterparty: .. container:: codeset @@ -297,12 +328,7 @@ We can send arbitrary data to a counterparty: :end-before: DOCEND 4 :dedent: 12 -If this is the first ``send``, the counterparty will either: - -1. Ignore the message if they are not registered to respond to messages from this flow. -2. Start the flow they have registered to respond to this flow, and run the flow until the first call to ``receive``, - at which point they process the message. In other words, we are assuming that the counterparty is registered to - respond to this flow, and has a corresponding ``receive`` call. +The flow on the other side must eventually reach a corresponding ``receive`` call to get this message. Receive ~~~~~~~ @@ -351,6 +377,11 @@ as it likes, and each party can invoke a different response flow: :end-before: DOCEND 6 :dedent: 12 +.. warning:: If you initiate several counter flows from the same ``@InitiatingFlow`` flow then on the receiving side you + must be prepared to be initiated by any of the corresponding ``initiateFlow()`` calls! A good way of handling this + ambiguity is to send as a first message a "role" message to the initiated flow, indicating which part of the + initiating flow the rest of the counter-flow should conform to. + SendAndReceive ~~~~~~~~~~~~~~ We can also use a single call to send data to a counterparty and wait to receive data of a specific type back. The @@ -395,19 +426,91 @@ Our side of the flow must mirror these calls. We could do this as follows: :end-before: DOCEND 8 :dedent: 12 +Porting from the old Party-based API +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Before ``FlowSession`` s were introduced the send/receive API looked a bit different. They were functions on +``FlowLogic`` and took the address ``Party`` as argument. The platform internally maintained a mapping from ``Party`` to +session, hiding sessions from the user completely. + +However we realised that this could introduce subtle bugs and security issues where sends meant for different sessions +may end up in the same session if the target ``Party`` happens to be the same. + +Therefore the session concept is now exposed through ``FlowSession`` which disambiguates which communication sequence a +message is intended for. + +In the old API the first ``send`` or ``receive`` to a ``Party`` was the one kicking off the counterflow. This is now +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 + :language: kotlin + :start-after: DOCSTART FlowSession porting + :end-before: DOCEND FlowSession porting + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART FlowSession porting + :end-before: DOCEND FlowSession porting + :dedent: 12 + + Subflows -------- + +Subflows are pieces of reusable flows that may be run by calling ``FlowLogic.subFlow``. + +Inlined subflows +^^^^^^^^^^^^^^^^ + +Inlined subflows inherit their calling flow's type when initiating a new session with a counterparty. For example say +we have flow A calling an inlined subflow B, which in turn initiates a session with a party. The FlowLogic type used to +determine which counterflow should be kicked off will be A, not B. Note that this means that the other side of this +session must therefore be called explicitly in the kicked off flow as well. + +An example of such a flow is ``CollectSignaturesFlow``. It has a counter-flow ``SignTransactionFlow`` that isn't +annotated with ``InitiatedBy``. This is because both of these flows are inlined, the kick-off relationship will be +defined by the parent flows calling ``CollectSignaturesFlow`` and ``SignTransactionFlow``. + +In the code inlined subflows appear as regular ``FlowLogic`` instances, `without` either of the ``@InitiatingFlow`` or +``@InitiatedBy`` annotation. + +.. note:: Inlined flows aren't versioned, they inherit their parent flow's version. + +Initiating subflows +^^^^^^^^^^^^^^^^^^^ + +Initiating subflows are ones annotated with the ``@InitiatingFlow`` annotation. When such a flow initiates a session its +type will be used to determine which ``@InitiatedBy`` flow to kick off on the counterparty. + +An example is the ``@InitiatingFlow InitiatorFlow``/``@InitiatedBy ResponderFlow`` flow pair in the ``FlowCookbook``. + +.. note:: Initiating flows are versioned separately from their parents. + +Core initiating subflows +^^^^^^^^^^^^^^^^^^^^^^^^ + +Corda-provided initiating subflows are a little different to standard ones as they are versioned together with the +platform, and their initiated counterflows are registered explicitly, so there is no need for the ``InitiatedBy`` +annotation. + +An example is the ``FinalityFlow``/``FinalityHandler`` flow pair. + +Built-in subflows +^^^^^^^^^^^^^^^^^ + Corda provides a number of built-in flows that should be used for handling common tasks. The most important are: -* ``CollectSignaturesFlow``, which should be used to collect a transaction's required signatures -* ``FinalityFlow``, which should be used to notarise and record a transaction -* ``SendTransactionFlow``, which should be used to send a signed transaction if it needed to be resolved on the other side. -* ``ReceiveTransactionFlow``, which should be used receive a signed transaction -* ``ContractUpgradeFlow``, which should be used to change a state's contract -* ``NotaryChangeFlow``, which should be used to change a state's notary +* ``CollectSignaturesFlow`` (inlined), which should be used to collect a transaction's required signatures +* ``FinalityFlow`` (initiating), which should be used to notarise and record a transaction +* ``SendTransactionFlow`` (inlined), which should be used to send a signed transaction if it needed to be resolved on the other side. +* ``ReceiveTransactionFlow`` (inlined), which should be used receive a signed transaction +* ``ContractUpgradeFlow`` (initiating), which should be used to change a state's contract +* ``NotaryChangeFlow`` (initiating), which should be used to change a state's notary -These flows are designed to be used as building blocks in your own flows. You invoke them by calling -``FlowLogic.subFlow`` from within your flow's ``call`` method. Let's look at three very common examples. +Let's look at three very common examples. FinalityFlow ^^^^^^^^^^^^ 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/FlowCookbookJava.java index 7d72d528ce..8d92b01786 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/FlowCookbookJava.java @@ -567,6 +567,13 @@ public class FlowCookbookJava { SignedTransaction notarisedTx2 = subFlow(new FinalityFlow(fullySignedTx, additionalParties, FINALISATION.childProgressTracker())); // DOCEND 10 + // DOCSTART FlowSession porting + send(regulator, new Object()); // Old API + // becomes + FlowSession session = initiateFlow(regulator); + session.send(new Object()); + // DOCEND FlowSession porting + return null; } } 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/FlowCookbook.kt index 4b0864d714..6fb9df18b2 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/FlowCookbook.kt @@ -121,6 +121,10 @@ object FlowCookbook { throw IllegalArgumentException("Couldn't find counterparty with key: $dummyPubKey in identity service") // DOCEND 2 + // DOCSTART initiateFlow + val counterpartySession = initiateFlow(counterparty) + // DOCEND initiateFlow + /**----------------------------- * SENDING AND RECEIVING DATA * -----------------------------**/ @@ -137,7 +141,6 @@ object FlowCookbook { // registered to respond to this flow, and has a corresponding // ``receive`` call. // DOCSTART 4 - val counterpartySession = initiateFlow(counterparty) counterpartySession.send(Any()) // DOCEND 4 @@ -496,7 +499,7 @@ object FlowCookbook { // other required signers using ``CollectSignaturesFlow``. // The responder flow will need to call ``SignTransactionFlow``. // DOCSTART 15 - val fullySignedTx: SignedTransaction = subFlow(CollectSignaturesFlow(twiceSignedTx, emptySet(), SIGS_GATHERING.childProgressTracker())) + val fullySignedTx: SignedTransaction = subFlow(CollectSignaturesFlow(twiceSignedTx, setOf(counterpartySession, regulatorSession), SIGS_GATHERING.childProgressTracker())) // DOCEND 15 /**----------------------- @@ -540,6 +543,13 @@ object FlowCookbook { val additionalParties: Set = setOf(regulator) val notarisedTx2: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, additionalParties, FINALISATION.childProgressTracker())) // DOCEND 10 + + // DOCSTART FlowSession porting + send(regulator, Any()) // Old API + // becomes + val session = initiateFlow(regulator) + session.send(Any()) + // DOCEND FlowSession porting } } diff --git a/docs/source/flow-state-machines.rst b/docs/source/flow-state-machines.rst index 2ca3b56759..a125c6c221 100644 --- a/docs/source/flow-state-machines.rst +++ b/docs/source/flow-state-machines.rst @@ -297,13 +297,13 @@ time, and perhaps communicating with the same counterparty node but for differen way to segregate communication channels so that concurrent conversations between flows on the same set of nodes do not interfere with each other. -To achieve this the flow framework initiates a new flow session each time a flow starts communicating with a ``Party`` -for the first time. A session is simply a pair of IDs, one for each side, to allow the node to route received messages to -the correct flow. If the other side accepts the session request then subsequent sends and receives to that same ``Party`` -will use the same session. A session ends when either flow ends, whether as expected or pre-maturely. If a flow ends -pre-maturely then the other side will be notified of that and they will also end, as the whole point of flows is a known -sequence of message transfers. Flows end pre-maturely due to exceptions, and as described above, if that exception is -``FlowException`` or a sub-type then it will propagate to the other side. Any other exception will not propagate. +To achieve this in order to communicate with a counterparty one needs to first initiate such a session with a ``Party`` +using ``initiateFlow``, which returns a ``FlowSession`` object, identifying this communication. Subsequently the first +actual communication will kick off a counter-flow on the other side, receiving a "reply" session object. A session ends +when either flow ends, whether as expected or pre-maturely. If a flow ends pre-maturely then the other side will be +notified of that and they will also end, as the whole point of flows is a known sequence of message transfers. Flows end +pre-maturely due to exceptions, and as described above, if that exception is ``FlowException`` or a sub-type then it +will propagate to the other side. Any other exception will not propagate. Taking a step back, we mentioned that the other side has to accept the session request for there to be a communication channel. A node accepts a session request if it has registered the flow type (the fully-qualified class name) that is From c8fc1b624b7d34f38bc305a0af6e0078960a15d8 Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Wed, 27 Sep 2017 15:51:44 +0100 Subject: [PATCH 026/185] Updating docs w.r.t. new FilteredTransaction and TransactionSignature (#1678) --- docs/source/oracles.rst | 4 +-- .../source/tutorial-building-transactions.rst | 2 +- docs/source/tutorial-tear-offs.rst | 28 +++++++++++-------- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/docs/source/oracles.rst b/docs/source/oracles.rst index c8c6de060a..177da9a320 100644 --- a/docs/source/oracles.rst +++ b/docs/source/oracles.rst @@ -112,13 +112,13 @@ Here is an extract from the ``NodeInterestRates.Oracle`` class and supporting ty class Oracle { fun query(queries: List, deadline: Instant): List - fun sign(ftx: FilteredTransaction, txId: SecureHash): DigitalSignature.WithKey + fun sign(ftx: FilteredTransaction, txId: SecureHash): TransactionSignature } Because the fix contains a timestamp (the ``forDay`` field), that identifies the version of the data being requested, there can be an arbitrary delay between a fix being requested via ``query`` and the signature being requested via ``sign`` as the Oracle can know which, potentially historical, value it is being asked to sign for. This is an important -technique for continously varying data. +technique for continuously varying data. The ``query`` method takes a deadline, which is a point in time the requester is willing to wait until for the necessary data to be available. Not every oracle will need this. This can be useful where data is expected to be available on a diff --git a/docs/source/tutorial-building-transactions.rst b/docs/source/tutorial-building-transactions.rst index b8c25bf04a..3a7586dbb9 100644 --- a/docs/source/tutorial-building-transactions.rst +++ b/docs/source/tutorial-building-transactions.rst @@ -67,7 +67,7 @@ different ``SignedTransaction``. For instance in a foreign exchange scenario we shouldn't send a ``SignedTransaction`` with only our sell side populated as that could be used to take the money without the expected return of the other currency. Also, it is best practice for -flows to receive back the ``DigitalSignature.WithKey`` of other parties +flows to receive back the ``TransactionSignature`` of other parties rather than a full ``SignedTransaction`` objects, because otherwise we have to separately check that this is still the same ``SignedTransaction`` and not a malicious substitute. diff --git a/docs/source/tutorial-tear-offs.rst b/docs/source/tutorial-tear-offs.rst index 5187aaac42..12a1ec8512 100644 --- a/docs/source/tutorial-tear-offs.rst +++ b/docs/source/tutorial-tear-offs.rst @@ -36,16 +36,18 @@ IRSDemo example. Then we can construct ``FilteredTransaction``: In the Oracle example this step takes place in ``RatesFixFlow`` by overriding ``filtering`` function, see: :ref:`filtering_ref`. -``FilteredTransaction`` holds ``filteredLeaves`` (data that we wanted to reveal) and Merkle branch for them. +Both ``WireTransaction`` and ``FilteredTransaction`` inherit from ``TraversableTransaction``, so access to the +transaction components is exactly the same. Note that unlike ``WireTransaction``, +``FilteredTransaction`` only holds data that we wanted to reveal (after filtering). .. container:: codeset .. sourcecode:: kotlin - // Direct accsess to included commands, inputs, outputs, attachments etc. - val cmds: List = ftx.filteredLeaves.commands - val ins: List = ftx.filteredLeaves.inputs - val timeWindow: TimeWindow? = ftx.filteredLeaves.timeWindow + // Direct access to included commands, inputs, outputs, attachments etc. + val cmds: List = ftx.commands + val ins: List = ftx.inputs + val timeWindow: TimeWindow? = ftx.timeWindow ... .. literalinclude:: ../../samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt @@ -54,8 +56,8 @@ In the Oracle example this step takes place in ``RatesFixFlow`` by overriding `` :end-before: DOCEND 1 Above code snippet is taken from ``NodeInterestRates.kt`` file and implements a signing part of an Oracle. You can -check only leaves using ``leaves.checkWithFun { check(it) }`` and then verify obtained ``FilteredTransaction`` to see -if data from ``PartialMerkleTree`` belongs to ``WireTransaction`` with provided ``id``. All you need is the root hash +check only leaves using ``ftx.checkWithFun { check(it) }`` and then verify obtained ``FilteredTransaction`` to see +if its data belongs to ``WireTransaction`` with provided ``id``. All you need is the root hash of the full transaction: .. container:: codeset @@ -74,7 +76,11 @@ Or combine the two steps together: ftx.verifyWithFunction(merkleRoot, ::check) -.. note:: The way the ``FilteredTransaction`` is constructed ensures that after signing of the root hash it's impossible to add or remove - leaves. However, it can happen that having transaction with multiple commands one party reveals only subset of them to the Oracle. - As signing is done now over the Merkle root hash, the service signs all commands of given type, even though it didn't see - all of them. This issue will be handled after implementing partial signatures. +.. note:: The way the ``FilteredTransaction`` is constructed ensures that after signing of the root hash it's impossible +to add or remove components (leaves). However, it can happen that having transaction with multiple commands one party +reveals only subset of them to the Oracle. As signing is done now over the Merkle root hash, the service signs all +commands of given type, even though it didn't see all of them. In the case however where all of the commands should be +visible to an Oracle, one can type ``ftx.checkAllComponentsVisible(COMMANDS_GROUP)`` before invoking ``ftx.verify``. +``checkAllComponentsVisible`` is using a sophisticated underlying partial Merkle tree check to guarantee that all of +the components of a particular group that existed in the original ``WireTransaction`` are included in the received +``FilteredTransaction``. From 5ed755d3fe3a6efc6f6ed93b6effedd04422d8ec Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Wed, 27 Sep 2017 18:02:35 +0100 Subject: [PATCH 027/185] CORDA-653 - Serialised enums should respect whitelist (#1692) --- docs/source/changelog.rst | 4 + .../serialization/amqp/SerializerFactory.kt | 1 + .../internal/serialization/amqp/EnumTests.kt | 78 +++++++++++++++++++ 3 files changed, 83 insertions(+) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 2f01167fb3..c32cc9913a 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -9,6 +9,10 @@ UNRELEASED * ``Cordapp`` now has a name field for identifying CorDapps and all CorDapp names are printed to console at startup. +* Enums now respsect the whitelist applied to the Serializer factory serializing / deserializing them. If the enum isn't + either annotated with the @CordaSerializable annotation or explicitly whitelisted then a NotSerializableException is + thrown. + Release 1.0 ----------- diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt index fe05093168..2f0a734800 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt @@ -82,6 +82,7 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) { } } Enum::class.java.isAssignableFrom(actualClass ?: declaredClass) -> serializersByType.computeIfAbsent(actualClass ?: declaredClass) { + whitelisted(actualType) EnumSerializer(actualType, actualClass ?: declaredClass, this) } else -> makeClassSerializer(actualClass ?: declaredClass, actualType, declaredType) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumTests.kt index d46ec93c5a..d10152baf0 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumTests.kt @@ -1,5 +1,7 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.serialization.ClassWhitelist +import net.corda.core.serialization.CordaSerializable import org.junit.Test import java.time.DayOfWeek @@ -10,12 +12,18 @@ import java.io.File import java.io.NotSerializableException import net.corda.core.serialization.SerializedBytes +import org.assertj.core.api.Assertions class EnumTests { enum class Bras { TSHIRT, UNDERWIRE, PUSHUP, BRALETTE, STRAPLESS, SPORTS, BACKLESS, PADDED } + @CordaSerializable + enum class AnnotatedBras { + TSHIRT, UNDERWIRE, PUSHUP, BRALETTE, STRAPLESS, SPORTS, BACKLESS, PADDED + } + // The state of the OldBras enum when the tests in changedEnum1 were serialised // - use if the test file needs regenerating //enum class OldBras { @@ -186,4 +194,74 @@ class EnumTests { // we expect this to throw DeserializationInput(sf1).deserialize(SerializedBytes(sc2)) } + + @Test + fun enumNotWhitelistedFails() { + data class C (val c: Bras) + + class WL (val allowed: String): ClassWhitelist { + override fun hasListed(type: Class<*>): Boolean { + return type.name == allowed + } + } + + val factory = SerializerFactory(WL(classTestName("C")), ClassLoader.getSystemClassLoader()) + + Assertions.assertThatThrownBy { + TestSerializationOutput(VERBOSE, factory).serialize(C(Bras.UNDERWIRE)) + }.isInstanceOf(NotSerializableException::class.java) + } + + @Test + fun enumWhitelisted() { + data class C (val c: Bras) + + class WL : ClassWhitelist { + override fun hasListed(type: Class<*>): Boolean { + return type.name == "net.corda.nodeapi.internal.serialization.amqp.EnumTests\$enumWhitelisted\$C" || + type.name == "net.corda.nodeapi.internal.serialization.amqp.EnumTests\$Bras" + } + } + + val factory = SerializerFactory(WL(), ClassLoader.getSystemClassLoader()) + + // if it all works, this won't explode + TestSerializationOutput(VERBOSE, factory).serialize(C(Bras.UNDERWIRE)) + } + + @Test + fun enumAnnotated() { + @CordaSerializable data class C (val c: AnnotatedBras) + + class WL : ClassWhitelist { + override fun hasListed(type: Class<*>) = false + } + + val factory = SerializerFactory(WL(), ClassLoader.getSystemClassLoader()) + + // if it all works, this won't explode + TestSerializationOutput(VERBOSE, factory).serialize(C(AnnotatedBras.UNDERWIRE)) + } + + @Test + fun deserializeNonWhitlistedEnum() { + data class C (val c: Bras) + + class WL (val allowed: List) : ClassWhitelist { + override fun hasListed(type: Class<*>) = type.name in allowed + } + + // first serialise the class using a context in which Bras are whitelisted + val factory = SerializerFactory(WL(listOf (classTestName("C"), + "net.corda.nodeapi.internal.serialization.amqp.EnumTests\$Bras")), + ClassLoader.getSystemClassLoader()) + val bytes = TestSerializationOutput(VERBOSE, factory).serialize(C(Bras.UNDERWIRE)) + + // then take that serialised object and attempt to deserialize it in a context that + // disallows the Bras enum + val factory2 = SerializerFactory(WL(listOf (classTestName("C"))), ClassLoader.getSystemClassLoader()) + Assertions.assertThatThrownBy { + DeserializationInput(factory2).deserialize(bytes) + }.isInstanceOf(NotSerializableException::class.java) + } } \ No newline at end of file From 334164aa8605d79cfef2831cc484dcf0c7a5d16a Mon Sep 17 00:00:00 2001 From: Clinton Date: Wed, 27 Sep 2017 18:34:17 +0100 Subject: [PATCH 028/185] Fixed several bugs in the contract constraints work (#1695) * Added schedulable flows to cordapp scanning * Fixed a bug where the core flows are included in every cordapp. * Added a test to prove the scheduled flows are loaded correctly. * Enabled a negative test to prove that we are not currently dynamically loading attachment classes from the network. --- .idea/compiler.xml | 2 + .../kotlin/net/corda/core/cordapp/Cordapp.kt | 2 + .../core/internal/cordapp/CordappImpl.kt | 1 + .../core/transactions/WireTransaction.kt | 3 +- .../node/services/AttachmentLoadingTests.kt | 17 +-- .../net/corda/node/internal/AbstractNode.kt | 7 +- .../kotlin/net/corda/node/internal/Node.kt | 10 +- .../node/internal/cordapp/CordappLoader.kt | 98 +++++++++++++----- .../internal/cordapp/CordappProviderImpl.kt | 2 +- .../node/services/messaging/RPCServer.kt | 2 +- .../corda/node/cordapp/CordappLoaderTest.kt | 54 ---------- .../internal/cordapp/CordappLoaderTest.kt | 89 ++++++++++++++++ .../cordapp/CordappProviderImplTests.kt | 4 +- .../node/{ => internal}/cordapp/empty.jar | Bin .../node/{ => internal}/cordapp/isolated.jar | Bin .../corda/testing/node/MockCordappProvider.kt | 2 +- 16 files changed, 193 insertions(+), 100 deletions(-) delete mode 100644 node/src/test/kotlin/net/corda/node/cordapp/CordappLoaderTest.kt create mode 100644 node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt rename node/src/test/kotlin/net/corda/node/{ => internal}/cordapp/CordappProviderImplTests.kt (94%) rename node/src/test/resources/net/corda/node/{ => internal}/cordapp/empty.jar (100%) rename node/src/test/resources/net/corda/node/{ => internal}/cordapp/isolated.jar (100%) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index f0b65c5a84..2d6467076b 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -41,6 +41,8 @@ + + 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 2df896cbbe..ea9557d7c9 100644 --- a/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt +++ b/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt @@ -17,6 +17,7 @@ import java.net.URL * @property contractClassNames List of contracts * @property initiatedFlows List of initiatable flow classes * @property rpcFlows List of RPC initiable flows classes + * @property schedulableFlows List of flows startable by the scheduler * @property servies List of RPC services * @property plugins List of Corda plugin registries * @property customSchemas List of custom schemas @@ -27,6 +28,7 @@ interface Cordapp { val contractClassNames: List val initiatedFlows: List>> val rpcFlows: List>> + val schedulableFlows: List>> val services: List> val plugins: List val customSchemas: Set 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 38a5a0ecf4..8cfda77d42 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 @@ -12,6 +12,7 @@ data class CordappImpl( override val contractClassNames: List, override val initiatedFlows: List>>, override val rpcFlows: List>>, + override val schedulableFlows: List>>, override val services: List>, override val plugins: List, override val customSchemas: Set, diff --git a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt index 39de4816a9..3bba0b8ae6 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -114,7 +114,8 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr } // Open attachments specified in this transaction. If we haven't downloaded them, we fail. val contractAttachments = findAttachmentContracts(resolvedInputs, resolveContractAttachment, resolveAttachment) - val attachments = contractAttachments + (attachments.map { resolveAttachment(it) ?: throw AttachmentResolutionException(it) }).distinct() + // Order of attachments is important since contracts may refer to indexes so only append automatic attachments + val attachments = (attachments.map { resolveAttachment(it) ?: throw AttachmentResolutionException(it) } + contractAttachments).distinct() return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, timeWindow, privacySalt) } 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 03b9a9435e..4a344aec4e 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 @@ -1,5 +1,6 @@ package net.corda.node.services +import net.corda.client.rpc.RPCException import net.corda.core.contracts.Contract import net.corda.core.contracts.PartyAndReference import net.corda.core.cordapp.CordappProvider @@ -13,6 +14,7 @@ import net.corda.core.serialization.SerializationFactory import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.loggerFor import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.nodeapi.User @@ -21,6 +23,7 @@ import net.corda.testing.DUMMY_NOTARY import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.driver.driver import net.corda.testing.node.MockServices +import net.corda.testing.resetTestSerialization import org.junit.Assert.* import org.junit.Before import org.junit.Test @@ -37,9 +40,10 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() { override val cordappProvider: CordappProvider = provider } - companion object { - private val isolatedJAR = this::class.java.getResource("isolated.jar")!! - private val ISOLATED_CONTRACT_ID = "net.corda.finance.contracts.isolated.AnotherDummyContract" + private companion object { + val logger = loggerFor() + val isolatedJAR = AttachmentLoadingTests::class.java.getResource("isolated.jar")!! + val ISOLATED_CONTRACT_ID = "net.corda.finance.contracts.isolated.AnotherDummyContract" } private lateinit var services: Services @@ -67,19 +71,20 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() { assertEquals(expected, actual) } - // TODO - activate this test - // @Test + @Test fun `test that attachments retrieved over the network are not used for code`() { driver(initialiseSerialization = false) { val bankAName = CordaX500Name("BankA", "Zurich", "CH") val bankBName = CordaX500Name("BankB", "Zurich", "CH") // Copy the app jar to the first node. The second won't have it. val path = (baseDirectory(bankAName.toString()) / "plugins").createDirectories() / "isolated.jar" + logger.info("Installing isolated jar to $path") isolatedJAR.openStream().buffered().use { input -> Files.newOutputStream(path).buffered().use { output -> input.copyTo(output) } } + val admin = User("admin", "admin", permissions = setOf("ALL")) val (bankA, bankB) = listOf( startNode(providedName = bankAName, rpcUsers = listOf(admin)), @@ -95,7 +100,7 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() { val proxy = rpc.proxy val party = proxy.wellKnownPartyFromX500Name(bankBName)!! - assertFailsWith("xxx") { + assertFailsWith("net.corda.client.rpc.RPCException: net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator") { proxy.startFlowDynamic(clazz, party).returnValue.getOrThrow() } } 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 6ba72a75d6..ec5034ccdb 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -147,6 +147,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, protected lateinit var database: CordaPersistence protected var dbCloser: (() -> Any?)? = null lateinit var cordappProvider: CordappProviderImpl + protected val cordappLoader by lazy { makeCordappLoader() } protected val _nodeReadyFuture = openFuture() /** Completes once the node has successfully registered with the network map service @@ -378,7 +379,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, */ private fun makeServices(): MutableList { checkpointStorage = DBCheckpointStorage() - cordappProvider = CordappProviderImpl(makeCordappLoader()) + cordappProvider = CordappProviderImpl(cordappLoader) _services = ServiceHubInternalImpl() attachments = NodeAttachmentService(services.monitoringService.metrics) cordappProvider.start(attachments) @@ -399,10 +400,10 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val scanPackages = System.getProperty("net.corda.node.cordapp.scan.packages") return if (CordappLoader.testPackages.isNotEmpty()) { check(configuration.devMode) { "Package scanning can only occur in dev mode" } - CordappLoader.createWithTestPackages(CordappLoader.testPackages) + CordappLoader.createDefaultWithTestPackages(configuration.baseDirectory, CordappLoader.testPackages) } else if (scanPackages != null) { check(configuration.devMode) { "Package scanning can only occur in dev mode" } - CordappLoader.createWithTestPackages(scanPackages.split(",")) + CordappLoader.createDefaultWithTestPackages(configuration.baseDirectory, scanPackages.split(",")) } else { CordappLoader.createDefault(configuration.baseDirectory) } 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 bc4dcb386f..17660cfe37 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -14,6 +14,7 @@ import net.corda.core.node.ServiceHub import net.corda.core.serialization.SerializationDefaults import net.corda.core.utilities.* import net.corda.node.VersionInfo +import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.node.serialization.KryoServerSerializationScheme import net.corda.node.serialization.NodeClock import net.corda.node.services.RPCUserService @@ -340,14 +341,15 @@ open class Node(override val configuration: FullNodeConfiguration, } private fun initialiseSerialization() { + val classloader = cordappLoader.appClassLoader SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) registerScheme(AMQPServerSerializationScheme()) } - SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT - SerializationDefaults.RPC_SERVER_CONTEXT = KRYO_RPC_SERVER_CONTEXT - SerializationDefaults.STORAGE_CONTEXT = KRYO_STORAGE_CONTEXT - SerializationDefaults.CHECKPOINT_CONTEXT = KRYO_CHECKPOINT_CONTEXT + SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT.withClassLoader(classloader) + SerializationDefaults.RPC_SERVER_CONTEXT = KRYO_RPC_SERVER_CONTEXT.withClassLoader(classloader) + SerializationDefaults.STORAGE_CONTEXT = KRYO_STORAGE_CONTEXT.withClassLoader(classloader) + SerializationDefaults.CHECKPOINT_CONTEXT = KRYO_CHECKPOINT_CONTEXT.withClassLoader(classloader) } /** Starts a blocking event loop for message dispatch. */ diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt index f29fe98ed4..bb98afbe43 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt @@ -5,10 +5,7 @@ import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult import net.corda.core.contracts.Contract import net.corda.core.contracts.UpgradedContract import net.corda.core.cordapp.Cordapp -import net.corda.core.flows.ContractUpgradeFlow -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.InitiatedBy -import net.corda.core.flows.StartableByRPC +import net.corda.core.flows.* import net.corda.core.internal.* import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.node.CordaPluginRegistry @@ -40,10 +37,17 @@ import kotlin.streams.toList * @property cordappJarPaths The classpath of cordapp JARs */ class CordappLoader private constructor(private val cordappJarPaths: List) { - val cordapps: List by lazy { loadCordapps() } + val cordapps: List by lazy { loadCordapps() + coreCordapp } - @VisibleForTesting - internal val appClassLoader: ClassLoader = javaClass.classLoader + internal val appClassLoader: ClassLoader = URLClassLoader(cordappJarPaths.toTypedArray(), javaClass.classLoader) + + init { + if (cordappJarPaths.isEmpty()) { + logger.info("No CorDapp paths provided") + } else { + logger.info("Loading CorDapps from ${cordappJarPaths.joinToString()}") + } + } companion object { private val logger = loggerFor() @@ -54,19 +58,31 @@ class CordappLoader private constructor(private val cordappJarPaths: List) * @param baseDir The directory that this node is running in. Will use this to resolve the plugins directory * for classpath scanning. */ - fun createDefault(baseDir: Path): CordappLoader { - val pluginsDir = getPluginsPath(baseDir) - return CordappLoader(if (!pluginsDir.exists()) emptyList() else pluginsDir.list { - it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.map { it.toUri().toURL() }.toList() - }) - } - - fun getPluginsPath(baseDir: Path): Path = baseDir / "plugins" + fun createDefault(baseDir: Path) = CordappLoader(getCordappsInDirectory(getPluginsPath(baseDir))) /** - * Create a dev mode CordappLoader for test environments + * Create a dev mode CordappLoader for test environments that creates and loads cordapps from the classpath + * and plugins directory. This is intended mostly for use by the driver. + * + * @param baseDir See [createDefault.baseDir] + * @param testPackages See [createWithTestPackages.testPackages] */ - fun createWithTestPackages(testPackages: List = CordappLoader.testPackages) = CordappLoader(testPackages.flatMap(this::createScanPackage)) + @VisibleForTesting + @JvmOverloads + fun createDefaultWithTestPackages(baseDir: Path, testPackages: List = CordappLoader.testPackages) + = CordappLoader(getCordappsInDirectory(getPluginsPath(baseDir)) + testPackages.flatMap(this::createScanPackage)) + + /** + * Create a dev mode CordappLoader for test environments that creates and loads cordapps from the classpath. + * This is intended for use in unit and integration tests. + * + * @param testPackages List of package names that contain CorDapp classes that can be automatically turned into + * CorDapps. + */ + @VisibleForTesting + @JvmOverloads + fun createWithTestPackages(testPackages: List = CordappLoader.testPackages) + = CordappLoader(testPackages.flatMap(this::createScanPackage)) /** * Creates a dev mode CordappLoader intended only to be used in test environments @@ -76,6 +92,8 @@ class CordappLoader private constructor(private val cordappJarPaths: List) @VisibleForTesting fun createDevMode(scanJars: List) = CordappLoader(scanJars) + private fun getPluginsPath(baseDir: Path): Path = baseDir / "plugins" + private fun createScanPackage(scanPackage: String): List { val resource = scanPackage.replace('.', '/') return this::class.java.classLoader.getResources(resource) @@ -90,7 +108,7 @@ class CordappLoader private constructor(private val cordappJarPaths: List) .toList() } - /** Takes a package of classes and creates a JAR from them - only use in tests */ + /** Takes a package of classes and creates a JAR from them - only use in tests. */ private fun createDevCordappJar(scanPackage: String, path: URL, jarPackageName: String): URI { if(!generatedCordapps.contains(path)) { val cordappDir = File("build/tmp/generated-test-cordapps") @@ -118,12 +136,41 @@ class CordappLoader private constructor(private val cordappJarPaths: List) return generatedCordapps[path]!! } + private fun getCordappsInDirectory(pluginsDir: Path): List { + return if (!pluginsDir.exists()) { + emptyList() + } else { + pluginsDir.list { + it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.map { it.toUri().toURL() }.toList() + } + } + } + /** - * A list of test packages that will be scanned as CorDapps and compiled into CorDapp JARs for use in tests only + * A list of test packages that will be scanned as CorDapps and compiled into CorDapp JARs for use in tests only. */ @VisibleForTesting var testPackages: List = emptyList() private val generatedCordapps = mutableMapOf() + + /** A list of the core RPC flows present in Corda */ + private val coreRPCFlows = listOf( + 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( + listOf(), + listOf(), + coreRPCFlows, + listOf(), + listOf(), + listOf(), + setOf(), + ContractUpgradeFlow.javaClass.protectionDomain.codeSource.location // Core JAR location + ) } private fun loadCordapps(): List { @@ -132,6 +179,7 @@ class CordappLoader private constructor(private val cordappJarPaths: List) CordappImpl(findContractClassNames(scanResult), findInitiatedFlows(scanResult), findRPCFlows(scanResult), + findSchedulableFlows(scanResult), findServices(scanResult), findPlugins(it), findCustomSchemas(scanResult), @@ -163,13 +211,11 @@ class CordappLoader private constructor(private val cordappJarPaths: List) return Modifier.isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || Modifier.isStatic(modifiers)) } - val found = scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class).filter { it.isUserInvokable() } - val coreFlows = listOf( - ContractUpgradeFlow.Initiate::class.java, - ContractUpgradeFlow.Authorise::class.java, - ContractUpgradeFlow.Deauthorise::class.java - ) - return found + coreFlows + return scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class).filter { it.isUserInvokable() } + } + + private fun findSchedulableFlows(scanResult: ScanResult): List>> { + return scanResult.getClassesWithAnnotation(FlowLogic::class, SchedulableFlow::class) } private fun findContractClassNames(scanResult: ScanResult): List { 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 3eb485787f..9c3f58293f 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 @@ -66,7 +66,7 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader) : Singl * @return A cordapp context for the given CorDapp */ fun getAppContext(cordapp: Cordapp): CordappContext { - return CordappContext(cordapp, getCordappAttachmentId(cordapp), URLClassLoader(arrayOf(cordapp.jarPath), cordappLoader.appClassLoader)) + return CordappContext(cordapp, getCordappAttachmentId(cordapp), cordappLoader.appClassLoader) } /** 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 70761cfc4e..ad2eb239fa 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 @@ -459,4 +459,4 @@ object RpcServerObservableSerializer : Serializer>() { observableContext.clientAddressToObservables.put(observableContext.clientAddress, observableId) observableContext.observableMap.put(observableId, observableWithSubscription) } -} \ No newline at end of file +} diff --git a/node/src/test/kotlin/net/corda/node/cordapp/CordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/cordapp/CordappLoaderTest.kt deleted file mode 100644 index aadf3ee066..0000000000 --- a/node/src/test/kotlin/net/corda/node/cordapp/CordappLoaderTest.kt +++ /dev/null @@ -1,54 +0,0 @@ -package net.corda.node.cordapp - -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.InitiatedBy -import net.corda.core.flows.InitiatingFlow -import net.corda.node.internal.cordapp.CordappLoader -import org.assertj.core.api.Assertions.assertThat -import org.junit.Test -import java.nio.file.Paths - -@InitiatingFlow -class DummyFlow : FlowLogic() { - override fun call() { } -} - -@InitiatedBy(DummyFlow::class) -class LoaderTestFlow : FlowLogic() { - override fun call() { } -} - -class CordappLoaderTest { - @Test - 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 = CordappLoader.createDefault(Paths.get(".")) - assertThat(loader.cordapps).isEmpty() - } - - @Test - fun `test that classes that are in a cordapp are loaded`() { - val loader = CordappLoader.createWithTestPackages(listOf("net.corda.node.cordapp")) - val initiatedFlows = loader.cordapps.first().initiatedFlows - val expectedClass = loader.appClassLoader.loadClass("net.corda.node.cordapp.LoaderTestFlow").asSubclass(FlowLogic::class.java) - assertThat(initiatedFlows).contains(expectedClass) - } - - @Test - fun `isolated JAR contains a CorDapp with a contract and plugin`() { - val isolatedJAR = CordappLoaderTest::class.java.getResource("isolated.jar")!! - val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) - - val actual = loader.cordapps.toTypedArray() - assertThat(actual).hasSize(1) - - val actualCordapp = actual.first() - assertThat(actualCordapp.contractClassNames).isEqualTo(listOf("net.corda.finance.contracts.isolated.AnotherDummyContract")) - assertThat(actualCordapp.initiatedFlows).isEmpty() - assertThat(actualCordapp.rpcFlows).contains(loader.appClassLoader.loadClass("net.corda.core.flows.ContractUpgradeFlow\$Initiate").asSubclass(FlowLogic::class.java)) - assertThat(actualCordapp.services).isEmpty() - assertThat(actualCordapp.plugins).hasSize(1) - assertThat(actualCordapp.plugins.first().javaClass.name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedPlugin") - assertThat(actualCordapp.jarPath).isEqualTo(isolatedJAR) - } -} diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt new file mode 100644 index 0000000000..806b7ac929 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt @@ -0,0 +1,89 @@ +package net.corda.node.internal.cordapp + +import net.corda.core.flows.* +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import java.nio.file.Paths + +@InitiatingFlow +class DummyFlow : FlowLogic() { + override fun call() { } +} + +@InitiatedBy(DummyFlow::class) +class LoaderTestFlow(unusedSession: FlowSession) : FlowLogic() { + override fun call() { } +} + +@SchedulableFlow +class DummySchedulableFlow : FlowLogic() { + override fun call() { } +} + +@StartableByRPC +class DummyRPCFlow : FlowLogic() { + override fun call() { } +} + +class CordappLoaderTest { + private companion object { + val testScanPackages = listOf("net.corda.node.internal.cordapp") + val isolatedContractId = "net.corda.finance.contracts.isolated.AnotherDummyContract" + val isolatedFlowName = "net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator" + } + + @Test + 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 = CordappLoader.createDefault(Paths.get(".")) + assertThat(loader.cordapps) + .hasSize(1) + .contains(CordappLoader.coreCordapp) + } + + @Test + fun `isolated JAR contains a CorDapp with a contract and plugin`() { + val isolatedJAR = CordappLoaderTest::class.java.getResource("isolated.jar")!! + val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) + + val actual = loader.cordapps.toTypedArray() + assertThat(actual).hasSize(2) + + val actualCordapp = actual.single { it != CordappLoader.coreCordapp } + assertThat(actualCordapp.contractClassNames).isEqualTo(listOf(isolatedContractId)) + assertThat(actualCordapp.initiatedFlows.single().name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Acceptor") + assertThat(actualCordapp.rpcFlows).isEmpty() + assertThat(actualCordapp.schedulableFlows).isEmpty() + assertThat(actualCordapp.services).isEmpty() + assertThat(actualCordapp.plugins).hasSize(1) + assertThat(actualCordapp.plugins.first().javaClass.name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedPlugin") + assertThat(actualCordapp.jarPath).isEqualTo(isolatedJAR) + } + + @Test + fun `flows are loaded by loader`() { + val loader = CordappLoader.createWithTestPackages(testScanPackages) + + val actual = loader.cordapps.toTypedArray() + // One core cordapp, one cordapp from this source tree, and two others due to identically named locations + // in resources and the non-test part of node. This is okay due to this being test code. In production this + // cannot happen. In gradle it will also pick up the node jar. + assertThat(actual.size == 4 || actual.size == 5).isTrue() + + val actualCordapp = actual.single { !it.initiatedFlows.isEmpty() } + assertThat(actualCordapp.initiatedFlows).first().hasSameClassAs(DummyFlow::class.java) + assertThat(actualCordapp.rpcFlows).first().hasSameClassAs(DummyRPCFlow::class.java) + assertThat(actualCordapp.schedulableFlows).first().hasSameClassAs(DummySchedulableFlow::class.java) + } + + // This test exists because the appClassLoader is used by serialisation and we need to ensure it is the classloader + // being used internally. Later iterations will use a classloader per cordapp and this test can be retired. + @Test + fun `cordapp classloader can load cordapp classes`() { + val isolatedJAR = CordappLoaderTest::class.java.getResource("isolated.jar")!! + val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) + + loader.appClassLoader.loadClass(isolatedContractId) + loader.appClassLoader.loadClass(isolatedFlowName) + } +} diff --git a/node/src/test/kotlin/net/corda/node/cordapp/CordappProviderImplTests.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt similarity index 94% rename from node/src/test/kotlin/net/corda/node/cordapp/CordappProviderImplTests.kt rename to node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt index 762761d99e..10dcd4a3ad 100644 --- a/node/src/test/kotlin/net/corda/node/cordapp/CordappProviderImplTests.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt @@ -1,8 +1,6 @@ -package net.corda.node.cordapp +package net.corda.node.internal.cordapp import net.corda.core.node.services.AttachmentStorage -import net.corda.node.internal.cordapp.CordappLoader -import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.testing.node.MockAttachmentStorage import org.junit.Assert import org.junit.Before diff --git a/node/src/test/resources/net/corda/node/cordapp/empty.jar b/node/src/test/resources/net/corda/node/internal/cordapp/empty.jar similarity index 100% rename from node/src/test/resources/net/corda/node/cordapp/empty.jar rename to node/src/test/resources/net/corda/node/internal/cordapp/empty.jar diff --git a/node/src/test/resources/net/corda/node/cordapp/isolated.jar b/node/src/test/resources/net/corda/node/internal/cordapp/isolated.jar similarity index 100% rename from node/src/test/resources/net/corda/node/cordapp/isolated.jar rename to node/src/test/resources/net/corda/node/internal/cordapp/isolated.jar diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt index a0ca53fe0e..f51d59d20d 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt @@ -14,7 +14,7 @@ class MockCordappProvider(cordappLoader: CordappLoader) : CordappProviderImpl(co val cordappRegistry = mutableListOf>() fun addMockCordapp(contractClassName: ContractClassName, services: ServiceHub) { - val cordapp = CordappImpl(listOf(contractClassName), emptyList(), emptyList(), emptyList(), emptyList(), emptySet(), Paths.get(".").toUri().toURL()) + val cordapp = CordappImpl(listOf(contractClassName), emptyList(), emptyList(), emptyList(), emptyList(), emptyList(), emptySet(), Paths.get(".").toUri().toURL()) if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) }) { cordappRegistry.add(Pair(cordapp, findOrImportAttachment(contractClassName.toByteArray(), services))) } From f59b22ba9808d8dd5874fff57e40979e18eb04a5 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Mon, 25 Sep 2017 20:37:19 +0100 Subject: [PATCH 029/185] CORDA-601 - Carpenter should respect whitelist The class carpenter should refuse to carpent classes that are not whitelisted or marked as CordaSerializable. This prevents any security issue where a malicious message could indicate a class had a member of some type that on construction did something bad. By respecting the whitelist we avoid this. As the carpeter annotates anythign it constructs as CordaSerializable, it will always be able to carpent classes that contain memebrs that were unknown, and thus unannotated, carpented classes --- .../serialization/amqp/SerializationHelper.kt | 26 ++++- .../serialization/amqp/SerializerFactory.kt | 23 +--- .../serialization/carpenter/ClassCarpenter.kt | 37 +++++-- .../serialization/carpenter/Schema.kt | 22 +++- .../carpenter/ClassCarpenterWhitelistTest.kt | 100 ++++++++++++++++++ 5 files changed, 174 insertions(+), 34 deletions(-) create mode 100644 node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterWhitelistTest.kt diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt index 45df43799e..1a56cf901b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt @@ -1,5 +1,7 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.serialization.ClassWhitelist +import net.corda.core.serialization.CordaSerializable import com.google.common.primitives.Primitives import com.google.common.reflect.TypeToken import net.corda.core.serialization.SerializationContext @@ -51,7 +53,7 @@ internal fun constructorForDeserialization(type: Type): KFunction? { } } - return preferredCandidate?.apply { isAccessible = true} + return preferredCandidate?.apply { isAccessible = true } ?: throw NotSerializableException("No constructor for deserialization found for $clazz.") } else { return null @@ -81,7 +83,7 @@ private fun propertiesForSerializationFromConstructor(kotlinConstructo val name = param.name ?: throw NotSerializableException("Constructor parameter of $clazz has no name.") val matchingProperty = properties[name] ?: throw NotSerializableException("No property matching constructor parameter named $name of $clazz." + - " If using Java, check that you have the -parameters option specified in the Java compiler.") + " If using Java, check that you have the -parameters option specified in the Java compiler.") // Check that the method has a getter in java. val getter = matchingProperty.readMethod ?: throw NotSerializableException("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.") @@ -123,7 +125,7 @@ private fun exploreType(type: Type?, interfaces: MutableSet, serializerFac val clazz = type?.asClass() if (clazz != null) { if (clazz.isInterface) { - if(serializerFactory.isNotWhitelisted(clazz)) return // We stop exploring once we reach a branch that has no `CordaSerializable` annotation or whitelisting. + 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) { @@ -263,3 +265,21 @@ private fun Throwable.setMessage(newMsg: String) { detailMessageField.isAccessible = true detailMessageField.set(this, newMsg) } + +fun ClassWhitelist.whitelisted(type: Type) { + val clazz = type.asClass()!! + if (isNotWhitelisted(clazz)) { + throw NotSerializableException("Class $type is not on the whitelist or annotated with @CordaSerializable.") + } +} + +// Ignore SimpleFieldAccess as we add it to anything we build in the carpenter. +fun ClassWhitelist.isNotWhitelisted(clazz: Class<*>) = + !(hasListed(clazz) || hasAnnotationInHierarchy(clazz)) + +// Recursively check the class, interfaces and superclasses for our annotation. +fun ClassWhitelist.hasAnnotationInHierarchy(type: Class<*>): Boolean { + return type.isAnnotationPresent(CordaSerializable::class.java) + || type.interfaces.any { hasAnnotationInHierarchy(it) } + || (type.superclass != null && hasAnnotationInHierarchy(type.superclass)) +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt index 2f0a734800..65083a6150 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt @@ -4,7 +4,6 @@ import com.google.common.primitives.Primitives import com.google.common.reflect.TypeResolver import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.ClassWhitelist -import net.corda.core.serialization.CordaSerializable import net.corda.nodeapi.internal.serialization.carpenter.* import org.apache.qpid.proton.amqp.* import java.io.NotSerializableException @@ -243,10 +242,10 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) { if (clazz.componentType.isPrimitive) PrimArraySerializer.make(type, this) else ArraySerializer.make(type, this) } else if (clazz.kotlin.objectInstance != null) { - whitelisted(clazz) + whitelist.whitelisted(clazz) SingletonSerializer(clazz, clazz.kotlin.objectInstance!!, this) } else { - whitelisted(type) + whitelist.whitelisted(type) ObjectSerializer(type, this) } } @@ -271,24 +270,6 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) { return null } - private fun whitelisted(type: Type) { - val clazz = type.asClass()!! - if (isNotWhitelisted(clazz)) { - throw NotSerializableException("Class $type is not on the whitelist or annotated with @CordaSerializable.") - } - } - - // Ignore SimpleFieldAccess as we add it to anything we build in the carpenter. - internal fun isNotWhitelisted(clazz: Class<*>): Boolean = clazz == SimpleFieldAccess::class.java || - (!whitelist.hasListed(clazz) && !hasAnnotationInHierarchy(clazz)) - - // Recursively check the class, interfaces and superclasses for our annotation. - private fun hasAnnotationInHierarchy(type: Class<*>): Boolean { - return type.isAnnotationPresent(CordaSerializable::class.java) || - type.interfaces.any { hasAnnotationInHierarchy(it) } - || (type.superclass != null && hasAnnotationInHierarchy(type.superclass)) - } - private fun makeMapSerializer(declaredType: ParameterizedType): AMQPSerializer { val rawType = declaredType.rawType as Class<*> rawType.checkSupportedMapType() diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt index 9f727e09c7..7a0dee0703 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt @@ -2,14 +2,13 @@ package net.corda.nodeapi.internal.serialization.carpenter import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.CordaSerializable +import net.corda.nodeapi.internal.serialization.amqp.whitelisted import org.objectweb.asm.ClassWriter import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Opcodes.* import org.objectweb.asm.Type - import java.lang.Character.isJavaIdentifierPart import java.lang.Character.isJavaIdentifierStart - import java.util.* /** @@ -17,6 +16,7 @@ import java.util.* * as if `this.class.getMethod("get" + name.capitalize()).invoke(this)` had been called. It is intended as a more * convenient alternative to reflection. */ +@CordaSerializable interface SimpleFieldAccess { operator fun get(name: String): Any? } @@ -134,7 +134,10 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader visit(TARGET_VERSION, ACC_PUBLIC + ACC_FINAL + ACC_SUPER + ACC_ENUM, schema.jvmName, "L$jlEnum;", jlEnum, null) - visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd() + if (schema.flags.getOrDefault(SchemaFlags.NotCordaSerializable, "false") == true) { + visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd() + } + generateFields(schema) generateStaticEnumConstructor(schema) generateEnumConstructor() @@ -151,8 +154,10 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader cw.apply { visit(TARGET_VERSION, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, schema.jvmName, null, jlObject, interfaces) - visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd() + if (schema.flags.getOrDefault(SchemaFlags.NotCordaSerializable, "false") == true) { + visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd() + } generateAbstractGetters(schema) }.visitEnd() } @@ -163,20 +168,25 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader val superName = schema.superclass?.jvmName ?: jlObject val interfaces = schema.interfaces.map { it.name.jvm }.toMutableList() - if (SimpleFieldAccess::class.java !in schema.interfaces) { + if (SimpleFieldAccess::class.java !in schema.interfaces && ( + (schema.flags.getOrDefault(SchemaFlags.NotCordaSerializable, "false") == true) || + (schema.flags.getOrDefault(SchemaFlags.NoSimpleFieldAccess, "false") == true))) { interfaces.add(SimpleFieldAccess::class.java.name.jvm) } cw.apply { visit(TARGET_VERSION, ACC_PUBLIC + ACC_SUPER, schema.jvmName, null, superName, interfaces.toTypedArray()) - visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd() + if (schema.flags.getOrDefault(SchemaFlags.NotCordaSerializable, "false") == true) { + visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd() + } generateFields(schema) generateClassConstructor(schema) generateGetters(schema) - if (schema.superclass == null) + if (schema.superclass == null) { generateGetMethod() // From SimplePropertyAccess + } generateToString(schema) }.visitEnd() } @@ -388,11 +398,22 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader } } + /** + * If a sub element isn't whitelist we will not build a class containing that type as a member. Since, by + * default, classes created by the [ClassCarpenter] are annotated as [CordaSerializable] we will always + * be able to carpent classes generated from our AMQP library as, at a base level, we will either be able to + * create the lowest level in the meta hierarchy because either all members are jvm primitives or + * whitelisted classes + */ private fun validateSchema(schema: Schema) { if (schema.name in _loaded) throw DuplicateNameException() fun isJavaName(n: String) = n.isNotBlank() && isJavaIdentifierStart(n.first()) && n.all(::isJavaIdentifierPart) require(isJavaName(schema.name.split(".").last())) { "Not a valid Java name: ${schema.name}" } - schema.fields.keys.forEach { require(isJavaName(it)) { "Not a valid Java name: $it" } } + schema.fields.forEach { + require(isJavaName(it.key)) { "Not a valid Java name: $it" } + whitelist.whitelisted(it.value.field) + } + // Now check each interface we've been asked to implement, as the JVM will unfortunately only catch the // fact that we didn't implement the interface we said we would at the moment the missing method is // actually called, which is a bit too dynamic for my tastes. diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Schema.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Schema.kt index e6684756b8..bd98a4f2c7 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Schema.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Schema.kt @@ -1,9 +1,12 @@ package net.corda.nodeapi.internal.serialization.carpenter -import kotlin.collections.LinkedHashMap import org.objectweb.asm.ClassWriter import org.objectweb.asm.Opcodes.* +enum class SchemaFlags { + NoSimpleFieldAccess, NotCordaSerializable +} + /** * A Schema is the representation of an object the Carpenter can contsruct * @@ -17,7 +20,8 @@ abstract class Schema( var fields: Map, val superclass: Schema? = null, val interfaces: List> = emptyList(), - updater: (String, Field) -> Unit) { + updater: (String, Field) -> Unit, + var flags : MutableMap = mutableMapOf()) { private fun Map.descriptors() = LinkedHashMap(this.mapValues { it.value.descriptor }) init { @@ -41,6 +45,20 @@ abstract class Schema( val asArray: String get() = "[L$jvmName;" + + @Suppress("Unused") + fun setNoSimpleFieldAccess() { + flags.replace (SchemaFlags.NoSimpleFieldAccess, true) + } + + fun setNotCordaSerializable() { + flags.replace (SchemaFlags.NotCordaSerializable, true) + } + + @Suppress("Unused") + fun setCordaSerializable() { + flags.replace (SchemaFlags.NotCordaSerializable, false) + } } /** diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterWhitelistTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterWhitelistTest.kt new file mode 100644 index 0000000000..f2da9018ef --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterWhitelistTest.kt @@ -0,0 +1,100 @@ +package net.corda.nodeapi.internal.serialization.carpenter + +import net.corda.core.serialization.ClassWhitelist +import net.corda.core.serialization.CordaSerializable +import org.assertj.core.api.Assertions +import org.junit.Test +import java.io.NotSerializableException + +class ClassCarpenterWhitelistTest { + + // whitelisting a class on the class path will mean we will carpente up a class that + // contains it as a member + @Test + fun whitelisted() { + data class A(val a: Int) + + class WL : ClassWhitelist { + private val allowedClasses = hashSetOf( + A::class.java.name + ) + + override fun hasListed(type: Class<*>): Boolean = type.name in allowedClasses + } + + val cc = ClassCarpenter(whitelist = WL()) + + // if this works, the test works, if it throws then we're in a world of pain, we could + // go further but there are a lot of other tests that test weather we can build + // carpented objects + cc.build(ClassSchema("thing", mapOf("a" to NonNullableField(A::class.java)))) + } + + // However, a class on the class path that isn't whitelisted we will not create + // an object that contains a member of that type + @Test + fun notWhitelisted() { + data class A(val a: Int) + + class WL : ClassWhitelist { + override fun hasListed(type: Class<*>) = false + } + + val cc = ClassCarpenter(whitelist = WL()) + + // Class A isn't on the whitelist, so we should fail to carpent it + Assertions.assertThatThrownBy { + cc.build(ClassSchema("thing", mapOf("a" to NonNullableField(A::class.java)))) + }.isInstanceOf(NotSerializableException::class.java) + } + + // despite now being whitelisted and on the class path, we will carpent this because + // it's marked as CordaSerializable + @Test + fun notWhitelistedButAnnotated() { + @CordaSerializable data class A(val a: Int) + + class WL : ClassWhitelist { + override fun hasListed(type: Class<*>) = false + } + + val cc = ClassCarpenter(whitelist = WL()) + + // again, simply not throwing here is enough to show the test worked and the carpenter + // didn't reject the type even though it wasn't on the whitelist because it was + // annotated properly + cc.build(ClassSchema("thing", mapOf("a" to NonNullableField(A::class.java)))) + } + + @Test + fun notWhitelistedButCarpented() { + // just have the white list reject *Everything* except ints + class WL : ClassWhitelist { + override fun hasListed(type: Class<*>) = type.name == "int" + } + + val cc = ClassCarpenter(whitelist = WL()) + + val schema1a = ClassSchema("thing1a", mapOf("a" to NonNullableField(Int::class.java))) + + // thing 1 won't be set as corda serializable, meaning we won't build schema 2 + schema1a.setNotCordaSerializable() + + val clazz1a = cc.build(schema1a) + val schema2 = ClassSchema("thing2", mapOf("a" to NonNullableField(clazz1a))) + + // thing 2 references thing 1 which wasn't carpented as corda s erializable and thus + // this will fail + Assertions.assertThatThrownBy { + cc.build(schema2) + }.isInstanceOf(NotSerializableException::class.java) + + // create a second type of schema1, this time leave it as corda serialzable + val schema1b = ClassSchema("thing1b", mapOf("a" to NonNullableField(Int::class.java))) + + val clazz1b = cc.build(schema1b) + + // since schema 1b was created as CordaSerializable this will work + val schema2b = ClassSchema("thing2", mapOf("a" to NonNullableField(clazz1b))) + } +} \ No newline at end of file From cfcc5aad6776d3ba89479f401931855bdbaee178 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Wed, 27 Sep 2017 14:01:42 +0100 Subject: [PATCH 030/185] CORDA-601 - Review Comments So... On reflection, and adding a number of tests for static initialisation with serialised types it looks like there is no chance that the serializer factory will ever pass a non white-listed type through to the carpenter in the first place. As such leaving the plumbing in as it may be useful to pass a blacklist at some point into the carpenter and the tests are always useful (ignoring those that won't work without the white-list checking) --- .../serialization/CordaClassResolver.kt | 9 +- .../serialization/amqp/SerializationHelper.kt | 10 +- .../serialization/amqp/SerializerFactory.kt | 10 +- .../serialization/carpenter/ClassCarpenter.kt | 14 +- .../serialization/carpenter/Schema.kt | 26 ++-- ...ticInitialisationOfSerializedObjectTest.kt | 144 ++++++++++++++++++ .../carpenter/ClassCarpenterWhitelistTest.kt | 11 +- ...tionOfSerializedObjectTest.deserializeTest | Bin 0 -> 530 bytes ...ionOfSerializedObjectTest.deserializeTest2 | Bin 0 -> 531 bytes 9 files changed, 181 insertions(+), 43 deletions(-) create mode 100644 node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/StaticInitialisationOfSerializedObjectTest.kt create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/StaticInitialisationOfSerializedObjectTest.deserializeTest create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/StaticInitialisationOfSerializedObjectTest.deserializeTest2 diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt index 09983c8c68..45f98cbda9 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt @@ -11,6 +11,7 @@ import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializationContext import net.corda.core.utilities.loggerFor +import net.corda.nodeapi.internal.serialization.amqp.hasAnnotationInHierarchy import java.io.PrintWriter import java.lang.reflect.Modifier.isAbstract import java.nio.charset.StandardCharsets @@ -115,13 +116,7 @@ class CordaClassResolver(serializationContext: SerializationContext) : DefaultCl return (type.classLoader !is AttachmentsClassLoader) && !KryoSerializable::class.java.isAssignableFrom(type) && !type.isAnnotationPresent(DefaultSerializer::class.java) - && (type.isAnnotationPresent(CordaSerializable::class.java) || hasInheritedAnnotation(type)) - } - - // Recursively check interfaces for our annotation. - private fun hasInheritedAnnotation(type: Class<*>): Boolean { - return type.interfaces.any { it.isAnnotationPresent(CordaSerializable::class.java) || hasInheritedAnnotation(it) } - || (type.superclass != null && hasInheritedAnnotation(type.superclass)) + && (type.isAnnotationPresent(CordaSerializable::class.java) || whitelist.hasAnnotationInHierarchy(type)) } // Need to clear out class names from attachments. diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt index 1a56cf901b..ab218fb247 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt @@ -266,16 +266,14 @@ private fun Throwable.setMessage(newMsg: String) { detailMessageField.set(this, newMsg) } -fun ClassWhitelist.whitelisted(type: Type) { - val clazz = type.asClass()!! - if (isNotWhitelisted(clazz)) { +fun ClassWhitelist.requireWhitelisted(type: Type) { + if (!this.isWhitelisted(type.asClass()!!)) { throw NotSerializableException("Class $type is not on the whitelist or annotated with @CordaSerializable.") } } -// Ignore SimpleFieldAccess as we add it to anything we build in the carpenter. -fun ClassWhitelist.isNotWhitelisted(clazz: Class<*>) = - !(hasListed(clazz) || hasAnnotationInHierarchy(clazz)) +fun ClassWhitelist.isWhitelisted(clazz: Class<*>) = (hasListed(clazz) || hasAnnotationInHierarchy(clazz)) +fun ClassWhitelist.isNotWhitelisted(clazz: Class<*>) = !(this.isWhitelisted(clazz)) // Recursively check the class, interfaces and superclasses for our annotation. fun ClassWhitelist.hasAnnotationInHierarchy(type: Class<*>): Boolean { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt index 65083a6150..da8ae1afc6 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt @@ -30,11 +30,11 @@ data class FactorySchemaAndDescriptor(val schema: Schema, val typeDescriptor: An // TODO: need to rethink matching of constructor to properties in relation to implementing interfaces and needing those properties etc. // TODO: need to support super classes as well as interfaces with our current code base... what's involved? If we continue to ban, what is the impact? @ThreadSafe -class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) { +open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) { private val serializersByType = ConcurrentHashMap>() private val serializersByDescriptor = ConcurrentHashMap>() private val customSerializers = CopyOnWriteArrayList>() - val classCarpenter = ClassCarpenter(cl, whitelist) + open val classCarpenter = ClassCarpenter(cl, whitelist) val classloader: ClassLoader get() = classCarpenter.classloader @@ -81,7 +81,7 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) { } } Enum::class.java.isAssignableFrom(actualClass ?: declaredClass) -> serializersByType.computeIfAbsent(actualClass ?: declaredClass) { - whitelisted(actualType) + whitelist.requireWhitelisted(actualType) EnumSerializer(actualType, actualClass ?: declaredClass, this) } else -> makeClassSerializer(actualClass ?: declaredClass, actualType, declaredType) @@ -242,10 +242,10 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) { if (clazz.componentType.isPrimitive) PrimArraySerializer.make(type, this) else ArraySerializer.make(type, this) } else if (clazz.kotlin.objectInstance != null) { - whitelist.whitelisted(clazz) + whitelist.requireWhitelisted(clazz) SingletonSerializer(clazz, clazz.kotlin.objectInstance!!, this) } else { - whitelist.whitelisted(type) + whitelist.requireWhitelisted(type) ObjectSerializer(type, this) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt index 7a0dee0703..2c98ebbc07 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt @@ -2,7 +2,6 @@ package net.corda.nodeapi.internal.serialization.carpenter import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.CordaSerializable -import net.corda.nodeapi.internal.serialization.amqp.whitelisted import org.objectweb.asm.ClassWriter import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Opcodes.* @@ -134,7 +133,7 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader visit(TARGET_VERSION, ACC_PUBLIC + ACC_FINAL + ACC_SUPER + ACC_ENUM, schema.jvmName, "L$jlEnum;", jlEnum, null) - if (schema.flags.getOrDefault(SchemaFlags.NotCordaSerializable, "false") == true) { + if (schema.flags.cordaSerializable()) { visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd() } @@ -155,7 +154,7 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader visit(TARGET_VERSION, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, schema.jvmName, null, jlObject, interfaces) - if (schema.flags.getOrDefault(SchemaFlags.NotCordaSerializable, "false") == true) { + if (schema.flags.cordaSerializable()) { visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd() } generateAbstractGetters(schema) @@ -168,9 +167,9 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader val superName = schema.superclass?.jvmName ?: jlObject val interfaces = schema.interfaces.map { it.name.jvm }.toMutableList() - if (SimpleFieldAccess::class.java !in schema.interfaces && ( - (schema.flags.getOrDefault(SchemaFlags.NotCordaSerializable, "false") == true) || - (schema.flags.getOrDefault(SchemaFlags.NoSimpleFieldAccess, "false") == true))) { + if (SimpleFieldAccess::class.java !in schema.interfaces + && schema.flags.cordaSerializable() + && schema.flags.simpleFieldAccess()) { interfaces.add(SimpleFieldAccess::class.java.name.jvm) } @@ -178,7 +177,7 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader visit(TARGET_VERSION, ACC_PUBLIC + ACC_SUPER, schema.jvmName, null, superName, interfaces.toTypedArray()) - if (schema.flags.getOrDefault(SchemaFlags.NotCordaSerializable, "false") == true) { + if (schema.flags.cordaSerializable()) { visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd() } generateFields(schema) @@ -411,7 +410,6 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader require(isJavaName(schema.name.split(".").last())) { "Not a valid Java name: ${schema.name}" } schema.fields.forEach { require(isJavaName(it.key)) { "Not a valid Java name: $it" } - whitelist.whitelisted(it.value.field) } // Now check each interface we've been asked to implement, as the JVM will unfortunately only catch the diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Schema.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Schema.kt index bd98a4f2c7..e3a3bfab56 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Schema.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Schema.kt @@ -2,9 +2,10 @@ package net.corda.nodeapi.internal.serialization.carpenter import org.objectweb.asm.ClassWriter import org.objectweb.asm.Opcodes.* +import java.util.* enum class SchemaFlags { - NoSimpleFieldAccess, NotCordaSerializable + SimpleFieldAccess, CordaSerializable } /** @@ -20,10 +21,11 @@ abstract class Schema( var fields: Map, val superclass: Schema? = null, val interfaces: List> = emptyList(), - updater: (String, Field) -> Unit, - var flags : MutableMap = mutableMapOf()) { + updater: (String, Field) -> Unit) { private fun Map.descriptors() = LinkedHashMap(this.mapValues { it.value.descriptor }) + var flags : EnumMap = EnumMap(SchemaFlags::class.java) + init { fields.forEach { updater(it.key, it.value) } @@ -46,19 +48,17 @@ abstract class Schema( val asArray: String get() = "[L$jvmName;" - @Suppress("Unused") - fun setNoSimpleFieldAccess() { - flags.replace (SchemaFlags.NoSimpleFieldAccess, true) + fun unsetCordaSerializable() { + flags.replace (SchemaFlags.CordaSerializable, false) } +} - fun setNotCordaSerializable() { - flags.replace (SchemaFlags.NotCordaSerializable, true) - } +fun EnumMap.cordaSerializable() : Boolean { + return this.getOrDefault(SchemaFlags.CordaSerializable, true) == true +} - @Suppress("Unused") - fun setCordaSerializable() { - flags.replace (SchemaFlags.NotCordaSerializable, false) - } +fun EnumMap.simpleFieldAccess() : Boolean { + return this.getOrDefault(SchemaFlags.SimpleFieldAccess, true) == true } /** diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/StaticInitialisationOfSerializedObjectTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/StaticInitialisationOfSerializedObjectTest.kt new file mode 100644 index 0000000000..47decde9c0 --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/StaticInitialisationOfSerializedObjectTest.kt @@ -0,0 +1,144 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import net.corda.core.serialization.ClassWhitelist +import net.corda.core.serialization.SerializedBytes +import net.corda.nodeapi.internal.serialization.AllWhitelist +import net.corda.nodeapi.internal.serialization.carpenter.ClassCarpenter +import org.assertj.core.api.Assertions +import org.junit.Test +import java.io.File +import java.io.NotSerializableException +import java.lang.reflect.Type +import java.util.concurrent.ConcurrentHashMap +import kotlin.test.assertEquals + +class InStatic : Exception ("Help!, help!, I'm being repressed") + +class C { + companion object { + init { + throw InStatic() + } + } +} + +// To re-setup the resource file for the tests +// * deserializeTest +// * deserializeTest2 +// comment out the companion object from here, comment out the test code and uncomment +// the generation code, then re-run the test and copy the file shown in the output print +// to the resource directory +class C2 (var b: Int) { + /* + companion object { + init { + throw InStatic() + } + } + */ +} + +class StaticInitialisationOfSerializedObjectTest { + @Test(expected=java.lang.ExceptionInInitializerError::class) + fun itBlowsUp() { + C() + } + + @Test + fun KotlinObjectWithCompanionObject() { + data class D (val c : C) + + val sf = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + + val typeMap = sf::class.java.getDeclaredField("serializersByType") + typeMap.isAccessible = true + + @Suppress("UNCHECKED_CAST") + val serialisersByType = typeMap.get(sf) as ConcurrentHashMap> + + // pre building a serializer, we shouldn't have anything registered + assertEquals(0, serialisersByType.size) + + // build a serializer for type D without an instance of it to serialise, since + // 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 + // proves we didn't statically construct an instance of C when building the serializer + assertEquals(1, serialisersByType.size) + } + + + @Test + fun deserializeTest() { + data class D (val c : C2) + + val path = EvolvabilityTests::class.java.getResource("StaticInitialisationOfSerializedObjectTest.deserializeTest") + val f = File(path.toURI()) + + // Original version of the class for the serialised version of this class + // + //val sf1 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + //val sc = SerializationOutput(sf1).serialize(D(C2(20))) + //f.writeBytes(sc.bytes) + //println (path) + + class WL : ClassWhitelist { + override fun hasListed(type: Class<*>) = + type.name == "net.corda.nodeapi.internal.serialization.amqp" + + ".StaticInitialisationOfSerializedObjectTest\$deserializeTest\$D" + } + + val sf2 = SerializerFactory(WL(), ClassLoader.getSystemClassLoader()) + val bytes = f.readBytes() + + Assertions.assertThatThrownBy { + DeserializationInput(sf2).deserialize(SerializedBytes(bytes)) + }.isInstanceOf(NotSerializableException::class.java) + } + + // Version of a serializer factory that will allow the class carpenter living on the + // factory to have a different whitelist applied to it than the factory + class TestSerializerFactory(wl1: ClassWhitelist, wl2: ClassWhitelist) : + SerializerFactory (wl1, ClassLoader.getSystemClassLoader()) { + override val classCarpenter = ClassCarpenter(ClassLoader.getSystemClassLoader(), wl2) + } + + // This time have the serilization factory and the carpenter use different whitelists + @Test + fun deserializeTest2() { + data class D (val c : C2) + + val path = EvolvabilityTests::class.java.getResource("StaticInitialisationOfSerializedObjectTest.deserializeTest2") + val f = File(path.toURI()) + + // Original version of the class for the serialised version of this class + // + //val sf1 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + //val sc = SerializationOutput(sf1).serialize(D(C2(20))) + //f.writeBytes(sc.bytes) + //println (path) + + // whitelist to be used by the serialisation factory + class WL1 : ClassWhitelist { + override fun hasListed(type: Class<*>) = + type.name == "net.corda.nodeapi.internal.serialization.amqp" + + ".StaticInitialisationOfSerializedObjectTest\$deserializeTest\$D" + } + + // whitelist to be used by the carpenter + class WL2 : ClassWhitelist { + override fun hasListed(type: Class<*>) = true + } + + val sf2 = TestSerializerFactory(WL1(), WL2()) + val bytes = f.readBytes() + + // Deserializing should throw because C is not on the whitelist NOT because + // we ever went anywhere near statically constructing it prior to not actually + // creating an instance of it + Assertions.assertThatThrownBy { + DeserializationInput(sf2).deserialize(SerializedBytes(bytes)) + }.isInstanceOf(NotSerializableException::class.java) + } +} \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterWhitelistTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterWhitelistTest.kt index f2da9018ef..deb51da65f 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterWhitelistTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterWhitelistTest.kt @@ -3,6 +3,7 @@ package net.corda.nodeapi.internal.serialization.carpenter import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.CordaSerializable import org.assertj.core.api.Assertions +import org.junit.Ignore import org.junit.Test import java.io.NotSerializableException @@ -30,9 +31,9 @@ class ClassCarpenterWhitelistTest { cc.build(ClassSchema("thing", mapOf("a" to NonNullableField(A::class.java)))) } - // However, a class on the class path that isn't whitelisted we will not create - // an object that contains a member of that type @Test + @Ignore("Currently the carpenter doesn't inspect it's whitelist so will carpent anything" + + "it's asked relying on the serializer factory to not ask for anything") fun notWhitelisted() { data class A(val a: Int) @@ -67,6 +68,8 @@ class ClassCarpenterWhitelistTest { } @Test + @Ignore("Currently the carpenter doesn't inspect it's whitelist so will carpent anything" + + "it's asked relying on the serializer factory to not ask for anything") fun notWhitelistedButCarpented() { // just have the white list reject *Everything* except ints class WL : ClassWhitelist { @@ -78,7 +81,7 @@ class ClassCarpenterWhitelistTest { val schema1a = ClassSchema("thing1a", mapOf("a" to NonNullableField(Int::class.java))) // thing 1 won't be set as corda serializable, meaning we won't build schema 2 - schema1a.setNotCordaSerializable() + schema1a.unsetCordaSerializable() val clazz1a = cc.build(schema1a) val schema2 = ClassSchema("thing2", mapOf("a" to NonNullableField(clazz1a))) @@ -95,6 +98,6 @@ class ClassCarpenterWhitelistTest { val clazz1b = cc.build(schema1b) // since schema 1b was created as CordaSerializable this will work - val schema2b = ClassSchema("thing2", mapOf("a" to NonNullableField(clazz1b))) + ClassSchema("thing2", mapOf("a" to NonNullableField(clazz1b))) } } \ No newline at end of file diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/StaticInitialisationOfSerializedObjectTest.deserializeTest b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/StaticInitialisationOfSerializedObjectTest.deserializeTest new file mode 100644 index 0000000000000000000000000000000000000000..0ba0299a3f1df420ae32306cb098a9605c61585d GIT binary patch literal 530 zcmYe!FG@*dWME)u0Ahv%w-^{NFfcIw2C|tL7AxhYmgpseR9Ize2j==kg%?+ud8HdA z7L`|In*^8!+S(q_W<*g}Xqjo|<7Z}46>gAQQI+YLWR~LQk`7eI%orj9w}c5~$uyuP zVB31Zwlx4rkONo`yk=dP1-Dr*FFz$Uu^>|~Gp{7IC@(QbuQ;_RGchN#DzPLpKTj_) zx3EAjxCF>a_RPyH0m&4DW&G2Ep$bz|{FAa$lS@KUi%V2eQepa2!F(47SGcp752!H_ z<7tNj9*l5JEC;OF7cwR?Q~35Nr+$c`5PIzDM3 OGca^r7cv?ET?7DUk*x** literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/StaticInitialisationOfSerializedObjectTest.deserializeTest2 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/StaticInitialisationOfSerializedObjectTest.deserializeTest2 new file mode 100644 index 0000000000000000000000000000000000000000..6423985eafd8f7f5da84012386d5d7311182f876 GIT binary patch literal 531 zcmYe!FG@*dWME)u0Ahv%w-^{NFfcIw0kWAG7AxhYmgpseR9IgAQQI+YLWR~LQk`7eI%orj9w}c5~$#kG4 zVB7k@wlx4rkONo`ykT9K4Yye@FFz$Uu^>|~Gp{7IC@(QbuQ;_RGchN#DzPLpKTj_) zx3EAjxCF>a_RPyH0m&4DW&G2Ep$bz|{FAa$lS@KUi%V2eQepa2L3|?>7YA3kyO Date: Wed, 27 Sep 2017 23:49:20 +0100 Subject: [PATCH 031/185] Prevent the root project from building an unwanted dummy cordapp. (#1706) --- build.gradle | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0466b24455..97a02e8c18 100644 --- a/build.gradle +++ b/build.gradle @@ -175,7 +175,6 @@ repositories { } } -// TODO: Corda root project currently produces a dummy cordapp when it shouldn't. // Required for building out the fat JAR. dependencies { cordaCompile project(':node') @@ -196,6 +195,11 @@ dependencies { testCompile project(':test-utils') } +jar { + // Prevent the root project from building an unwanted dummy CorDapp. + enabled = false +} + task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) { dependsOn = subprojects.test additionalSourceDirs = files(subprojects.sourceSets.main.allSource.srcDirs) From c0dd8d338e53792138fed17ee4ab1953d76d6693 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Thu, 28 Sep 2017 10:06:10 +0100 Subject: [PATCH 032/185] CORDA-649: Improving logging (#1699) --- .../kotlin/net/corda/node/internal/Node.kt | 7 +++++- .../network/PersistentNetworkMapCache.kt | 8 +++++++ .../network/PersistentNetworkMapCacheTest.kt | 23 +++++++++++++------ 3 files changed, 30 insertions(+), 8 deletions(-) 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 17660cfe37..efb817f54a 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -47,6 +47,7 @@ import org.slf4j.LoggerFactory import java.io.IOException import java.time.Clock import java.util.* +import java.util.concurrent.atomic.AtomicInteger import javax.management.ObjectName import kotlin.system.exitProcess @@ -83,6 +84,8 @@ open class Node(override val configuration: FullNodeConfiguration, private fun createClock(configuration: FullNodeConfiguration): Clock { return if (configuration.useTestClock) TestClock() else NodeClock() } + + private val sameVmNodeCounter = AtomicInteger() } override val log: Logger get() = logger @@ -90,6 +93,8 @@ open class Node(override val configuration: FullNodeConfiguration, override val networkMapAddress: NetworkMapAddress? get() = configuration.networkMapService?.address?.let(::NetworkMapAddress) override fun makeTransactionVerifierService() = (network as NodeMessagingClient).verifierService + private val sameVmNodeNumber = sameVmNodeCounter.incrementAndGet() // Under normal (non-test execution) it will always be "1" + // DISCUSSION // // We use a single server thread for now, which means all message handling is serialized. @@ -127,7 +132,7 @@ open class Node(override val configuration: FullNodeConfiguration, // // The primary work done by the server thread is execution of flow logics, and related // serialisation/deserialisation work. - override val serverThread = AffinityExecutor.ServiceAffinityExecutor("Node thread", 1) + override val serverThread = AffinityExecutor.ServiceAffinityExecutor("Node thread-$sameVmNodeNumber", 1) private var messageBroker: ArtemisMessagingServer? = null 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 45e2b6f9b0..1105c4981c 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 @@ -157,23 +157,30 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) } override fun addNode(node: NodeInfo) { + logger.info("Adding node with info: $node") synchronized(_changed) { val previousNode = registeredNodes.put(node.legalIdentities.first().owningKey, node) // TODO hack... we left the first one as special one if (previousNode == null) { + logger.info("No previous node found") serviceHub.database.transaction { updateInfoDB(node) changePublisher.onNext(MapChange.Added(node)) } } else if (previousNode != node) { + logger.info("Previous node was found as: $previousNode") serviceHub.database.transaction { updateInfoDB(node) changePublisher.onNext(MapChange.Modified(node, previousNode)) } + } else { + logger.info("Previous node was identical to incoming one - doing nothing") } } + logger.info("Done adding node with info: $node") } override fun removeNode(node: NodeInfo) { + logger.info("Removing node with info: $node") synchronized(_changed) { registeredNodes.remove(node.legalIdentities.first().owningKey) serviceHub.database.transaction { @@ -181,6 +188,7 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) changePublisher.onNext(MapChange.Removed(node)) } } + logger.info("Done removing node with info: $node") } /** diff --git a/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt b/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt index da94d376aa..0d34b7d142 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt @@ -25,6 +25,10 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { val addressesMap: HashMap = HashMap() val infos: MutableSet = HashSet() + companion object { + val logger = loggerFor() + } + @Before fun start() { val nodes = startNodesWithPort(partiesList) @@ -127,10 +131,13 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { assertTrue(nms.info.chooseIdentity() in it.services.networkMapCache.allNodes.map { it.chooseIdentity() }) } charlie.internals.nodeReadyFuture.get() // Finish registration. + logger.info("Checking connectivity") checkConnectivity(listOf(otherNodes[0], nms)) // Checks connectivity from A to NMS. + logger.info("Loading caches") val cacheA = otherNodes[0].services.networkMapCache.allNodes val cacheB = otherNodes[1].services.networkMapCache.allNodes val cacheC = charlie.services.networkMapCache.allNodes + logger.info("Performing verification") assertEquals(4, cacheC.size) // Charlie fetched data from NetworkMap assertThat(cacheB).contains(charlie.info) assertEquals(cacheA.toSet(), cacheB.toSet()) @@ -158,9 +165,11 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { private fun checkConnectivity(nodes: List>) { nodes.forEach { node1 -> nodes.forEach { node2 -> - node2.internals.registerInitiatedFlow(SendBackFlow::class.java) - val resultFuture = node1.services.startFlow(SendFlow(node2.info.chooseIdentity())).resultFuture - assertThat(resultFuture.getOrThrow()).isEqualTo("Hello!") + if(!(node1 === node2)) { // Do not check connectivity to itself + node2.internals.registerInitiatedFlow(SendBackFlow::class.java) + val resultFuture = node1.services.startFlow(SendFlow(node2.info.chooseIdentity())).resultFuture + assertThat(resultFuture.getOrThrow()).isEqualTo("Hello!") + } } } } @@ -169,8 +178,8 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { private class SendFlow(val otherParty: Party) : FlowLogic() { @Suspendable override fun call(): String { - println("SEND FLOW to $otherParty") - println("Party key ${otherParty.owningKey.toBase58String()}") + logger.info("SEND FLOW to $otherParty") + logger.info("Party key ${otherParty.owningKey.toBase58String()}") val session = initiateFlow(otherParty) return session.sendAndReceive("Hi!").unwrap { it } } @@ -180,8 +189,8 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { private class SendBackFlow(val otherSideSession: FlowSession) : FlowLogic() { @Suspendable override fun call() { - println("SEND BACK FLOW to ${otherSideSession.counterparty}") - println("Party key ${otherSideSession.counterparty.owningKey.toBase58String()}") + logger.info("SEND BACK FLOW to ${otherSideSession.counterparty}") + logger.info("Party key ${otherSideSession.counterparty.owningKey.toBase58String()}") otherSideSession.send("Hello!") } } From f8a43a8331f8c6a07f37e411c482627d65935eb2 Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Wed, 27 Sep 2017 17:43:30 +0100 Subject: [PATCH 033/185] A temporary fix for contract upgrade transactions: during LedgerTransaction verification run the right logic based on whether it contains the UpgradeCommand. Move ContractUpgradeFlowTest away from createSomeNodes() Remove assembleBareTx as it's not used --- .../net/corda/core/contracts/Structures.kt | 3 -- .../corda/core/flows/ContractUpgradeFlow.kt | 46 +------------------ .../net/corda/core/internal/UpgradeCommand.kt | 7 +++ .../core/transactions/LedgerTransaction.kt | 29 +++++++++++- .../core/contracts/DummyContractV2Tests.kt | 1 + .../core/flows/ContractUpgradeFlowTest.kt | 15 ++---- .../corda/node/services/CoreFlowHandlers.kt | 6 +-- .../kotlin/net/corda/testing/node/MockNode.kt | 2 +- .../testing/contracts/DummyContractV2.kt | 3 +- 9 files changed, 44 insertions(+), 68 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/internal/UpgradeCommand.kt diff --git a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt index 9ce12b7293..e509b7da14 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt @@ -199,9 +199,6 @@ interface MoveCommand : CommandData { val contract: Class? } -/** Indicates that this transaction replaces the inputs contract state to another contract state */ -data class UpgradeCommand(val upgradedContractClass: ContractClassName) : CommandData - // DOCSTART 6 /** A [Command] where the signing parties have been looked up if they have a well known/recognised institutional key. */ @CordaSerializable diff --git a/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt b/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt index c3ee51a861..e5eb960ac5 100644 --- a/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt @@ -3,10 +3,6 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* import net.corda.core.internal.ContractUpgradeUtils -import net.corda.core.internal.uncheckedCast -import net.corda.core.transactions.LedgerTransaction -import net.corda.core.transactions.TransactionBuilder -import java.security.PublicKey /** * A flow to be used for authorising and upgrading state objects of an old contract to a new contract. @@ -17,29 +13,6 @@ import java.security.PublicKey * use the new updated state for future transactions. */ object ContractUpgradeFlow { - - @JvmStatic - fun verify(tx: LedgerTransaction) { - // Contract Upgrade transaction should have 1 input, 1 output and 1 command. - verify(tx.inputs.single().state, - tx.outputs.single(), - tx.commandsOfType().single()) - } - - @JvmStatic - fun verify(input: TransactionState, output: TransactionState, commandData: Command) { - val command = commandData.value - val participantKeys: Set = input.data.participants.map { it.owningKey }.toSet() - val keysThatSigned: Set = commandData.signers.toSet() - val upgradedContract: UpgradedContract = uncheckedCast(javaClass.classLoader.loadClass(command.upgradedContractClass).newInstance()) - requireThat { - "The signing keys include all participant keys" using keysThatSigned.containsAll(participantKeys) - "Inputs state reference the legacy contract" using (input.contract == upgradedContract.legacyContract) - "Outputs state reference the upgraded contract" using (output.contract == command.upgradedContractClass) - "Output state must be an upgraded version of the input state" using (output.data == upgradedContract.upgrade(input.data)) - } - } - /** * Authorise a contract state upgrade. * @@ -53,7 +26,7 @@ object ContractUpgradeFlow { class Authorise( val stateAndRef: StateAndRef<*>, private val upgradedContractClass: Class> - ) : FlowLogic() { + ) : FlowLogic() { @Suspendable override fun call(): Void? { val upgrade = upgradedContractClass.newInstance() @@ -89,23 +62,6 @@ object ContractUpgradeFlow { newContractClass: Class> ) : AbstractStateReplacementFlow.Instigator>>(originalState, newContractClass) { - companion object { - fun assembleBareTx( - stateRef: StateAndRef, - upgradedContractClass: Class>, - privacySalt: PrivacySalt - ): TransactionBuilder { - val contractUpgrade = upgradedContractClass.newInstance() - return TransactionBuilder(stateRef.state.notary) - .withItems( - stateRef, - StateAndContract(contractUpgrade.upgrade(stateRef.state.data), upgradedContractClass.name), - Command(UpgradeCommand(upgradedContractClass.name), stateRef.state.data.participants.map { it.owningKey }), - privacySalt - ) - } - } - @Suspendable override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx { val baseTx = ContractUpgradeUtils.assembleBareTx(originalState, modification, PrivacySalt()) diff --git a/core/src/main/kotlin/net/corda/core/internal/UpgradeCommand.kt b/core/src/main/kotlin/net/corda/core/internal/UpgradeCommand.kt new file mode 100644 index 0000000000..9c4e3abb7f --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/UpgradeCommand.kt @@ -0,0 +1,7 @@ +package net.corda.core.internal + +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.ContractClassName + +/** Indicates that this transaction replaces the inputs contract state to another contract state */ +data class UpgradeCommand(val upgradedContractClass: ContractClassName) : CommandData \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index f54d02e5fc..9f8bdf1001 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -3,9 +3,11 @@ package net.corda.core.transactions import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party +import net.corda.core.internal.UpgradeCommand import net.corda.core.internal.castIfPossible import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.CordaSerializable +import java.security.PublicKey import java.util.* import java.util.function.Predicate @@ -63,7 +65,13 @@ data class LedgerTransaction( @Throws(TransactionVerificationException::class) fun verify() { verifyConstraints() - verifyContracts() + // TODO: make contract upgrade transactions have a separate type + if (commands.any { it.value is UpgradeCommand }) { + verifyContractUpgrade() + } + else { + verifyContracts() + } } /** @@ -153,6 +161,25 @@ data class LedgerTransaction( } } + private fun verifyContractUpgrade() { + // Contract Upgrade transaction should have 1 input, 1 output and 1 command. + val input = inputs.single().state + val output = outputs.single() + val commandData = commandsOfType().single() + + val command = commandData.value + val participantKeys: Set = input.data.participants.map { it.owningKey }.toSet() + val keysThatSigned: Set = commandData.signers.toSet() + @Suppress("UNCHECKED_CAST") + val upgradedContract = javaClass.classLoader.loadClass(command.upgradedContractClass).newInstance() as UpgradedContract + requireThat { + "The signing keys include all participant keys" using keysThatSigned.containsAll(participantKeys) + "Inputs state reference the legacy contract" using (input.contract == upgradedContract.legacyContract) + "Outputs state reference the upgraded contract" using (output.contract == command.upgradedContractClass) + "Output state must be an upgraded version of the input state" using (output.data == upgradedContract.upgrade(input.data)) + } + } + /** * Given a type and a function that returns a grouping key, associates inputs and outputs together so that they * can be processed as one. The grouping key is any arbitrary object that can act as a map key (so must implement diff --git a/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt b/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt index 153ce6cc47..24509c3c52 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt @@ -1,6 +1,7 @@ package net.corda.core.contracts import net.corda.core.crypto.SecureHash +import net.corda.core.internal.UpgradeCommand import net.corda.testing.ALICE import net.corda.testing.DUMMY_NOTARY import net.corda.testing.TestDependencyInjectionBase diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index aeca91feb4..042e185472 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -42,22 +42,15 @@ class ContractUpgradeFlowTest { fun setup() { setCordappPackages("net.corda.testing.contracts", "net.corda.finance.contracts.asset", "net.corda.core.flows") mockNet = MockNetwork() - val nodes = mockNet.createSomeNodes(notaryKeyPair = null) // prevent generation of notary override - a = nodes.partyNodes[0] - b = nodes.partyNodes[1] + val notaryNode = mockNet.createNotaryNode() + a = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) + b = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) // Process registration mockNet.runNetwork() a.internals.ensureRegistered() - notary = a.services.getDefaultNotary() - val nodeIdentity = nodes.notaryNode.info.legalIdentitiesAndCerts.single { it.party == notary } - a.database.transaction { - a.services.identityService.verifyAndRegisterIdentity(nodeIdentity) - } - b.database.transaction { - b.services.identityService.verifyAndRegisterIdentity(nodeIdentity) - } + notary = notaryNode.services.getDefaultNotary() } @After diff --git a/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt b/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt index e3819b661d..16d240eecc 100644 --- a/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt +++ b/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt @@ -2,7 +2,6 @@ package net.corda.node.services import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.ContractState -import net.corda.core.contracts.UpgradeCommand import net.corda.core.contracts.UpgradedContract import net.corda.core.contracts.requireThat import net.corda.core.flows.* @@ -64,9 +63,6 @@ class ContractUpgradeHandler(otherSide: FlowSession) : AbstractStateReplacementF "The proposed upgrade ${proposal.modification.javaClass} is a trusted upgrade path" using (proposal.modification.name == authorisedUpgrade) "The proposed tx matches the expected tx for this upgrade" using (proposedTx == expectedTx) } - ContractUpgradeFlow.verify( - oldStateAndRef.state, - expectedTx.outRef(0).state, - expectedTx.toLedgerTransaction(serviceHub).commandsOfType().single()) + proposedTx.toLedgerTransaction(serviceHub).verify() } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index 9baeb6d470..4d0a796258 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -391,7 +391,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, @JvmOverloads fun createSomeNodes(numPartyNodes: Int = 2, nodeFactory: Factory<*> = defaultFactory, notaryKeyPair: KeyPair? = DUMMY_NOTARY_KEY): BasketOfNodes { require(nodes.isEmpty()) - val notaryServiceInfo = ServiceInfo(SimpleNotaryService.type) + val notaryServiceInfo = ServiceInfo(ValidatingNotaryService.type) val notaryOverride = if (notaryKeyPair != null) mapOf(Pair(notaryServiceInfo, notaryKeyPair)) else diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt index 180aaa7b85..5b7f80044d 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt @@ -1,8 +1,8 @@ package net.corda.testing.contracts import net.corda.core.contracts.* -import net.corda.core.flows.ContractUpgradeFlow import net.corda.core.identity.AbstractParty +import net.corda.core.internal.UpgradeCommand import net.corda.core.node.ServicesForResolution import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder @@ -35,7 +35,6 @@ class DummyContractV2 : UpgradedContract Date: Thu, 28 Sep 2017 15:01:08 +0100 Subject: [PATCH 034/185] Improve NodeTabView in DemoBench (#1639) * Improve NodeTabView in DemoBench Reflect recent changes with advertised services removal. * Force selection of only one notary * Update screenshots --- .../net/corda/node/internal/NodeStartup.kt | 4 +-- tools/demobench/demobench-configure-bank.png | Bin 62526 -> 58499 bytes tools/demobench/demobench-initial.png | Bin 43631 -> 53694 bytes .../corda/demobench/model/InstallFactory.kt | 2 +- .../corda/demobench/model/NodeController.kt | 3 +- .../demobench/model/ServiceController.kt | 28 +++++++++++------- .../net/corda/demobench/views/NodeTabView.kt | 16 ++++++++-- .../src/main/resources/services.conf | 14 ++++----- .../demobench/model/ServiceControllerTest.kt | 4 +-- .../test/resources/duplicate-services.conf | 6 ++-- .../src/test/resources/notary-services.conf | 4 +-- 11 files changed, 49 insertions(+), 32 deletions(-) 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 0b645a2c48..9fd82c83d0 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -260,8 +260,8 @@ open class NodeStartup(val args: Array) { } private fun printPluginsAndServices(node: Node) { - node.configuration.extraAdvertisedServiceIds.let { - if (it.isNotEmpty()) Node.printBasicNodeInfo("Providing network services", it.joinToString()) + node.configuration.extraAdvertisedServiceIds.filter { it.startsWith("corda.notary.") || it.startsWith("corda.network_map") }.let { + if (it.isNotEmpty()) Node.printBasicNodeInfo("Providing additional services", it.joinToString()) } Node.printBasicNodeInfo("Loaded CorDapps", node.cordappProvider.cordapps.map { it.name }.joinToString()) val plugins = node.pluginRegistries diff --git a/tools/demobench/demobench-configure-bank.png b/tools/demobench/demobench-configure-bank.png index 5e2c01211b75612f9fbb62dd9fb8d14ac45125bc..867e762506fcb7b2f0e7ecb6937e0307bf06e382 100644 GIT binary patch literal 58499 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 z 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 literal 62526 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 yPSdE2F!lDzHSmC}+PxYqF+v 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¨4>iM1&XXdxjLIx6Z{N!CcRx$+ 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�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 E0R)o^ZvX%Q diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt index e2eeb63203..ef4e59d91a 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt @@ -54,7 +54,7 @@ class InstallFactory : Controller() { } private fun Config.parseExtraServices(path: String): MutableList { - val services = serviceController.services.toSortedSet() + ServiceInfo(ServiceType.networkMap).toString() + val services = serviceController.services.values.toSortedSet() + ServiceInfo(ServiceType.networkMap).toString() return this.getStringList(path) .filter { !it.isNullOrEmpty() } .map { svc -> 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 798c3609e1..f6479308c2 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 @@ -24,6 +24,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { private val jvm by inject() private val pluginController by inject() + private val serviceController by inject() private var baseDir: Path = baseDirFor(ManagementFactory.getRuntimeMXBean().startTime) private val cordaPath: Path = jvm.applicationDir.resolve("corda").resolve("corda.jar") @@ -63,7 +64,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { nodeData.rpcPort.value, nodeData.webPort.value, nodeData.h2Port.value, - nodeData.extraServices.toMutableList() + nodeData.extraServices.map { serviceController.services[it]!! }.toMutableList() ) if (nodes.putIfAbsent(config.key, config) != null) { diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/ServiceController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/ServiceController.kt index cfd1c23038..2f63b8e1c0 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/ServiceController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/ServiceController.kt @@ -8,31 +8,37 @@ import java.util.logging.Level class ServiceController(resourceName: String = "/services.conf") : Controller() { - val services: List = loadConf(resources.url(resourceName)) + val services: Map = loadConf(resources.url(resourceName)) - val notaries: List = services.filter { it.startsWith("corda.notary.") }.toList() + val notaries: Map = services.filter { it.value.startsWith("corda.notary.") } + + val issuers: Map = services.filter { it.value.startsWith("corda.issuer.") } /* * Load our list of known extra Corda services. */ - private fun loadConf(url: URL?): List { + private fun loadConf(url: URL?): Map { return if (url == null) { - emptyList() + emptyMap() } else { try { - val set = sortedSetOf() + val map = linkedMapOf() InputStreamReader(url.openStream()).useLines { sq -> sq.forEach { line -> - val service = line.trim() - set.add(service) - - log.info("Supports: $service") + val service = line.split(":").map { it.trim() } + if (service.size != 2) { + log.warning("Encountered corrupted line '$line' while reading services from config: $url") + } + else { + map[service[1]] = service[0] + log.info("Supports: $service") + } } + map } - set.toList() } catch (e: IOException) { log.log(Level.SEVERE, "Failed to load $url: ${e.message}", e) - emptyList() + emptyMap() } } } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt index 8f3ec2d19d..f36edea872 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt @@ -5,6 +5,7 @@ import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory import javafx.application.Platform import javafx.beans.InvalidationListener +import javafx.collections.ListChangeListener import javafx.geometry.Pos import javafx.scene.control.ComboBox import javafx.scene.image.Image @@ -61,7 +62,7 @@ class NodeTabView : Fragment() { private val chooser = FileChooser() private val model = NodeDataModel() - private val availableServices: List = if (nodeController.hasNetworkMap()) serviceController.services else serviceController.notaries + private val availableServices: List = if (nodeController.hasNetworkMap()) serviceController.issuers.keys.toList() else serviceController.notaries.keys.toList() private val nodeTerminalView = find() private val nodeConfigView = stackpane { @@ -109,7 +110,7 @@ class NodeTabView : Fragment() { } } - fieldset("Services") { + fieldset("Additional configuration") { styleClass.addAll("services-panel") val servicesList = CheckListView(availableServices.observable()).apply { @@ -117,6 +118,17 @@ class NodeTabView : Fragment() { model.item.extraServices.set(checkModel.checkedItems) if (!nodeController.hasNetworkMap()) { checkModel.check(0) + checkModel.checkedItems.addListener(ListChangeListener { change -> + while (change.next()) { + if (change.wasAdded()) { + val item = change.addedSubList.last() + val idx = checkModel.getItemIndex(item) + checkModel.checkedIndices.forEach { + if (it != idx) checkModel.clearCheck(it) + } + } + } + }) } } add(servicesList) diff --git a/tools/demobench/src/main/resources/services.conf b/tools/demobench/src/main/resources/services.conf index cb475c0ade..d8319ddccb 100644 --- a/tools/demobench/src/main/resources/services.conf +++ b/tools/demobench/src/main/resources/services.conf @@ -1,8 +1,6 @@ -corda.notary.validating -corda.notary.simple -corda.interest_rates -corda.issuer.USD -corda.issuer.GBP -corda.issuer.CHF -corda.issuer.EUR -corda.cash +corda.notary.validating : Validating Notary +corda.notary.simple : Non-validating Notary +corda.issuer.USD : Issuer USD +corda.issuer.GBP : Issuer GBP +corda.issuer.CHF : Issuer CHF +corda.issuer.EUR : Issuer EUR \ No newline at end of file diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/ServiceControllerTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/ServiceControllerTest.kt index 629ee90903..f80f13b165 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/ServiceControllerTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/ServiceControllerTest.kt @@ -21,14 +21,14 @@ class ServiceControllerTest { fun `test duplicates`() { val controller = ServiceController("/duplicate-services.conf") assertNotNull(controller.services) - assertEquals(listOf("corda.example"), controller.services) + assertEquals(listOf("corda.example"), controller.services.map { it.value }) } @Test fun `test notaries`() { val controller = ServiceController("/notary-services.conf") assertNotNull(controller.notaries) - assertEquals(listOf("corda.notary.simple"), controller.notaries) + assertEquals(listOf("corda.notary.simple"), controller.notaries.map { it.value }) } @Test diff --git a/tools/demobench/src/test/resources/duplicate-services.conf b/tools/demobench/src/test/resources/duplicate-services.conf index ffa2ac0e33..f7faf80aed 100644 --- a/tools/demobench/src/test/resources/duplicate-services.conf +++ b/tools/demobench/src/test/resources/duplicate-services.conf @@ -1,3 +1,3 @@ -corda.example -corda.example -corda.example +corda.example : Example +corda.example : Example +corda.example : Example diff --git a/tools/demobench/src/test/resources/notary-services.conf b/tools/demobench/src/test/resources/notary-services.conf index 5f833b04af..79c6835073 100644 --- a/tools/demobench/src/test/resources/notary-services.conf +++ b/tools/demobench/src/test/resources/notary-services.conf @@ -1,2 +1,2 @@ -corda.notary.simple -corda.example +corda.notary.simple : Notary Simple +corda.example : Example From 5e5f9d779031e72f08d6ff9cf6882331a83f7f02 Mon Sep 17 00:00:00 2001 From: Alberto Arri <30873160+al-r3@users.noreply.github.com> Date: Thu, 28 Sep 2017 16:44:05 +0100 Subject: [PATCH 035/185] add message warning windows users they might need to manually kill explorer demo nodes started by gradle (#1717) --- docs/source/node-explorer.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/node-explorer.rst b/docs/source/node-explorer.rst index 4fc1a53068..48c0a96088 100644 --- a/docs/source/node-explorer.rst +++ b/docs/source/node-explorer.rst @@ -73,6 +73,9 @@ Explorer login credentials to the Issuer nodes are defaulted to ``manager`` and Explorer login credentials to the Participants nodes are defaulted to ``user1`` and ``test``. Please note you are not allowed to login to the notary. +.. note:: When you start the nodes in Windows using the command prompt, they might not be killed when you close the + window or terminate the task. If that happens you need to manually terminate the Java processes running the nodes. + .. note:: Alternatively, you may start the demo nodes from within IntelliJ using either of the run configurations ``Explorer - demo nodes`` or ``Explorer - demo nodes (simulation)`` From 2aaeb4c0b5761f7c0b81537d52476b8ecfd55c9d Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Thu, 28 Sep 2017 17:12:21 +0100 Subject: [PATCH 036/185] CORDA-652: Remove uses of `createSomeNodes` for consistency (#1492) Replace use of `createSomeNodes()` with creating notary and party nodes individually. This typically results in less code as the basket of nodes isn't built first then the nodes, but instead the nodes generated directly. Notably this identified issues in notary change and contract upgrade tests, which were not actually using a validating notary and therefore it had been missed that the transactions were failing validation. Renamed nodes in tests for consistency as well, so nodes are now `aliceNode`, `bobNode`, etc. instead of `a`, `b`, or `n0`, `n1`, or other variants of those. --- .../confidential/IdentitySyncFlowTests.kt | 2 +- .../confidential/SwapIdentitiesFlowTests.kt | 2 +- .../net/corda/core/flows/FlowsInJavaTest.java | 22 ++-- .../net/corda/core/flows/AttachmentTests.kt | 68 +++++------ .../core/flows/CollectSignaturesFlowTests.kt | 56 +++++---- .../core/flows/ContractUpgradeFlowTest.kt | 70 +++++------ .../net/corda/core/flows/FinalityFlowTests.kt | 42 +++---- .../internal/ResolveTransactionsFlowTest.kt | 112 +++++++++--------- .../corda/finance/flows/CashExitFlowTests.kt | 11 +- .../corda/finance/flows/CashIssueFlowTests.kt | 10 +- .../finance/flows/CashPaymentFlowTests.kt | 5 +- .../node/messaging/TwoPartyTradeFlowTests.kt | 22 ++-- .../services/network/NetworkMapCacheTest.kt | 54 ++++----- .../kotlin/net/corda/testing/node/MockNode.kt | 30 +---- 14 files changed, 238 insertions(+), 268 deletions(-) diff --git a/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt b/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt index f2907c7c7c..1fe89e43ec 100644 --- a/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt +++ b/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt @@ -40,7 +40,7 @@ class IdentitySyncFlowTests { @Test fun `sync confidential identities`() { // Set up values we'll need - val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) + val notaryNode = mockNet.createNotaryNode() val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) val alice: Party = aliceNode.services.myInfo.chooseIdentity() diff --git a/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt b/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt index 684cd92c82..202da9412e 100644 --- a/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt +++ b/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt @@ -22,7 +22,7 @@ class SwapIdentitiesFlowTests { val mockNet = MockNetwork(false, true) // Set up values we'll need - val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) + val notaryNode = mockNet.createNotaryNode() val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) val alice: Party = aliceNode.services.myInfo.chooseIdentity() diff --git a/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java b/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java index 01855fb5e3..ff30e6547d 100644 --- a/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java +++ b/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java @@ -5,6 +5,7 @@ import com.google.common.primitives.Primitives; import net.corda.core.identity.Party; import net.corda.node.internal.StartedNode; import net.corda.testing.node.MockNetwork; +import net.corda.testing.TestConstants; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -19,17 +20,18 @@ import static org.junit.Assert.fail; public class FlowsInJavaTest { private final MockNetwork mockNet = new MockNetwork(); - private StartedNode node1; - private StartedNode node2; + private StartedNode notaryNode; + private StartedNode aliceNode; + private StartedNode bobNode; @Before public void setUp() throws Exception { - MockNetwork.BasketOfNodes someNodes = mockNet.createSomeNodes(2); - node1 = someNodes.getPartyNodes().get(0); - node2 = someNodes.getPartyNodes().get(1); + notaryNode = mockNet.createNotaryNode(); + aliceNode = mockNet.createPartyNode(notaryNode.getNetwork().getMyAddress(), TestConstants.getALICE().getName()); + bobNode = mockNet.createPartyNode(notaryNode.getNetwork().getMyAddress(), TestConstants.getBOB().getName()); mockNet.runNetwork(); // Ensure registration was successful - node1.getInternals().getNodeReadyFuture().get(); + aliceNode.getInternals().getNodeReadyFuture().get(); } @After @@ -39,8 +41,8 @@ public class FlowsInJavaTest { @Test public void suspendableActionInsideUnwrap() throws Exception { - node2.getInternals().registerInitiatedFlow(SendHelloAndThenReceive.class); - Future result = node1.getServices().startFlow(new SendInUnwrapFlow(chooseIdentity(node2.getInfo()))).getResultFuture(); + bobNode.getInternals().registerInitiatedFlow(SendHelloAndThenReceive.class); + Future result = aliceNode.getServices().startFlow(new SendInUnwrapFlow(chooseIdentity(bobNode.getInfo()))).getResultFuture(); mockNet.runNetwork(); assertThat(result.get()).isEqualTo("Hello"); } @@ -55,8 +57,8 @@ public class FlowsInJavaTest { } private void primitiveReceiveTypeTest(Class receiveType) throws InterruptedException { - PrimitiveReceiveFlow flow = new PrimitiveReceiveFlow(chooseIdentity(node2.getInfo()), receiveType); - Future result = node1.getServices().startFlow(flow).getResultFuture(); + PrimitiveReceiveFlow flow = new PrimitiveReceiveFlow(chooseIdentity(bobNode.getInfo()), receiveType); + Future result = aliceNode.getServices().startFlow(flow).getResultFuture(); mockNet.runNetwork(); try { result.get(); diff --git a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt index 5a61e01605..dd5f1e6783 100644 --- a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt @@ -10,12 +10,14 @@ import net.corda.core.internal.FetchDataFlow import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode -import net.corda.nodeapi.internal.ServiceInfo import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.network.NetworkMapService import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.utilities.DatabaseTransactionManager +import net.corda.nodeapi.internal.ServiceInfo +import net.corda.testing.ALICE +import net.corda.testing.BOB import net.corda.testing.chooseIdentity import net.corda.testing.node.MockNetwork import org.junit.After @@ -55,68 +57,68 @@ class AttachmentTests { @Test fun `download and store`() { - val nodes = mockNet.createSomeNodes(2) - val n0 = nodes.partyNodes[0] - val n1 = nodes.partyNodes[1] + val notaryNode = mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) + val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) // Ensure that registration was successful before progressing any further mockNet.runNetwork() - n0.internals.ensureRegistered() + aliceNode.internals.ensureRegistered() - n0.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) - n1.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) + aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) + bobNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) // Insert an attachment into node zero's store directly. - val id = n0.database.transaction { - n0.attachments.importAttachment(ByteArrayInputStream(fakeAttachment())) + val id = aliceNode.database.transaction { + aliceNode.attachments.importAttachment(ByteArrayInputStream(fakeAttachment())) } // Get node one to run a flow to fetch it and insert it. mockNet.runNetwork() - val f1 = n1.startAttachmentFlow(setOf(id), n0.info.chooseIdentity()) + val bobFlow = bobNode.startAttachmentFlow(setOf(id), aliceNode.info.chooseIdentity()) mockNet.runNetwork() - assertEquals(0, f1.resultFuture.getOrThrow().fromDisk.size) + assertEquals(0, bobFlow.resultFuture.getOrThrow().fromDisk.size) // Verify it was inserted into node one's store. - val attachment = n1.database.transaction { - n1.attachments.openAttachment(id)!! + val attachment = bobNode.database.transaction { + bobNode.attachments.openAttachment(id)!! } assertEquals(id, attachment.open().readBytes().sha256()) // Shut down node zero and ensure node one can still resolve the attachment. - n0.dispose() + aliceNode.dispose() - val response: FetchDataFlow.Result = n1.startAttachmentFlow(setOf(id), n0.info.chooseIdentity()).resultFuture.getOrThrow() + val response: FetchDataFlow.Result = bobNode.startAttachmentFlow(setOf(id), aliceNode.info.chooseIdentity()).resultFuture.getOrThrow() assertEquals(attachment, response.fromDisk[0]) } @Test fun `missing`() { - val nodes = mockNet.createSomeNodes(2) - val n0 = nodes.partyNodes[0] - val n1 = nodes.partyNodes[1] + val notaryNode = mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) + val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) // Ensure that registration was successful before progressing any further mockNet.runNetwork() - n0.internals.ensureRegistered() + aliceNode.internals.ensureRegistered() - n0.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) - n1.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) + aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) + bobNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) // Get node one to fetch a non-existent attachment. val hash = SecureHash.randomSHA256() mockNet.runNetwork() - val f1 = n1.startAttachmentFlow(setOf(hash), n0.info.chooseIdentity()) + val bobFlow = bobNode.startAttachmentFlow(setOf(hash), aliceNode.info.chooseIdentity()) mockNet.runNetwork() - val e = assertFailsWith { f1.resultFuture.getOrThrow() } + val e = assertFailsWith { bobFlow.resultFuture.getOrThrow() } assertEquals(hash, e.requested) } @Test fun `malicious response`() { // Make a node that doesn't do sanity checking at load time. - val n0 = mockNet.createNode(nodeFactory = object : MockNetwork.Factory { + val aliceNode = mockNet.createNode(legalName = ALICE.name, nodeFactory = object : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, advertisedServices: Set, id: Int, overrideServices: Map?, @@ -126,19 +128,19 @@ class AttachmentTests { } } }, advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type))) - val n1 = mockNet.createNode(n0.network.myAddress) + val bobNode = mockNet.createNode(aliceNode.network.myAddress, legalName = BOB.name) // Ensure that registration was successful before progressing any further mockNet.runNetwork() - n0.internals.ensureRegistered() + aliceNode.internals.ensureRegistered() - n0.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) - n1.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) + aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) + bobNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) val attachment = fakeAttachment() // Insert an attachment into node zero's store directly. - val id = n0.database.transaction { - n0.attachments.importAttachment(ByteArrayInputStream(attachment)) + val id = aliceNode.database.transaction { + aliceNode.attachments.importAttachment(ByteArrayInputStream(attachment)) } // Corrupt its store. @@ -146,15 +148,15 @@ class AttachmentTests { System.arraycopy(corruptBytes, 0, attachment, 0, corruptBytes.size) val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = attachment) - n0.database.transaction { + aliceNode.database.transaction { DatabaseTransactionManager.current().session.update(corruptAttachment) } // Get n1 to fetch the attachment. Should receive corrupted bytes. mockNet.runNetwork() - val f1 = n1.startAttachmentFlow(setOf(id), n0.info.chooseIdentity()) + val bobFlow = bobNode.startAttachmentFlow(setOf(id), aliceNode.info.chooseIdentity()) mockNet.runNetwork() - assertFailsWith { f1.resultFuture.getOrThrow() } + assertFailsWith { bobFlow.resultFuture.getOrThrow() } } private fun StartedNode<*>.startAttachmentFlow(hashes: Set, otherSide: Party) = services.startFlow(InitiatingFetchAttachmentsFlow(otherSide, hashes)) diff --git a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt index 6ea86cd6a3..8a913d5983 100644 --- a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -24,24 +24,22 @@ import kotlin.test.assertFailsWith class CollectSignaturesFlowTests { lateinit var mockNet: MockNetwork - lateinit var a: StartedNode - lateinit var b: StartedNode - lateinit var c: StartedNode + lateinit var aliceNode: StartedNode + lateinit var bobNode: StartedNode + lateinit var charlieNode: StartedNode lateinit var notary: Party - lateinit var services: MockServices @Before fun setup() { setCordappPackages("net.corda.testing.contracts") - services = MockServices() mockNet = MockNetwork() - val nodes = mockNet.createSomeNodes(3) - a = nodes.partyNodes[0] - b = nodes.partyNodes[1] - c = nodes.partyNodes[2] + val notaryNode = mockNet.createNotaryNode() + aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) + bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) + charlieNode = mockNet.createPartyNode(notaryNode.network.myAddress, CHARLIE.name) mockNet.runNetwork() - notary = a.services.getDefaultNotary() - a.internals.ensureRegistered() + notary = notaryNode.services.getDefaultNotary() + aliceNode.internals.ensureRegistered() } @After @@ -51,7 +49,7 @@ class CollectSignaturesFlowTests { } private fun registerFlowOnAllNodes(flowClass: KClass>) { - listOf(a, b, c).forEach { + listOf(aliceNode, bobNode, charlieNode).forEach { it.internals.registerInitiatedFlow(flowClass.java) } } @@ -142,18 +140,18 @@ class CollectSignaturesFlowTests { @Test fun `successfully collects two signatures`() { - val bConfidentialIdentity = b.database.transaction { - b.services.keyManagementService.freshKeyAndCert(b.info.chooseIdentityAndCert(), false) + val bConfidentialIdentity = bobNode.database.transaction { + bobNode.services.keyManagementService.freshKeyAndCert(bobNode.info.chooseIdentityAndCert(), false) } - a.database.transaction { + aliceNode.database.transaction { // Normally this is handled by TransactionKeyFlow, but here we have to manually let A know about the identity - a.services.identityService.verifyAndRegisterIdentity(bConfidentialIdentity) + aliceNode.services.identityService.verifyAndRegisterIdentity(bConfidentialIdentity) } registerFlowOnAllNodes(TestFlowTwo.Responder::class) val magicNumber = 1337 - val parties = listOf(a.info.chooseIdentity(), bConfidentialIdentity.party, c.info.chooseIdentity()) + val parties = listOf(aliceNode.info.chooseIdentity(), bConfidentialIdentity.party, charlieNode.info.chooseIdentity()) val state = DummyContract.MultiOwnerState(magicNumber, parties) - val flow = a.services.startFlow(TestFlowTwo.Initiator(state)) + val flow = aliceNode.services.startFlow(TestFlowTwo.Initiator(state)) mockNet.runNetwork() val result = flow.resultFuture.getOrThrow() result.verifyRequiredSignatures() @@ -163,9 +161,9 @@ class CollectSignaturesFlowTests { @Test fun `no need to collect any signatures`() { - val onePartyDummyContract = DummyContract.generateInitial(1337, notary, a.info.chooseIdentity().ref(1)) - val ptx = a.services.signInitialTransaction(onePartyDummyContract) - val flow = a.services.startFlow(CollectSignaturesFlow(ptx, emptySet())) + val onePartyDummyContract = DummyContract.generateInitial(1337, notary, aliceNode.info.chooseIdentity().ref(1)) + val ptx = aliceNode.services.signInitialTransaction(onePartyDummyContract) + val flow = aliceNode.services.startFlow(CollectSignaturesFlow(ptx, emptySet())) mockNet.runNetwork() val result = flow.resultFuture.getOrThrow() result.verifyRequiredSignatures() @@ -175,10 +173,10 @@ class CollectSignaturesFlowTests { @Test fun `fails when not signed by initiator`() { - val onePartyDummyContract = DummyContract.generateInitial(1337, notary, a.info.chooseIdentity().ref(1)) + val onePartyDummyContract = DummyContract.generateInitial(1337, notary, aliceNode.info.chooseIdentity().ref(1)) val miniCorpServices = MockServices(MINI_CORP_KEY) val ptx = miniCorpServices.signInitialTransaction(onePartyDummyContract) - val flow = a.services.startFlow(CollectSignaturesFlow(ptx, emptySet())) + val flow = aliceNode.services.startFlow(CollectSignaturesFlow(ptx, emptySet())) mockNet.runNetwork() assertFailsWith("The Initiator of CollectSignaturesFlow must have signed the transaction.") { flow.resultFuture.getOrThrow() @@ -188,12 +186,12 @@ class CollectSignaturesFlowTests { @Test fun `passes with multiple initial signatures`() { val twoPartyDummyContract = DummyContract.generateInitial(1337, notary, - a.info.chooseIdentity().ref(1), - b.info.chooseIdentity().ref(2), - b.info.chooseIdentity().ref(3)) - val signedByA = a.services.signInitialTransaction(twoPartyDummyContract) - val signedByBoth = b.services.addSignature(signedByA) - val flow = a.services.startFlow(CollectSignaturesFlow(signedByBoth, emptySet())) + aliceNode.info.chooseIdentity().ref(1), + bobNode.info.chooseIdentity().ref(2), + bobNode.info.chooseIdentity().ref(3)) + val signedByA = aliceNode.services.signInitialTransaction(twoPartyDummyContract) + val signedByBoth = bobNode.services.addSignature(signedByA) + val flow = aliceNode.services.startFlow(CollectSignaturesFlow(signedByBoth, emptySet())) mockNet.runNetwork() val result = flow.resultFuture.getOrThrow() println(result.tx) diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index 042e185472..18a422441a 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -34,8 +34,8 @@ import kotlin.test.assertTrue class ContractUpgradeFlowTest { lateinit var mockNet: MockNetwork - lateinit var a: StartedNode - lateinit var b: StartedNode + lateinit var aliceNode: StartedNode + lateinit var bobNode: StartedNode lateinit var notary: Party @Before @@ -43,12 +43,12 @@ class ContractUpgradeFlowTest { setCordappPackages("net.corda.testing.contracts", "net.corda.finance.contracts.asset", "net.corda.core.flows") mockNet = MockNetwork() val notaryNode = mockNet.createNotaryNode() - a = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) - b = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) + aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) + bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) // Process registration mockNet.runNetwork() - a.internals.ensureRegistered() + aliceNode.internals.ensureRegistered() notary = notaryNode.services.getDefaultNotary() } @@ -62,37 +62,37 @@ class ContractUpgradeFlowTest { @Test fun `2 parties contract upgrade`() { // Create dummy contract. - val twoPartyDummyContract = DummyContract.generateInitial(0, notary, a.info.chooseIdentity().ref(1), b.info.chooseIdentity().ref(1)) - val signedByA = a.services.signInitialTransaction(twoPartyDummyContract) - val stx = b.services.addSignature(signedByA) + val twoPartyDummyContract = DummyContract.generateInitial(0, notary, aliceNode.info.chooseIdentity().ref(1), bobNode.info.chooseIdentity().ref(1)) + val signedByA = aliceNode.services.signInitialTransaction(twoPartyDummyContract) + val stx = bobNode.services.addSignature(signedByA) - a.services.startFlow(FinalityFlow(stx, setOf(b.info.chooseIdentity()))) + aliceNode.services.startFlow(FinalityFlow(stx, setOf(bobNode.info.chooseIdentity()))) mockNet.runNetwork() - val atx = a.database.transaction { a.services.validatedTransactions.getTransaction(stx.id) } - val btx = b.database.transaction { b.services.validatedTransactions.getTransaction(stx.id) } + val atx = aliceNode.database.transaction { aliceNode.services.validatedTransactions.getTransaction(stx.id) } + val btx = bobNode.database.transaction { bobNode.services.validatedTransactions.getTransaction(stx.id) } requireNotNull(atx) requireNotNull(btx) // The request is expected to be rejected because party B hasn't authorised the upgrade yet. - val rejectedFuture = a.services.startFlow(ContractUpgradeFlow.Initiate(atx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture + val rejectedFuture = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(atx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture mockNet.runNetwork() assertFailsWith(UnexpectedFlowEndException::class) { rejectedFuture.getOrThrow() } // Party B authorise the contract state upgrade, and immediately deauthorise the same. - b.services.startFlow(ContractUpgradeFlow.Authorise(btx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture.getOrThrow() - b.services.startFlow(ContractUpgradeFlow.Deauthorise(btx.tx.outRef(0).ref)).resultFuture.getOrThrow() + bobNode.services.startFlow(ContractUpgradeFlow.Authorise(btx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture.getOrThrow() + bobNode.services.startFlow(ContractUpgradeFlow.Deauthorise(btx.tx.outRef(0).ref)).resultFuture.getOrThrow() // The request is expected to be rejected because party B has subsequently deauthorised and a previously authorised upgrade. - val deauthorisedFuture = a.services.startFlow(ContractUpgradeFlow.Initiate(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture + val deauthorisedFuture = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture mockNet.runNetwork() assertFailsWith(UnexpectedFlowEndException::class) { deauthorisedFuture.getOrThrow() } // Party B authorise the contract state upgrade - b.services.startFlow(ContractUpgradeFlow.Authorise(btx.tx.outRef(0), DummyContractV2::class.java)).resultFuture.getOrThrow() + bobNode.services.startFlow(ContractUpgradeFlow.Authorise(btx.tx.outRef(0), DummyContractV2::class.java)).resultFuture.getOrThrow() // Party A initiates contract upgrade flow, expected to succeed this time. - val resultFuture = a.services.startFlow(ContractUpgradeFlow.Initiate(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture + val resultFuture = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture mockNet.runNetwork() val result = resultFuture.getOrThrow() @@ -113,8 +113,8 @@ class ContractUpgradeFlowTest { // Verify outputs. assertTrue(nodeStx!!.tx.outputs.single().data is DummyContractV2.State) } - check(a) - check(b) + check(aliceNode) + check(bobNode) } private fun RPCDriverExposedDSLInterface.startProxy(node: StartedNode<*>, user: User): CordaRPCOps { @@ -132,9 +132,9 @@ class ContractUpgradeFlowTest { fun `2 parties contract upgrade using RPC`() { rpcDriver(initialiseSerialization = false) { // Create dummy contract. - val twoPartyDummyContract = DummyContract.generateInitial(0, notary, a.info.chooseIdentity().ref(1), b.info.chooseIdentity().ref(1)) - val signedByA = a.services.signInitialTransaction(twoPartyDummyContract) - val stx = b.services.addSignature(signedByA) + val twoPartyDummyContract = DummyContract.generateInitial(0, notary, aliceNode.info.chooseIdentity().ref(1), bobNode.info.chooseIdentity().ref(1)) + val signedByA = aliceNode.services.signInitialTransaction(twoPartyDummyContract) + val stx = bobNode.services.addSignature(signedByA) val user = rpcTestUser.copy(permissions = setOf( startFlowPermission(), @@ -142,14 +142,14 @@ class ContractUpgradeFlowTest { startFlowPermission(), startFlowPermission() )) - val rpcA = startProxy(a, user) - val rpcB = startProxy(b, user) - val handle = rpcA.startFlow(::FinalityInvoker, stx, setOf(b.info.chooseIdentity())) + val rpcA = startProxy(aliceNode, user) + val rpcB = startProxy(bobNode, user) + val handle = rpcA.startFlow(::FinalityInvoker, stx, setOf(bobNode.info.chooseIdentity())) mockNet.runNetwork() handle.returnValue.getOrThrow() - val atx = a.database.transaction { a.services.validatedTransactions.getTransaction(stx.id) } - val btx = b.database.transaction { b.services.validatedTransactions.getTransaction(stx.id) } + val atx = aliceNode.database.transaction { aliceNode.services.validatedTransactions.getTransaction(stx.id) } + val btx = bobNode.database.transaction { bobNode.services.validatedTransactions.getTransaction(stx.id) } requireNotNull(atx) requireNotNull(btx) @@ -188,12 +188,12 @@ class ContractUpgradeFlowTest { mockNet.runNetwork() val result = resultFuture.getOrThrow() // Check results. - listOf(a, b).forEach { - val signedTX = a.database.transaction { a.services.validatedTransactions.getTransaction(result.ref.txhash) } + listOf(aliceNode, bobNode).forEach { + val signedTX = aliceNode.database.transaction { aliceNode.services.validatedTransactions.getTransaction(result.ref.txhash) } requireNotNull(signedTX) // Verify inputs. - val input = a.database.transaction { a.services.validatedTransactions.getTransaction(signedTX!!.tx.inputs.single().txhash) } + val input = aliceNode.database.transaction { aliceNode.services.validatedTransactions.getTransaction(signedTX!!.tx.inputs.single().txhash) } requireNotNull(input) assertTrue(input!!.tx.outputs.single().data is DummyContract.State) @@ -206,20 +206,20 @@ class ContractUpgradeFlowTest { @Test fun `upgrade Cash to v2`() { // Create some cash. - val chosenIdentity = a.info.chooseIdentity() - val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), notary)).resultFuture + val chosenIdentity = aliceNode.info.chooseIdentity() + val result = aliceNode.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), notary)).resultFuture mockNet.runNetwork() val stx = result.getOrThrow().stx val anonymisedRecipient = result.get().recipient!! val stateAndRef = stx.tx.outRef(0) - val baseState = a.database.transaction { a.services.vaultService.queryBy().states.single() } + val baseState = aliceNode.database.transaction { aliceNode.services.vaultService.queryBy().states.single() } assertTrue(baseState.state.data is Cash.State, "Contract state is old version.") // Starts contract upgrade flow. - val upgradeResult = a.services.startFlow(ContractUpgradeFlow.Initiate(stateAndRef, CashV2::class.java)).resultFuture + val upgradeResult = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(stateAndRef, CashV2::class.java)).resultFuture mockNet.runNetwork() upgradeResult.getOrThrow() // Get contract state from the vault. - val firstState = a.database.transaction { a.services.vaultService.queryBy().states.single() } + val firstState = aliceNode.database.transaction { aliceNode.services.vaultService.queryBy().states.single() } assertTrue(firstState.state.data is CashV2.State, "Contract state is upgraded to the new version.") assertEquals(Amount(1000000, USD).`issued by`(chosenIdentity.ref(1)), (firstState.state.data as CashV2.State).amount, "Upgraded cash contain the correct amount.") assertEquals>(listOf(anonymisedRecipient), (firstState.state.data as CashV2.State).owners, "Upgraded cash belongs to the right owner.") diff --git a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt index 865026c958..0d38513c25 100644 --- a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt @@ -7,13 +7,8 @@ import net.corda.finance.POUNDS import net.corda.finance.contracts.asset.Cash import net.corda.finance.issuedBy import net.corda.node.internal.StartedNode -import net.corda.testing.ALICE -import net.corda.testing.chooseIdentity -import net.corda.testing.getDefaultNotary +import net.corda.testing.* import net.corda.testing.node.MockNetwork -import net.corda.testing.node.MockServices -import net.corda.testing.setCordappPackages -import net.corda.testing.unsetCordappPackages import org.junit.After import org.junit.Before import org.junit.Test @@ -22,21 +17,20 @@ import kotlin.test.assertFailsWith class FinalityFlowTests { lateinit var mockNet: MockNetwork - lateinit var nodeA: StartedNode - lateinit var nodeB: StartedNode + lateinit var aliceNode: StartedNode + lateinit var bobNode: StartedNode lateinit var notary: Party - val services = MockServices() @Before fun setup() { setCordappPackages("net.corda.finance.contracts.asset") mockNet = MockNetwork() - val nodes = mockNet.createSomeNodes(2) - nodeA = nodes.partyNodes[0] - nodeB = nodes.partyNodes[1] + val notaryNode = mockNet.createNotaryNode() + aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) + bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) mockNet.runNetwork() - nodeA.internals.ensureRegistered() - notary = nodeA.services.getDefaultNotary() + aliceNode.internals.ensureRegistered() + notary = aliceNode.services.getDefaultNotary() } @After @@ -47,28 +41,28 @@ class FinalityFlowTests { @Test fun `finalise a simple transaction`() { - val amount = 1000.POUNDS.issuedBy(nodeA.info.chooseIdentity().ref(0)) + val amount = 1000.POUNDS.issuedBy(aliceNode.info.chooseIdentity().ref(0)) val builder = TransactionBuilder(notary) - Cash().generateIssue(builder, amount, nodeB.info.chooseIdentity(), notary) - val stx = nodeA.services.signInitialTransaction(builder) - val flow = nodeA.services.startFlow(FinalityFlow(stx)) + Cash().generateIssue(builder, amount, bobNode.info.chooseIdentity(), notary) + val stx = aliceNode.services.signInitialTransaction(builder) + val flow = aliceNode.services.startFlow(FinalityFlow(stx)) mockNet.runNetwork() val notarisedTx = flow.resultFuture.getOrThrow() notarisedTx.verifyRequiredSignatures() - val transactionSeenByB = nodeB.services.database.transaction { - nodeB.services.validatedTransactions.getTransaction(notarisedTx.id) + val transactionSeenByB = bobNode.services.database.transaction { + bobNode.services.validatedTransactions.getTransaction(notarisedTx.id) } assertEquals(notarisedTx, transactionSeenByB) } @Test fun `reject a transaction with unknown parties`() { - val amount = 1000.POUNDS.issuedBy(nodeA.info.chooseIdentity().ref(0)) - val fakeIdentity = ALICE // Alice isn't part of this network, so node A won't recognise them + val amount = 1000.POUNDS.issuedBy(aliceNode.info.chooseIdentity().ref(0)) + val fakeIdentity = CHARLIE // Charlie isn't part of this network, so node A won't recognise them val builder = TransactionBuilder(notary) Cash().generateIssue(builder, amount, fakeIdentity, notary) - val stx = nodeA.services.signInitialTransaction(builder) - val flow = nodeA.services.startFlow(FinalityFlow(stx)) + val stx = aliceNode.services.signInitialTransaction(builder) + val flow = aliceNode.services.startFlow(FinalityFlow(stx)) mockNet.runNetwork() assertFailsWith { flow.resultFuture.getOrThrow() diff --git a/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt b/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt index c5762a136e..1576852791 100644 --- a/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt @@ -12,7 +12,6 @@ import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.getDefaultNotary import net.corda.testing.node.MockNetwork -import net.corda.testing.node.MockServices import org.junit.After import org.junit.Before import org.junit.Test @@ -27,25 +26,26 @@ import kotlin.test.assertNull class ResolveTransactionsFlowTest { lateinit var mockNet: MockNetwork - lateinit var a: StartedNode - lateinit var b: StartedNode + lateinit var notaryNode: StartedNode + lateinit var megaCorpNode: StartedNode + lateinit var miniCorpNode: StartedNode + lateinit var megaCorp: Party + lateinit var miniCorp: Party lateinit var notary: Party - lateinit var megaCorpServices: MockServices - lateinit var notaryServices: MockServices @Before fun setup() { setCordappPackages("net.corda.testing.contracts") - megaCorpServices = MockServices(MEGA_CORP_KEY) - notaryServices = MockServices(DUMMY_NOTARY_KEY) mockNet = MockNetwork() - val nodes = mockNet.createSomeNodes() - a = nodes.partyNodes[0] - b = nodes.partyNodes[1] - a.internals.registerInitiatedFlow(TestResponseFlow::class.java) - b.internals.registerInitiatedFlow(TestResponseFlow::class.java) + notaryNode = mockNet.createNotaryNode() + megaCorpNode = mockNet.createPartyNode(notaryNode.network.myAddress, MEGA_CORP.name) + miniCorpNode = mockNet.createPartyNode(notaryNode.network.myAddress, MINI_CORP.name) + megaCorpNode.internals.registerInitiatedFlow(TestResponseFlow::class.java) + miniCorpNode.internals.registerInitiatedFlow(TestResponseFlow::class.java) mockNet.runNetwork() - notary = a.services.getDefaultNotary() + notary = notaryNode.services.getDefaultNotary() + megaCorp = megaCorpNode.info.chooseIdentity() + miniCorp = miniCorpNode.info.chooseIdentity() } @After @@ -58,14 +58,14 @@ class ResolveTransactionsFlowTest { @Test fun `resolve from two hashes`() { val (stx1, stx2) = makeTransactions() - val p = TestFlow(setOf(stx2.id), a.info.chooseIdentity()) - val future = b.services.startFlow(p).resultFuture + val p = TestFlow(setOf(stx2.id), megaCorp) + val future = miniCorpNode.services.startFlow(p).resultFuture mockNet.runNetwork() val results = future.getOrThrow() assertEquals(listOf(stx1.id, stx2.id), results.map { it.id }) - b.database.transaction { - assertEquals(stx1, b.services.validatedTransactions.getTransaction(stx1.id)) - assertEquals(stx2, b.services.validatedTransactions.getTransaction(stx2.id)) + miniCorpNode.database.transaction { + assertEquals(stx1, miniCorpNode.services.validatedTransactions.getTransaction(stx1.id)) + assertEquals(stx2, miniCorpNode.services.validatedTransactions.getTransaction(stx2.id)) } } // DOCEND 1 @@ -73,8 +73,8 @@ class ResolveTransactionsFlowTest { @Test fun `dependency with an error`() { val stx = makeTransactions(signFirstTX = false).second - val p = TestFlow(setOf(stx.id), a.info.chooseIdentity()) - val future = b.services.startFlow(p).resultFuture + val p = TestFlow(setOf(stx.id), megaCorp) + val future = miniCorpNode.services.startFlow(p).resultFuture mockNet.runNetwork() assertFailsWith(SignedTransaction.SignaturesMissingException::class) { future.getOrThrow() } } @@ -82,14 +82,14 @@ class ResolveTransactionsFlowTest { @Test fun `resolve from a signed transaction`() { val (stx1, stx2) = makeTransactions() - val p = TestFlow(stx2, a.info.chooseIdentity()) - val future = b.services.startFlow(p).resultFuture + val p = TestFlow(stx2, megaCorp) + val future = miniCorpNode.services.startFlow(p).resultFuture mockNet.runNetwork() future.getOrThrow() - b.database.transaction { - assertEquals(stx1, b.services.validatedTransactions.getTransaction(stx1.id)) + miniCorpNode.database.transaction { + assertEquals(stx1, miniCorpNode.services.validatedTransactions.getTransaction(stx1.id)) // But stx2 wasn't inserted, just stx1. - assertNull(b.services.validatedTransactions.getTransaction(stx2.id)) + assertNull(miniCorpNode.services.validatedTransactions.getTransaction(stx2.id)) } } @@ -100,15 +100,15 @@ class ResolveTransactionsFlowTest { val count = 50 var cursor = stx2 repeat(count) { - val builder = DummyContract.move(cursor.tx.outRef(0), MINI_CORP) - val stx = megaCorpServices.signInitialTransaction(builder) - a.database.transaction { - a.services.recordTransactions(stx) + val builder = DummyContract.move(cursor.tx.outRef(0), miniCorp) + val stx = megaCorpNode.services.signInitialTransaction(builder) + megaCorpNode.database.transaction { + megaCorpNode.services.recordTransactions(stx) } cursor = stx } - val p = TestFlow(setOf(cursor.id), a.info.chooseIdentity(), 40) - val future = b.services.startFlow(p).resultFuture + val p = TestFlow(setOf(cursor.id), megaCorp, 40) + val future = miniCorpNode.services.startFlow(p).resultFuture mockNet.runNetwork() assertFailsWith { future.getOrThrow() } } @@ -117,22 +117,22 @@ class ResolveTransactionsFlowTest { fun `triangle of transactions resolves fine`() { val stx1 = makeTransactions().first - val stx2 = DummyContract.move(stx1.tx.outRef(0), MINI_CORP).run { - val ptx = megaCorpServices.signInitialTransaction(this) - notaryServices.addSignature(ptx) + val stx2 = DummyContract.move(stx1.tx.outRef(0), miniCorp).let { builder -> + val ptx = megaCorpNode.services.signInitialTransaction(builder) + notaryNode.services.addSignature(ptx, notary.owningKey) } - val stx3 = DummyContract.move(listOf(stx1.tx.outRef(0), stx2.tx.outRef(0)), MINI_CORP).run { - val ptx = megaCorpServices.signInitialTransaction(this) - notaryServices.addSignature(ptx) + val stx3 = DummyContract.move(listOf(stx1.tx.outRef(0), stx2.tx.outRef(0)), miniCorp).let { builder -> + val ptx = megaCorpNode.services.signInitialTransaction(builder) + notaryNode.services.addSignature(ptx, notary.owningKey) } - a.database.transaction { - a.services.recordTransactions(stx2, stx3) + megaCorpNode.database.transaction { + megaCorpNode.services.recordTransactions(stx2, stx3) } - val p = TestFlow(setOf(stx3.id), a.info.chooseIdentity()) - val future = b.services.startFlow(p).resultFuture + val p = TestFlow(setOf(stx3.id), megaCorp) + val future = miniCorpNode.services.startFlow(p).resultFuture mockNet.runNetwork() future.getOrThrow() } @@ -149,43 +149,43 @@ class ResolveTransactionsFlowTest { return bs.toByteArray().sequence().open() } // TODO: this operation should not require an explicit transaction - val id = a.database.transaction { - a.services.attachments.importAttachment(makeJar()) + val id = megaCorpNode.database.transaction { + megaCorpNode.services.attachments.importAttachment(makeJar()) } val stx2 = makeTransactions(withAttachment = id).second - val p = TestFlow(stx2, a.info.chooseIdentity()) - val future = b.services.startFlow(p).resultFuture + val p = TestFlow(stx2, megaCorp) + val future = miniCorpNode.services.startFlow(p).resultFuture mockNet.runNetwork() future.getOrThrow() // TODO: this operation should not require an explicit transaction - b.database.transaction { - assertNotNull(b.services.attachments.openAttachment(id)) + miniCorpNode.database.transaction { + assertNotNull(miniCorpNode.services.attachments.openAttachment(id)) } } // DOCSTART 2 private fun makeTransactions(signFirstTX: Boolean = true, withAttachment: SecureHash? = null): Pair { // Make a chain of custody of dummy states and insert into node A. - val dummy1: SignedTransaction = DummyContract.generateInitial(0, notary, MEGA_CORP.ref(1)).let { + val dummy1: SignedTransaction = DummyContract.generateInitial(0, notary, megaCorp.ref(1)).let { if (withAttachment != null) it.addAttachment(withAttachment) when (signFirstTX) { true -> { - val ptx = megaCorpServices.signInitialTransaction(it) - notaryServices.addSignature(ptx) + val ptx = megaCorpNode.services.signInitialTransaction(it) + notaryNode.services.addSignature(ptx, notary.owningKey) } false -> { - notaryServices.signInitialTransaction(it) + notaryNode.services.signInitialTransaction(it, notary.owningKey) } } } - val dummy2: SignedTransaction = DummyContract.move(dummy1.tx.outRef(0), MINI_CORP).let { - val ptx = megaCorpServices.signInitialTransaction(it) - notaryServices.addSignature(ptx) + val dummy2: SignedTransaction = DummyContract.move(dummy1.tx.outRef(0), miniCorp).let { + val ptx = megaCorpNode.services.signInitialTransaction(it) + notaryNode.services.addSignature(ptx, notary.owningKey) } - a.database.transaction { - a.services.recordTransactions(dummy1, dummy2) + megaCorpNode.database.transaction { + megaCorpNode.services.recordTransactions(dummy1, dummy2) } return Pair(dummy1, dummy2) } diff --git a/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt b/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt index 1180a73ff0..60c50f3211 100644 --- a/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt @@ -7,13 +7,10 @@ import net.corda.finance.DOLLARS import net.corda.finance.`issued by` import net.corda.finance.contracts.asset.Cash import net.corda.node.internal.StartedNode -import net.corda.testing.chooseIdentity -import net.corda.testing.getDefaultNotary +import net.corda.testing.* import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode -import net.corda.testing.setCordappPackages -import net.corda.testing.unsetCordappPackages import org.junit.After import org.junit.Before import org.junit.Test @@ -33,9 +30,9 @@ class CashExitFlowTests { fun start() { setCordappPackages("net.corda.finance.contracts.asset") mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) - val nodes = mockNet.createSomeNodes(1) - notaryNode = nodes.notaryNode - bankOfCordaNode = nodes.partyNodes[0] + notaryNode = mockNet.createNotaryNode() + bankOfCordaNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) + notary = notaryNode.services.getDefaultNotary() bankOfCorda = bankOfCordaNode.info.chooseIdentity() mockNet.runNetwork() 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 b5901c1c30..87299eea8d 100644 --- a/finance/src/test/kotlin/net/corda/finance/flows/CashIssueFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashIssueFlowTests.kt @@ -9,6 +9,8 @@ import net.corda.finance.contracts.asset.Cash import net.corda.node.internal.StartedNode import net.corda.testing.chooseIdentity import net.corda.testing.getDefaultNotary +import net.corda.testing.BOC +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode @@ -30,13 +32,11 @@ class CashIssueFlowTests { fun start() { setCordappPackages("net.corda.finance.contracts.asset") mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) - val nodes = mockNet.createSomeNodes(1) - notaryNode = nodes.notaryNode - bankOfCordaNode = nodes.partyNodes[0] + notaryNode = mockNet.createNotaryNode() + bankOfCordaNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) bankOfCorda = bankOfCordaNode.info.chooseIdentity() - + notary = notaryNode.services.getDefaultNotary() mockNet.runNetwork() - notary = bankOfCordaNode.services.getDefaultNotary() } @After diff --git a/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt b/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt index 5a26f1196f..5ba26a9712 100644 --- a/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt @@ -33,9 +33,8 @@ class CashPaymentFlowTests { fun start() { setCordappPackages("net.corda.finance.contracts.asset") mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) - val nodes = mockNet.createSomeNodes(1) - notaryNode = nodes.notaryNode - bankOfCordaNode = nodes.partyNodes[0] + notaryNode = mockNet.createNotaryNode() + bankOfCordaNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) bankOfCorda = bankOfCordaNode.info.chooseIdentity() notary = notaryNode.services.getDefaultNotary() val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, notary)).resultFuture diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 25f2f94923..30ec451570 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -93,15 +93,13 @@ class TwoPartyTradeFlowTests { mockNet = MockNetwork(false, true) ledger(initialiseSerialization = false) { - val basketOfNodes = mockNet.createSomeNodes(3) - val notaryNode = basketOfNodes.notaryNode - val aliceNode = basketOfNodes.partyNodes[0] - val bobNode = basketOfNodes.partyNodes[1] - val bankNode = basketOfNodes.partyNodes[2] + val notaryNode = mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) + val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) + val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) + val notary = notaryNode.services.getDefaultNotary() val cashIssuer = bankNode.info.chooseIdentity().ref(1) val cpIssuer = bankNode.info.chooseIdentity().ref(1, 2, 3) - val notary = aliceNode.services.getDefaultNotary() - aliceNode.internals.disableDBCloseOnStop() bobNode.internals.disableDBCloseOnStop() @@ -144,7 +142,7 @@ class TwoPartyTradeFlowTests { mockNet = MockNetwork(false, true) ledger(initialiseSerialization = false) { - val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) + val notaryNode = mockNet.createNotaryNode() val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) @@ -198,7 +196,7 @@ class TwoPartyTradeFlowTests { fun `shutdown and restore`() { mockNet = MockNetwork(false) ledger(initialiseSerialization = false) { - val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) + val notaryNode = mockNet.createNotaryNode() val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) var bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) @@ -332,7 +330,7 @@ class TwoPartyTradeFlowTests { fun `check dependencies of sale asset are resolved`() { mockNet = MockNetwork(false) - val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) + val notaryNode = mockNet.createNotaryNode() val aliceNode = makeNodeWithTracking(notaryNode.network.myAddress, ALICE.name) val bobNode = makeNodeWithTracking(notaryNode.network.myAddress, BOB.name) val bankNode = makeNodeWithTracking(notaryNode.network.myAddress, BOC.name) @@ -439,7 +437,7 @@ class TwoPartyTradeFlowTests { fun `track works`() { mockNet = MockNetwork(false) - val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) + val notaryNode = mockNet.createNotaryNode() val aliceNode = makeNodeWithTracking(notaryNode.network.myAddress, ALICE.name) val bobNode = makeNodeWithTracking(notaryNode.network.myAddress, BOB.name) val bankNode = makeNodeWithTracking(notaryNode.network.myAddress, BOC.name) @@ -596,7 +594,7 @@ class TwoPartyTradeFlowTests { aliceError: Boolean, expectedMessageSubstring: String ) { - val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) + val notaryNode = mockNet.createNotaryNode() val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) 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 1297d979cb..01a65de71e 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 @@ -5,6 +5,7 @@ import net.corda.core.utilities.getOrThrow import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.ALICE import net.corda.testing.BOB +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.chooseIdentity import net.corda.testing.node.MockNetwork import org.assertj.core.api.Assertions.assertThat @@ -29,10 +30,9 @@ class NetworkMapCacheTest { @Test fun registerWithNetwork() { - val nodes = mockNet.createSomeNodes(1) - val n0 = nodes.mapNode - val n1 = nodes.partyNodes[0] - val future = n1.services.networkMapCache.addMapService(n1.network, n0.network.myAddress, false, null) + val mapNode = mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(mapNode.network.myAddress, ALICE.name) + val future = aliceNode.services.networkMapCache.addMapService(aliceNode.network, mapNode.network.myAddress, false, null) mockNet.runNetwork() future.getOrThrow() } @@ -40,30 +40,29 @@ class NetworkMapCacheTest { @Test fun `key collision`() { val entropy = BigInteger.valueOf(24012017L) - val nodeA = mockNet.createNode(nodeFactory = MockNetwork.DefaultFactory, legalName = ALICE.name, entropyRoot = entropy, advertisedServices = ServiceInfo(NetworkMapService.type)) - val nodeB = mockNet.createNode(nodeFactory = MockNetwork.DefaultFactory, legalName = BOB.name, entropyRoot = entropy, advertisedServices = ServiceInfo(NetworkMapService.type)) - assertEquals(nodeA.info.chooseIdentity(), nodeB.info.chooseIdentity()) + val aliceNode = mockNet.createNode(nodeFactory = MockNetwork.DefaultFactory, legalName = ALICE.name, entropyRoot = entropy, advertisedServices = ServiceInfo(NetworkMapService.type)) + val bobNode = mockNet.createNode(nodeFactory = MockNetwork.DefaultFactory, legalName = BOB.name, entropyRoot = entropy, advertisedServices = ServiceInfo(NetworkMapService.type)) + assertEquals(aliceNode.info.chooseIdentity(), bobNode.info.chooseIdentity()) mockNet.runNetwork() // Node A currently knows only about itself, so this returns node A - assertEquals(nodeA.services.networkMapCache.getNodesByLegalIdentityKey(nodeA.info.chooseIdentity().owningKey).singleOrNull(), nodeA.info) + assertEquals(aliceNode.services.networkMapCache.getNodesByLegalIdentityKey(aliceNode.info.chooseIdentity().owningKey).singleOrNull(), aliceNode.info) - nodeA.services.networkMapCache.addNode(nodeB.info) + aliceNode.services.networkMapCache.addNode(bobNode.info) // The details of node B write over those for node A - assertEquals(nodeA.services.networkMapCache.getNodesByLegalIdentityKey(nodeA.info.chooseIdentity().owningKey).singleOrNull(), nodeB.info) + assertEquals(aliceNode.services.networkMapCache.getNodesByLegalIdentityKey(aliceNode.info.chooseIdentity().owningKey).singleOrNull(), bobNode.info) } @Test fun `getNodeByLegalIdentity`() { - val nodes = mockNet.createSomeNodes(1) - val n0 = nodes.mapNode - val n1 = nodes.partyNodes[0] - val node0Cache: NetworkMapCache = n0.services.networkMapCache - val expected = n1.info + val notaryNode = mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) + val notaryCache: NetworkMapCache = notaryNode.services.networkMapCache + val expected = aliceNode.info mockNet.runNetwork() - val actual = n0.database.transaction { node0Cache.getNodeByLegalIdentity(n1.info.chooseIdentity()) } + val actual = notaryNode.database.transaction { notaryCache.getNodeByLegalIdentity(aliceNode.info.chooseIdentity()) } assertEquals(expected, actual) // TODO: Should have a test case with anonymous lookup @@ -71,19 +70,18 @@ class NetworkMapCacheTest { @Test fun `remove node from cache`() { - val nodes = mockNet.createSomeNodes(1) - val n0 = nodes.mapNode - val n1 = nodes.partyNodes[0] - val n0Identity = n0.info.chooseIdentity() - val n1Identity = n1.info.chooseIdentity() - val node0Cache = n0.services.networkMapCache as PersistentNetworkMapCache + val notaryNode = mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) + val notaryLegalIdentity = notaryNode.info.chooseIdentity() + val alice = aliceNode.info.chooseIdentity() + val notaryCache = notaryNode.services.networkMapCache as PersistentNetworkMapCache mockNet.runNetwork() - n0.database.transaction { - assertThat(node0Cache.getNodeByLegalIdentity(n1Identity) != null) - node0Cache.removeNode(n1.info) - assertThat(node0Cache.getNodeByLegalIdentity(n1Identity) == null) - assertThat(node0Cache.getNodeByLegalIdentity(n0Identity) != null) - assertThat(node0Cache.getNodeByLegalName(n1Identity.name) == null) + notaryNode.database.transaction { + assertThat(notaryCache.getNodeByLegalIdentity(alice) != null) + notaryCache.removeNode(aliceNode.info) + assertThat(notaryCache.getNodeByLegalIdentity(alice) == null) + assertThat(notaryCache.getNodeByLegalIdentity(notaryLegalIdentity) != null) + assertThat(notaryCache.getNodeByLegalName(alice.name) == null) } } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index 4d0a796258..63dd79f8d8 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -379,31 +379,9 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, } /** - * A bundle that separates the generic user nodes and service-providing nodes. A real network might not be so - * clearly separated, but this is convenient for testing. + * Construct a default notary node. */ - data class BasketOfNodes(val partyNodes: List>, val notaryNode: StartedNode, val mapNode: StartedNode) - - /** - * Sets up a network with the requested number of nodes (defaulting to two), with one or more service nodes that - * run a notary, network map, any oracles etc. - */ - @JvmOverloads - fun createSomeNodes(numPartyNodes: Int = 2, nodeFactory: Factory<*> = defaultFactory, notaryKeyPair: KeyPair? = DUMMY_NOTARY_KEY): BasketOfNodes { - require(nodes.isEmpty()) - val notaryServiceInfo = ServiceInfo(ValidatingNotaryService.type) - val notaryOverride = if (notaryKeyPair != null) - mapOf(Pair(notaryServiceInfo, notaryKeyPair)) - else - null - val mapNode = createNode(nodeFactory = nodeFactory, advertisedServices = ServiceInfo(NetworkMapService.type)) - val mapAddress = mapNode.network.myAddress - val notaryNode = createNode(mapAddress, nodeFactory = nodeFactory, overrideServices = notaryOverride, advertisedServices = notaryServiceInfo) - val nodes = (1..numPartyNodes).map { - createPartyNode(mapAddress) - } - return BasketOfNodes(nodes, notaryNode, mapNode) - } + fun createNotaryNode() = createNotaryNode(null, DUMMY_NOTARY.name, null, null) fun createNotaryNode(networkMapAddress: SingleMessageRecipient? = null, legalName: CordaX500Name? = null, @@ -413,6 +391,10 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type, serviceName))) } + // Convenience method for Java + fun createPartyNode(networkMapAddress: SingleMessageRecipient, + legalName: CordaX500Name) = createPartyNode(networkMapAddress, legalName, null) + fun createPartyNode(networkMapAddress: SingleMessageRecipient, legalName: CordaX500Name? = null, overrideServices: Map? = null): StartedNode { From 89ef4034c07c85a0f43f6918c2d18a6582eb2afd Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Thu, 28 Sep 2017 17:42:32 +0100 Subject: [PATCH 037/185] Clean up of Dokka comments (#1632) * Remove use of @see as a cross-reference to actual docs; this is inappropriate (it reflects "See Also", not "See Other"), and often longer than having the actual documentation in place. * Correct syntactical errors in docs * Correct "@returns" to "@return" * Add note about currencies with 3 decimal places --- .../kotlin/net/corda/core/contracts/Amount.kt | 40 +++++++++++-------- .../kotlin/net/corda/core/flows/FlowLogic.kt | 6 +-- .../net/corda/core/flows/FlowSession.kt | 6 +-- .../core/transactions/MerkleTransaction.kt | 4 +- .../finance/contracts/asset/Obligation.kt | 3 +- .../kotlin/net/corda/irs/contract/IRSUtils.kt | 2 +- .../net/corda/testing/LedgerDSLInterpreter.kt | 8 ++-- .../testing/TransactionDSLInterpreter.kt | 15 ++++--- 8 files changed, 48 insertions(+), 36 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/contracts/Amount.kt b/core/src/main/kotlin/net/corda/core/contracts/Amount.kt index 43101b2a3e..64849d3430 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Amount.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Amount.kt @@ -21,7 +21,8 @@ interface TokenizableAssetInfo { * Amount represents a positive quantity of some token (currency, asset, etc.), measured in quantity of the smallest * representable units. The nominal quantity represented by each individual token is equal to the [displayTokenSize]. * The scale property of the [displayTokenSize] should correctly reflect the displayed decimal places and is used - * when rounding conversions from indicative/displayed amounts in [BigDecimal] to Amount occur via the Amount.fromDecimal method. + * when rounding conversions from indicative/displayed amounts in [BigDecimal] to Amount occur via the + * [Amount.fromDecimal] method. * * Amounts of different tokens *do not mix* and attempting to add or subtract two amounts of different currencies * will throw [IllegalArgumentException]. Amounts may not be negative. Amounts are represented internally using a signed @@ -29,10 +30,11 @@ interface TokenizableAssetInfo { * multiplication are overflow checked and will throw [ArithmeticException] if the operation would have caused integer * overflow. * - * @property quantity the number of tokens as a Long value. - * @property displayTokenSize the nominal display unit size of a single token, potentially with trailing decimal display places if the scale parameter is non-zero. - * @property token an instance of type T, usually a singleton. - * @param T the type of the token, for example [Currency]. T should implement TokenizableAssetInfo if automatic conversion to/from a display format is required. + * @property quantity the number of tokens as a long value. + * @property displayTokenSize the nominal display unit size of a single token, potentially with trailing decimal display + * places if the scale parameter is non-zero. + * @property token the type of token this is an amount of. This is usually a singleton. + * @param T the type of the token, for example [Currency]. T should implement [TokenizableAssetInfo] if automatic conversion to/from a display format is required. */ @CordaSerializable data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, val token: T) : Comparable> { @@ -40,11 +42,10 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, companion object { /** * Build an Amount from a decimal representation. For example, with an input of "12.34 GBP", - * returns an amount with a quantity of "1234" tokens. The displayTokenSize as determined via - * getDisplayTokenSize is used to determine the conversion scaling. - * e.g. Bonds might be in nominal amounts of 100, currencies in 0.01 penny units. + * returns an amount with a quantity of "1234" tokens. The function [getDisplayTokenSize] is used to determine the + * conversion scaling, for example bonds might be in nominal amounts of 100, currencies in 0.01 penny units. * - * @see Amount.toDecimal + * @see Amount.toDecimal * @throws ArithmeticException if the intermediate calculations cannot be converted to an unsigned 63-bit token amount. */ @JvmStatic @@ -195,6 +196,7 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, * Mixing non-identical token types will throw [IllegalArgumentException]. * * @throws ArithmeticException if there is overflow of Amount tokens during the summation + * @throws IllegalArgumentException if mixing non-identical token types. */ operator fun plus(other: Amount): Amount { checkToken(other) @@ -202,11 +204,11 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, } /** - * A checked addition operator is supported to simplify netting of Amounts. - * If this leads to the Amount going negative this will throw [IllegalArgumentException]. - * Mixing non-identical token types will throw [IllegalArgumentException]. + * A checked subtraction operator is supported to simplify netting of Amounts. * - * @throws ArithmeticException if there is Numeric underflow + * @throws ArithmeticException if there is numeric underflow. + * @throws IllegalArgumentException if this leads to the amount going negative, or would mix non-identical token + * types. */ operator fun minus(other: Amount): Amount { checkToken(other) @@ -222,6 +224,8 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, * The multiplication operator is supported to allow easy calculation for multiples of a primitive Amount. * Note this is not a conserving operation, so it may not always be correct modelling of proper token behaviour. * N.B. Division is not supported as fractional tokens are not representable by an Amount. + * + * @throws ArithmeticException if there is overflow of Amount tokens during the multiplication. */ operator fun times(other: Long): Amount = Amount(Math.multiplyExact(quantity, other), displayTokenSize, token) @@ -229,13 +233,15 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, * The multiplication operator is supported to allow easy calculation for multiples of a primitive Amount. * Note this is not a conserving operation, so it may not always be correct modelling of proper token behaviour. * N.B. Division is not supported as fractional tokens are not representable by an Amount. + * + * @throws ArithmeticException if there is overflow of Amount tokens during the multiplication. */ operator fun times(other: Int): Amount = Amount(Math.multiplyExact(quantity, other.toLong()), displayTokenSize, token) /** * This method provides a token conserving divide mechanism. * @param partitions the number of amounts to divide the current quantity into. - * @result Returns [partitions] separate Amount objects which sum to the same quantity as this Amount + * @return 'partitions' separate Amount objects which sum to the same quantity as this Amount * and differ by no more than a single token in size. */ fun splitEvenly(partitions: Int): List> { @@ -249,8 +255,10 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, /** * Convert a currency [Amount] to a decimal representation. For example, with an amount with a quantity - * of "1234" GBP, returns "12.34". The precise representation is controlled by the displayTokenSize, - * which determines the size of a single token and controls the trailing decimal places via it's scale property. + * of "1234" GBP, returns "12.34". The precise representation is controlled by the display token size ( + * from [getDisplayTokenSize]), which determines the size of a single token and controls the trailing decimal + * places via its scale property. *Note* that currencies such as the Bahraini Dinar use 3 decimal places, + * and it must not be presumed that this converts amounts to 2 decimal places. * * @see Amount.fromDecimal */ diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt index 2137b5ee03..9145ae4baa 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt @@ -100,7 +100,7 @@ abstract class FlowLogic { * Note that this function is not just a simple send+receive pair: it is more efficient and more correct to * use this when you expect to do a message swap than do use [send] and then [receive] in turn. * - * @returns an [UntrustworthyData] wrapper around the received object. + * @return an [UntrustworthyData] wrapper around the received object. */ @Deprecated("Use FlowSession.sendAndReceive()", level = DeprecationLevel.WARNING) inline fun sendAndReceive(otherParty: Party, payload: Any): UntrustworthyData { @@ -116,7 +116,7 @@ abstract class FlowLogic { * Note that this function is not just a simple send+receive pair: it is more efficient and more correct to * use this when you expect to do a message swap than do use [send] and then [receive] in turn. * - * @returns an [UntrustworthyData] wrapper around the received object. + * @return an [UntrustworthyData] wrapper around the received object. */ @Deprecated("Use FlowSession.sendAndReceive()", level = DeprecationLevel.WARNING) @Suspendable @@ -165,7 +165,7 @@ abstract class FlowLogic { * verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly * corrupted data in order to exploit your code. * - * @returns an [UntrustworthyData] wrapper around the received object. + * @return an [UntrustworthyData] wrapper around the received object. */ @Deprecated("Use FlowSession.receive()", level = DeprecationLevel.WARNING) @Suspendable diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt b/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt index 74bd247a78..7effaa9f52 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt @@ -69,7 +69,7 @@ abstract class FlowSession { * Note that this function is not just a simple send+receive pair: it is more efficient and more correct to * use this when you expect to do a message swap than do use [send] and then [receive] in turn. * - * @returns an [UntrustworthyData] wrapper around the received object. + * @return an [UntrustworthyData] wrapper around the received object. */ @Suspendable inline fun sendAndReceive(payload: Any): UntrustworthyData { @@ -84,7 +84,7 @@ abstract class FlowSession { * Note that this function is not just a simple send+receive pair: it is more efficient and more correct to * use this when you expect to do a message swap than do use [send] and then [receive] in turn. * - * @returns an [UntrustworthyData] wrapper around the received object. + * @return an [UntrustworthyData] wrapper around the received object. */ @Suspendable abstract fun sendAndReceive(receiveType: Class, payload: Any): UntrustworthyData @@ -107,7 +107,7 @@ abstract class FlowSession { * verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly * corrupted data in order to exploit your code. * - * @returns an [UntrustworthyData] wrapper around the received object. + * @return an [UntrustworthyData] wrapper around the received object. */ @Suspendable abstract fun receive(receiveType: Class): UntrustworthyData 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 ea99e30811..0daaedc713 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt @@ -104,7 +104,7 @@ class FilteredTransaction private constructor( * Construction of partial transaction from [WireTransaction] based on filtering. * Note that list of nonces to be sent is updated on the fly, based on the index of the filtered tx component. * @param filtering filtering over the whole WireTransaction. - * @returns a list of [FilteredComponentGroup] used in PartialMerkleTree calculation and verification. + * @return a list of [FilteredComponentGroup] used in PartialMerkleTree calculation and verification. */ private fun filterWithFun(wtx: WireTransaction, filtering: Predicate): List { val filteredSerialisedComponents: MutableMap> = hashMapOf() @@ -196,7 +196,7 @@ class FilteredTransaction private constructor( * over a transaction with the attachment that wasn't verified. Of course it depends on how you implement it, but else -> false * should solve a problem with possible later extensions to WireTransaction. * @param checkingFun function that performs type checking on the structure fields and provides verification logic accordingly. - * @returns false if no elements were matched on a structure or checkingFun returned false. + * @return false if no elements were matched on a structure or checkingFun returned false. */ fun checkWithFun(checkingFun: (Any) -> Boolean): Boolean { val checkList = availableComponentGroups.flatten().map { checkingFun(it) } diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt index d499c08233..38371297bb 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt @@ -199,7 +199,8 @@ class Obligation

: Contract { /** * A command stating that the obligor is settling some or all of the amount owed by transferring a suitable * state object to the beneficiary. If this reduces the balance to zero, the state object is destroyed. - * @see [MoveCommand]. + * + * @see MoveCommand */ data class Settle

(val amount: Amount>>) : CommandData diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt index 12ef6e2cd4..b8950ee871 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt @@ -52,7 +52,7 @@ open class Rate(val ratioUnit: RatioUnit? = null) { } /** - * @returns the hash code of the ratioUnit or zero if the ratioUnit is null, as is the case for floating rate fixings + * @return the hash code of the ratioUnit or zero if the ratioUnit is null, as is the case for floating rate fixings * that have not yet happened. Yet-to-be fixed floating rates need to be equal such that schedules can be tested * for equality. */ diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt index c3fb15115e..9f4f829433 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt @@ -127,7 +127,7 @@ class LedgerDSL by interpreter { /** - * @see LedgerDSLInterpreter._transaction + * Creates and adds a transaction to the ledger. */ @JvmOverloads fun transaction(label: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(notary = DUMMY_NOTARY), @@ -135,7 +135,7 @@ class LedgerDSL String.outputStateAndRef(): StateAndRef = retrieveOutputStateAndRef(S::class.java, this) @@ -156,7 +156,7 @@ class LedgerDSL().state.data /** - * @see OutputStateLookup.retrieveOutputStateAndRef + * Retrieves an output previously defined by [TransactionDSLInterpreter._output] with a label passed in. */ fun retrieveOutput(clazz: Class, label: String) = retrieveOutputStateAndRef(clazz, label).state.data diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt index 22b70bc15a..4916b32b3c 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt @@ -12,7 +12,7 @@ import java.time.Instant /** * This interface defines the bare bone functionality that a Transaction DSL interpreter should implement. * @param The return type of [verifies]/[failsWith] and the like. It is generic so that we have control over whether - * we want to enforce users to call these methods (@see [EnforceVerifyOrFail]) or not. + * we want to enforce users to call these methods (see [EnforceVerifyOrFail]) or not. */ interface TransactionDSLInterpreter : Verifies, OutputStateLookup { /** @@ -33,7 +33,7 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup { * @param encumbrance The position of the encumbrance state. * @param attachmentConstraint The attachment constraint * @param contractState The state itself. - * @params contractClassName The class name of the contract that verifies this state. + * @param contractClassName The class name of the contract that verifies this state. */ fun _output(contractClassName: ContractClassName, label: String?, @@ -96,7 +96,7 @@ class TransactionDSL(val interpreter: T) : Tr fun input(contractClassName: ContractClassName, stateClosure: () -> ContractState) = input(contractClassName, stateClosure()) /** - * @see TransactionDSLInterpreter._output + * Adds an output to the transaction. */ @JvmOverloads fun output(contractClassName: ContractClassName, @@ -108,24 +108,27 @@ class TransactionDSL(val interpreter: T) : Tr _output(contractClassName, label, notary, encumbrance, attachmentConstraint, contractStateClosure()) /** - * @see TransactionDSLInterpreter._output + * Adds a labelled output to the transaction. */ @JvmOverloads fun output(contractClassName: ContractClassName, label: String, contractState: ContractState, attachmentConstraint: AttachmentConstraint = AutomaticHashConstraint) = _output(contractClassName, label, DUMMY_NOTARY, null, attachmentConstraint, contractState) + /** + * Adds an output to the transaction. + */ @JvmOverloads fun output(contractClassName: ContractClassName, contractState: ContractState, attachmentConstraint: AttachmentConstraint = AutomaticHashConstraint) = _output(contractClassName,null, DUMMY_NOTARY, null, attachmentConstraint, contractState) /** - * @see TransactionDSLInterpreter._command + * Adds a command to the transaction. */ fun command(vararg signers: PublicKey, commandDataClosure: () -> CommandData) = _command(listOf(*signers), commandDataClosure()) /** - * @see TransactionDSLInterpreter._command + * Adds a command to the transaction. */ fun command(signer: PublicKey, commandData: CommandData) = _command(listOf(signer), commandData) From d13bf77473f06151659e5f8ab1f787475c0c3f3c Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Thu, 28 Sep 2017 17:46:36 +0100 Subject: [PATCH 038/185] CORDA-649: Improve stability of PersistentNetworkMapCacheTest (#1711) Improve stability of the NetworkMap test by ensuring that cluster of nodes is in a stable state before performing testing --- build.gradle | 1 + node/build.gradle | 1 + .../network/PersistentNetworkMapCacheTest.kt | 58 ++++++++++++------- .../services/messaging/P2PSecurityTest.kt | 3 + .../node/services/config/NodeConfiguration.kt | 14 ++++- .../messaging/ArtemisMessagingServer.kt | 8 +-- node/src/main/resources/reference.conf | 7 +++ .../config/FullNodeConfigurationTest.kt | 3 +- 8 files changed, 68 insertions(+), 27 deletions(-) rename node/src/{test => integration-test}/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt (77%) diff --git a/build.gradle b/build.gradle index 97a02e8c18..1a624d147a 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,7 @@ buildscript { ext.jersey_version = '2.25' ext.jolokia_version = '2.0.0-M3' ext.assertj_version = '3.6.1' + ext.kotlintest_version = '2.0.5' ext.slf4j_version = '1.7.25' ext.log4j_version = '2.7' ext.bouncycastle_version = constants.getProperty("bouncycastleVersion") diff --git a/node/build.gradle b/node/build.gradle index d6b76f8b45..f793837c13 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -132,6 +132,7 @@ dependencies { // Unit testing helpers. testCompile "junit:junit:$junit_version" testCompile "org.assertj:assertj-core:${assertj_version}" + testCompile "io.kotlintest:kotlintest:${kotlintest_version}" testCompile project(':test-utils') testCompile project(':client:jfx') testCompile project(':finance') diff --git a/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt similarity index 77% rename from node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt rename to node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt index 0d34b7d142..0e69839461 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt @@ -1,6 +1,8 @@ package net.corda.node.services.network import co.paralleluniverse.fibers.Suspendable +import io.kotlintest.eventually +import io.kotlintest.milliseconds import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession import net.corda.core.flows.InitiatedBy @@ -20,10 +22,12 @@ import kotlin.test.assertEquals import kotlin.test.assertFails import kotlin.test.assertTrue +private const val BRIDGE_RETRY_MS: Long = 100 + class PersistentNetworkMapCacheTest : NodeBasedTest() { - val partiesList = listOf(DUMMY_NOTARY, ALICE, BOB) - val addressesMap: HashMap = HashMap() - val infos: MutableSet = HashSet() + private val partiesList = listOf(DUMMY_NOTARY, ALICE, BOB) + private val addressesMap: HashMap = HashMap() + private val infos: MutableSet = HashSet() companion object { val logger = loggerFor() @@ -48,7 +52,7 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { val res = netCache.getNodeByLegalIdentity(alice.info.chooseIdentity()) assertEquals(alice.info, res) val res2 = netCache.getNodeByLegalName(DUMMY_NOTARY.name) - assertEquals(infos.filter { DUMMY_NOTARY.name in it.legalIdentitiesAndCerts.map { it.name } }.singleOrNull(), res2) + assertEquals(infos.singleOrNull { DUMMY_NOTARY.name in it.legalIdentitiesAndCerts.map { it.name } }, res2) } } @@ -111,19 +115,23 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { @Test fun `new node joins network without network map started`() { + + fun customNodesStart(parties: List): List> = + startNodesWithPort(parties, noNetworkMap = false, customRetryIntervalMs = BRIDGE_RETRY_MS) + val parties = partiesList.subList(1, partiesList.size) // Start 2 nodes pointing at network map, but don't start network map service. - val otherNodes = startNodesWithPort(parties, noNetworkMap = false) + val otherNodes = customNodesStart(parties) otherNodes.forEach { node -> assertTrue(infos.any { it.legalIdentitiesAndCerts.toSet() == node.info.legalIdentitiesAndCerts.toSet() }) } // Start node that is not in databases of other nodes. Point to NMS. Which has't started yet. - val charlie = startNodesWithPort(listOf(CHARLIE), noNetworkMap = false)[0] + val charlie = customNodesStart(listOf(CHARLIE)).single() otherNodes.forEach { assertThat(it.services.networkMapCache.allNodes).doesNotContain(charlie.info) } // Start Network Map and see that charlie node appears in caches. - val nms = startNodesWithPort(listOf(DUMMY_NOTARY), noNetworkMap = false)[0] + val nms = customNodesStart(listOf(DUMMY_NOTARY)).single() nms.internals.startupComplete.get() assertTrue(nms.inNodeNetworkMapService != NullNetworkMapService) assertTrue(infos.any { it.legalIdentities.toSet() == nms.info.legalIdentities.toSet() }) @@ -131,24 +139,34 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { assertTrue(nms.info.chooseIdentity() in it.services.networkMapCache.allNodes.map { it.chooseIdentity() }) } charlie.internals.nodeReadyFuture.get() // Finish registration. - logger.info("Checking connectivity") - checkConnectivity(listOf(otherNodes[0], nms)) // Checks connectivity from A to NMS. - logger.info("Loading caches") - val cacheA = otherNodes[0].services.networkMapCache.allNodes - val cacheB = otherNodes[1].services.networkMapCache.allNodes - val cacheC = charlie.services.networkMapCache.allNodes - logger.info("Performing verification") - assertEquals(4, cacheC.size) // Charlie fetched data from NetworkMap - assertThat(cacheB).contains(charlie.info) - assertEquals(cacheA.toSet(), cacheB.toSet()) - assertEquals(cacheA.toSet(), cacheC.toSet()) + + val allTheStartedNodesPopulation = otherNodes.plus(charlie).plus(nms) + + // This is prediction of the longest time it will take to get the cluster into a stable state such that further + // testing can be performed upon it + val maxInstabilityInterval = BRIDGE_RETRY_MS * allTheStartedNodesPopulation.size * 2 + + eventually(maxInstabilityInterval.milliseconds) { + logger.info("Checking connectivity") + checkConnectivity(listOf(otherNodes[0], nms)) // Checks connectivity from A to NMS. + logger.info("Loading caches") + val cacheA = otherNodes[0].services.networkMapCache.allNodes + val cacheB = otherNodes[1].services.networkMapCache.allNodes + val cacheC = charlie.services.networkMapCache.allNodes + logger.info("Performing verification") + assertEquals(4, cacheC.size) // Charlie fetched data from NetworkMap + assertThat(cacheB).contains(charlie.info) + assertEquals(cacheA.toSet(), cacheB.toSet()) + assertEquals(cacheA.toSet(), cacheC.toSet()) + } } // HELPERS // Helper function to restart nodes with the same host and port. - private fun startNodesWithPort(nodesToStart: List, noNetworkMap: Boolean = false): List> { + private fun startNodesWithPort(nodesToStart: List, noNetworkMap: Boolean = false, customRetryIntervalMs: Long? = null): List> { return nodesToStart.map { party -> - val configOverrides = addressesMap[party.name]?.let { mapOf("p2pAddress" to it.toString()) } ?: emptyMap() + val configOverrides = (addressesMap[party.name]?.let { mapOf("p2pAddress" to it.toString()) } ?: emptyMap()) + + (customRetryIntervalMs?.let { mapOf("activeMQServer.bridge.retryIntervalMs" to it.toString()) } ?: emptyMap()) if (party == DUMMY_NOTARY) { startNetworkMapNode(party.name, configOverrides = configOverrides) } diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt index 8d4b6fab55..317252e531 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt @@ -9,6 +9,8 @@ import net.corda.core.internal.cert import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds import net.corda.node.internal.NetworkMapInfo +import net.corda.node.services.config.ActiveMqServerConfiguration +import net.corda.node.services.config.BridgeConfiguration import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.messaging.sendRequest import net.corda.node.services.network.NetworkMapService @@ -60,6 +62,7 @@ class P2PSecurityTest : NodeBasedTest() { baseDirectory = baseDirectory(legalName), myLegalName = legalName).also { whenever(it.networkMapService).thenReturn(NetworkMapInfo(networkMapNode.internals.configuration.p2pAddress, networkMapNode.info.chooseIdentity().name)) + whenever(it.activeMQServer).thenReturn(ActiveMqServerConfiguration(BridgeConfiguration(1001, 2, 3.4))) } config.configureWithDevSSLCertificate() // This creates the node's TLS cert with the CN as the legal name return SimpleNode(config, trustRoot = trustRoot).apply { start() } 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 d078c1df48..7d500b2202 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 @@ -37,8 +37,17 @@ interface NodeConfiguration : NodeSSLConfiguration { val bftSMaRt: BFTSMaRtConfiguration val notaryNodeAddress: NetworkHostAndPort? val notaryClusterAddresses: List + val activeMQServer: ActiveMqServerConfiguration } +data class BridgeConfiguration( + val retryIntervalMs: Long, + val maxRetryIntervalMin: Long, + val retryIntervalMultiplier: Double +) + +data class ActiveMqServerConfiguration(val bridge: BridgeConfiguration) + data class FullNodeConfiguration( /** This is not retrieved from the config file but rather from a command line argument. */ override val baseDirectory: Path, @@ -68,7 +77,8 @@ data class FullNodeConfiguration( override val certificateChainCheckPolicies: List, override val devMode: Boolean = false, val useTestClock: Boolean = false, - val detectPublicIp: Boolean = true + val detectPublicIp: Boolean = true, + override val activeMQServer: ActiveMqServerConfiguration ) : NodeConfiguration { override val exportJMXto: String get() = "http" @@ -104,7 +114,7 @@ enum class CertChainPolicyType { MustContainOneOf } -data class CertChainPolicyConfig(val role: String, val policy: CertChainPolicyType, val trustedAliases: Set) { +data class CertChainPolicyConfig(val role: String, private val policy: CertChainPolicyType, private val trustedAliases: Set) { val certificateChainCheckPolicy: CertificateChainCheckPolicy get() { return when (policy) { CertChainPolicyType.Any -> CertificateChainCheckPolicy.Any 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 32c866da19..b648721633 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 @@ -57,6 +57,7 @@ import java.math.BigInteger import java.security.KeyStore import java.security.KeyStoreException import java.security.Principal +import java.time.Duration import java.util.* import java.util.concurrent.Executor import java.util.concurrent.ScheduledExecutorService @@ -390,10 +391,9 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, isUseDuplicateDetection = true // Enable the bridge's automatic deduplication logic // We keep trying until the network map deems the node unreachable and tells us it's been removed at which // point we destroy the bridge - // TODO Give some thought to the retry settings - retryInterval = 5.seconds.toMillis() - retryIntervalMultiplier = 1.5 // Exponential backoff - maxRetryInterval = 3.minutes.toMillis() + retryInterval = config.activeMQServer.bridge.retryIntervalMs + retryIntervalMultiplier = config.activeMQServer.bridge.retryIntervalMultiplier + maxRetryInterval = Duration.ofMinutes(config.activeMQServer.bridge.maxRetryIntervalMin).toMillis() // As a peer of the target node we must connect to it using the peer user. Actual authentication is done using // our TLS certificate. user = PEER_USER diff --git a/node/src/main/resources/reference.conf b/node/src/main/resources/reference.conf index 1b3711c8c4..37a91c5e5e 100644 --- a/node/src/main/resources/reference.conf +++ b/node/src/main/resources/reference.conf @@ -23,3 +23,10 @@ bftSMaRt = { replicaId = -1 debug = false } +activeMQServer = { + bridge = { + retryIntervalMs = 5000 + retryIntervalMultiplier = 1.5 + maxRetryIntervalMin = 3 + } +} diff --git a/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt index ef4e7128e4..b4cce7d46f 100644 --- a/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt @@ -34,7 +34,8 @@ class FullNodeConfigurationTest { notaryNodeAddress = null, notaryClusterAddresses = emptyList(), certificateChainCheckPolicies = emptyList(), - devMode = true) + devMode = true, + activeMQServer = ActiveMqServerConfiguration(BridgeConfiguration(0, 0, 0.0))) fun configWithRPCUsername(username: String) { testConfiguration.copy(rpcUsers = listOf(User(username, "pass", emptySet()))) From 5fa7381883f6d819b565a75d44f56a76dd4fd683 Mon Sep 17 00:00:00 2001 From: Matthew Nesbit Date: Thu, 28 Sep 2017 13:25:08 +0100 Subject: [PATCH 039/185] Custom exceptions in corda, should either derive from an appropriate closely related java exception, or CordaException, or CordaRuntimeException. They should not inherit just from Exception, or RuntimeException. Handle PR comments Add nicer constructors to CordaException and CordaRuntimeException (cherry picked from commit 89478c8) Fix ambiguous defaulted constructor (cherry picked from commit ec9bafe) Address PR comment Update a few more custom exceptions --- .../client/jackson/StringToMethodCallParser.kt | 3 ++- .../net/corda/client/rpc/PermissionException.kt | 4 ++-- .../main/kotlin/net/corda/core/CordaException.kt | 9 ++++++--- .../net/corda/core/crypto/PartialMerkleTree.kt | 3 ++- .../corda/core/internal/ResolveTransactionsFlow.kt | 3 ++- .../net/corda/core/node/services/IdentityService.kt | 3 ++- .../serialization/MissingAttachmentsException.kt | 3 ++- .../corda/core/transactions/MerkleTransaction.kt | 5 +++-- .../core/transactions/MissingContractAttachments.kt | 3 ++- .../net/corda/finance/contracts/FinanceTypes.kt | 3 ++- .../amqp/custom/ThrowableSerializer.kt | 2 +- .../internal/serialization/carpenter/Exceptions.kt | 11 +++++++---- .../kotlin/net/corda/node/internal/AbstractNode.kt | 5 +++-- .../src/main/kotlin/net/corda/node/internal/Node.kt | 3 ++- .../corda/node/services/api/ServiceHubInternal.kt | 5 +++-- .../node/services/network/NetworkMapService.kt | 13 +++++++------ .../services/network/PersistentNetworkMapCache.kt | 8 ++++---- .../services/persistence/NodeAttachmentService.kt | 3 ++- .../services/statemachine/StateMachineManager.kt | 3 ++- .../kotlin/net/corda/node/shell/InteractiveShell.kt | 3 ++- .../registration/NetworkRegistrationService.kt | 3 ++- .../main/kotlin/net/corda/irs/flows/RatesFixFlow.kt | 4 ++-- .../main/kotlin/net/corda/testing/driver/Driver.kt | 3 ++- .../src/main/kotlin/net/corda/testing/TestDSL.kt | 7 ++++--- 24 files changed, 68 insertions(+), 44 deletions(-) diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/StringToMethodCallParser.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/StringToMethodCallParser.kt index ddbf924c27..a49409eca2 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/StringToMethodCallParser.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/StringToMethodCallParser.kt @@ -6,6 +6,7 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import com.google.common.collect.HashMultimap import com.google.common.collect.Multimap import net.corda.client.jackson.StringToMethodCallParser.ParsedMethodCall +import net.corda.core.CordaException import org.slf4j.LoggerFactory import java.lang.reflect.Constructor import java.lang.reflect.Method @@ -146,7 +147,7 @@ open class StringToMethodCallParser @JvmOverloads constructor( } } - open class UnparseableCallException(command: String, cause: Throwable? = null) : Exception("Could not parse as a command: $command", cause) { + open class UnparseableCallException(command: String, cause: Throwable? = null) : CordaException("Could not parse as a command: $command", cause) { class UnknownMethod(val methodName: String) : UnparseableCallException("Unknown command name: $methodName") class MissingParameter(methodName: String, val paramName: String, command: String) : UnparseableCallException("Parameter $paramName missing from attempt to invoke $methodName in command: $command") class TooManyParameters(methodName: String, command: String) : UnparseableCallException("Too many parameters provided for $methodName: $command") diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/PermissionException.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/PermissionException.kt index 0498801989..71596c6e5e 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/PermissionException.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/PermissionException.kt @@ -1,10 +1,10 @@ package net.corda.client.rpc +import net.corda.core.CordaRuntimeException import net.corda.core.serialization.CordaSerializable /** * Thrown to indicate that the calling user does not have permission for something they have requested (for example * calling a method). */ -@CordaSerializable -class PermissionException(msg: String) : RuntimeException(msg) +class PermissionException(msg: String) : CordaRuntimeException(msg) diff --git a/core/src/main/kotlin/net/corda/core/CordaException.kt b/core/src/main/kotlin/net/corda/core/CordaException.kt index 4e5353fa77..52bbd82172 100644 --- a/core/src/main/kotlin/net/corda/core/CordaException.kt +++ b/core/src/main/kotlin/net/corda/core/CordaException.kt @@ -15,10 +15,11 @@ interface CordaThrowable { open class CordaException internal constructor(override var originalExceptionClassName: String? = null, private var _message: String? = null, private var _cause: Throwable? = null) : Exception(null, null, true, true), CordaThrowable { - constructor(message: String?, cause: Throwable?) : this(null, message, cause) + constructor(message: String?) : this(null, message, null) + override val message: String? get() = if (originalExceptionClassName == null) originalMessage else { if (originalMessage == null) "$originalExceptionClassName" else "$originalExceptionClassName: $originalMessage" @@ -59,10 +60,12 @@ open class CordaException internal constructor(override var originalExceptionCla } open class CordaRuntimeException(override var originalExceptionClassName: String?, - private var _message: String? = null, - private var _cause: Throwable? = null) : RuntimeException(null, null, true, true), CordaThrowable { + private var _message: String?, + private var _cause: Throwable?) : RuntimeException(null, null, true, true), CordaThrowable { constructor(message: String?, cause: Throwable?) : this(null, message, cause) + constructor(message: String?) : this(null, message, null) + override val message: String? get() = if (originalExceptionClassName == null) originalMessage else { if (originalMessage == null) "$originalExceptionClassName" else "$originalExceptionClassName: $originalMessage" diff --git a/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt b/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt index 0011198d75..77e06bf1c2 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt @@ -1,11 +1,12 @@ package net.corda.core.crypto +import net.corda.core.CordaException import net.corda.core.crypto.SecureHash.Companion.zeroHash import net.corda.core.serialization.CordaSerializable import java.util.* @CordaSerializable -class MerkleTreeException(val reason: String) : Exception("Partial Merkle Tree exception. Reason: $reason") +class MerkleTreeException(val reason: String) : CordaException("Partial Merkle Tree exception. Reason: $reason") /** * Building and verification of Partial Merkle Tree. diff --git a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt index f297fc7c5b..9ef8101bdd 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt @@ -2,6 +2,7 @@ package net.corda.core.internal import co.paralleluniverse.fibers.Suspendable import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession import net.corda.core.serialization.CordaSerializable @@ -63,7 +64,7 @@ class ResolveTransactionsFlow(private val txHashes: Set, } @CordaSerializable - class ExcessivelyLargeTransactionGraph : Exception() + class ExcessivelyLargeTransactionGraph : FlowException() /** Transaction for fetch attachments for */ private var signedTransaction: SignedTransaction? = null 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 41a60bb30d..645afb5550 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 @@ -1,5 +1,6 @@ package net.corda.core.node.services +import net.corda.core.CordaException import net.corda.core.contracts.PartyAndReference import net.corda.core.identity.* import java.security.InvalidAlgorithmParameterException @@ -105,4 +106,4 @@ interface IdentityService { fun partiesFromName(query: String, exactMatch: Boolean): Set } -class UnknownAnonymousPartyException(msg: String) : Exception(msg) +class UnknownAnonymousPartyException(msg: String) : CordaException(msg) diff --git a/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsException.kt b/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsException.kt index 08a920ab63..a5934be42d 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsException.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsException.kt @@ -1,7 +1,8 @@ package net.corda.core.serialization +import net.corda.core.CordaException import net.corda.core.crypto.SecureHash /** Thrown during deserialisation to indicate that an attachment needed to construct the [WireTransaction] is not found. */ @CordaSerializable -class MissingAttachmentsException(val ids: List) : Exception() \ No newline at end of file +class MissingAttachmentsException(val ids: List) : CordaException() \ No newline at end of file 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 0daaedc713..4cd1332659 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt @@ -1,5 +1,6 @@ package net.corda.core.transactions +import net.corda.core.CordaException import net.corda.core.contracts.* import net.corda.core.crypto.* import net.corda.core.identity.Party @@ -263,11 +264,11 @@ data class FilteredComponentGroup(override val groupIndex: Int, override val com * @param reason information about the exception. */ @CordaSerializable -class ComponentVisibilityException(val id: SecureHash, val reason: String) : Exception("Component visibility error for transaction with id:$id. Reason: $reason") +class ComponentVisibilityException(val id: SecureHash, val reason: String) : CordaException("Component visibility error for transaction with id:$id. Reason: $reason") /** Thrown when [FilteredTransaction.verify] fails. * @param id transaction's id. * @param reason information about the exception. */ @CordaSerializable -class FilteredTransactionVerificationException(val id: SecureHash, val reason: String) : Exception("Transaction with id:$id cannot be verified. Reason: $reason") +class FilteredTransactionVerificationException(val id: SecureHash, val reason: String) : CordaException("Transaction with id:$id cannot be verified. Reason: $reason") diff --git a/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt b/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt index ac75722df9..935ddfdd8d 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt @@ -2,6 +2,7 @@ package net.corda.core.transactions import net.corda.core.contracts.ContractState import net.corda.core.contracts.TransactionState +import net.corda.core.flows.FlowException import net.corda.core.serialization.CordaSerializable /** @@ -11,4 +12,4 @@ import net.corda.core.serialization.CordaSerializable */ @CordaSerializable class MissingContractAttachments(val states: List>) - : Exception("Cannot find contract attachments for ${states.map { it.contract }.distinct() }") \ No newline at end of file + : FlowException("Cannot find contract attachments for ${states.map { it.contract }.distinct() }") \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt b/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt index b79c9d01a6..13248bd58e 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt @@ -12,6 +12,7 @@ import net.corda.core.contracts.CommandData import net.corda.core.contracts.LinearState import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.TokenizableAssetInfo +import net.corda.core.flows.FlowException import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.TransactionBuilder @@ -199,7 +200,7 @@ enum class Frequency(val annualCompoundCount: Int, val offset: LocalDate.(Long) @CordaSerializable open class BusinessCalendar (val holidayDates: List) { @CordaSerializable - class UnknownCalendar(name: String) : Exception("$name not found") + class UnknownCalendar(name: String) : FlowException("$name not found") companion object { val calendars = listOf("London", "NewYork") diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ThrowableSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ThrowableSerializer.kt index 696b2616b9..ad8394db2d 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ThrowableSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ThrowableSerializer.kt @@ -67,7 +67,7 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy initialiseDatabasePersistence(insideTransaction: () -> T): T { val props = configuration.dataSourceProperties 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 efb817f54a..689ef67e20 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -1,6 +1,7 @@ package net.corda.node.internal import com.codahale.metrics.JmxReporter +import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture import net.corda.core.identity.CordaX500Name import net.corda.core.identity.PartyAndCertificate @@ -384,6 +385,6 @@ open class Node(override val configuration: FullNodeConfiguration, } } -class ConfigurationException(message: String) : Exception(message) +class ConfigurationException(message: String) : CordaException(message) data class NetworkMapInfo(val address: NetworkHostAndPort, val legalName: CordaX500Name) 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 5492f24c1d..c1fb4f0c1b 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 @@ -1,5 +1,6 @@ package net.corda.node.services.api +import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowInitiator @@ -62,9 +63,9 @@ interface NetworkMapCacheInternal : NetworkMapCache { } @CordaSerializable -sealed class NetworkCacheError : Exception() { +sealed class NetworkCacheException : CordaException("Network Cache Error") { /** Indicates a failure to deregister, because of a rejected request from the remote node */ - class DeregistrationFailed : NetworkCacheError() + class DeregistrationFailed : NetworkCacheException() } interface ServiceHubInternal : ServiceHub { diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt index 81448e1469..3e9d3984ae 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt @@ -1,5 +1,6 @@ package net.corda.node.services.network +import net.corda.core.CordaException import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SignedData import net.corda.core.crypto.isFulfilledBy @@ -189,7 +190,7 @@ abstract class AbstractNetworkMapService(services: ServiceHubInternal, } private fun addSubscriber(subscriber: MessageRecipients) { - if (subscriber !is SingleMessageRecipient) throw NodeMapError.InvalidSubscriber() + if (subscriber !is SingleMessageRecipient) throw NodeMapException.InvalidSubscriber() subscribers.locked { if (!containsKey(subscriber)) { put(subscriber, LastAcknowledgeInfo(mapVersion)) @@ -198,12 +199,12 @@ abstract class AbstractNetworkMapService(services: ServiceHubInternal, } private fun removeSubscriber(subscriber: MessageRecipients) { - if (subscriber !is SingleMessageRecipient) throw NodeMapError.InvalidSubscriber() + if (subscriber !is SingleMessageRecipient) throw NodeMapException.InvalidSubscriber() subscribers.locked { remove(subscriber) } } private fun processAcknowledge(request: UpdateAcknowledge): Unit { - if (request.replyTo !is SingleMessageRecipient) throw NodeMapError.InvalidSubscriber() + if (request.replyTo !is SingleMessageRecipient) throw NodeMapException.InvalidSubscriber() subscribers.locked { val lastVersionAcked = this[request.replyTo]?.mapVersion if ((lastVersionAcked ?: 0) < request.mapVersion) { @@ -360,13 +361,13 @@ class WireNodeRegistration(raw: SerializedBytes, sig: DigitalS } @CordaSerializable -sealed class NodeMapError : Exception() { +sealed class NodeMapException : CordaException("Network Map Protocol Error") { /** Thrown if the signature on the node info does not match the public key for the identity */ - class InvalidSignature : NodeMapError() + class InvalidSignature : NodeMapException() /** Thrown if the replyTo of a subscription change message is not a single message recipient */ - class InvalidSubscriber : NodeMapError() + class InvalidSubscriber : NodeMapException() } @CordaSerializable 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 1105c4981c..3260da43ca 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 @@ -21,7 +21,7 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor import net.corda.core.utilities.parsePublicKeyBase58 import net.corda.core.utilities.toBase58String -import net.corda.node.services.api.NetworkCacheError +import net.corda.node.services.api.NetworkCacheException import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.messaging.MessagingService @@ -135,7 +135,7 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) data = NetworkMapService.UpdateAcknowledge(req.mapVersion, network.myAddress).serialize().bytes) network.send(ackMessage, req.replyTo) processUpdatePush(req) - } catch (e: NodeMapError) { + } catch (e: NodeMapException) { logger.warn("Failure during node map update due to bad update: ${e.javaClass.name}") } catch (e: Exception) { logger.error("Exception processing update from network map service", e) @@ -202,7 +202,7 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) val address = getPartyInfo(mapParty)?.let { network.getAddressOfParty(it) } ?: throw IllegalArgumentException("Can't deregister for updates, don't know the party: $mapParty") val future = network.sendRequest(NetworkMapService.SUBSCRIPTION_TOPIC, req, address).map { - if (it.confirmed) Unit else throw NetworkCacheError.DeregistrationFailed() + if (it.confirmed) Unit else throw NetworkCacheException.DeregistrationFailed() } _registrationFuture.captureLater(future.map { null }) return future @@ -213,7 +213,7 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) val reg = req.wireReg.verified() processRegistration(reg) } catch (e: SignatureException) { - throw NodeMapError.InvalidSignature() + throw NodeMapException.InvalidSignature() } } 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 b6acddd45e..3a175effb4 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,6 +6,7 @@ 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.CordaRuntimeException import net.corda.core.internal.AbstractAttachment import net.corda.core.contracts.Attachment import net.corda.core.crypto.SecureHash @@ -58,7 +59,7 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single } @CordaSerializable - class HashMismatchException(val expected: SecureHash, val actual: SecureHash) : RuntimeException("File $expected hashed to $actual: corruption in attachment store?") + class HashMismatchException(val expected: SecureHash, val actual: SecureHash) : CordaRuntimeException("File $expected hashed to $actual: corruption in attachment store?") /** * Wraps a stream and hashes data as it is read: if the entire stream is consumed, then at the end the hash of diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index 2382614752..11c3d219bc 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -9,6 +9,7 @@ import com.codahale.metrics.Gauge import com.esotericsoftware.kryo.KryoException import com.google.common.collect.HashMultimap import com.google.common.util.concurrent.MoreExecutors +import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.SecureHash import net.corda.core.crypto.random63BitValue @@ -641,6 +642,6 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } } -class SessionRejectException(val rejectMessage: String, val logMessage: String) : Exception() { +class SessionRejectException(val rejectMessage: String, val logMessage: String) : CordaException(rejectMessage) { constructor(message: String) : this(message, message) } diff --git a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt b/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt index 35e695d78e..07ee7e3f96 100644 --- a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt +++ b/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt @@ -23,6 +23,7 @@ import net.corda.core.messaging.StateMachineUpdate import net.corda.core.utilities.loggerFor import net.corda.client.jackson.JacksonSupport import net.corda.client.jackson.StringToMethodCallParser +import net.corda.core.CordaException import net.corda.node.internal.Node import net.corda.node.internal.StartedNode import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT @@ -258,7 +259,7 @@ object InteractiveShell { } } - class NoApplicableConstructor(val errors: List) : Exception() { + class NoApplicableConstructor(val errors: List) : CordaException(this.toString()) { override fun toString() = (listOf("No applicable constructor for flow. Problems were:") + errors).joinToString(System.lineSeparator()) } diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationService.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationService.kt index e22121b2e2..ffe8bece0f 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationService.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationService.kt @@ -1,5 +1,6 @@ package net.corda.node.utilities.registration +import net.corda.core.CordaException import net.corda.core.serialization.CordaSerializable import org.bouncycastle.pkcs.PKCS10CertificationRequest import java.security.cert.Certificate @@ -14,4 +15,4 @@ interface NetworkRegistrationService { } @CordaSerializable -class CertificateRequestException(message: String) : Exception(message) +class CertificateRequestException(message: String) : CordaException(message) diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt index 285d477a69..c2370a8ddf 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt @@ -3,6 +3,7 @@ package net.corda.irs.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.isFulfilledBy +import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession import net.corda.core.flows.InitiatingFlow @@ -44,8 +45,7 @@ open class RatesFixFlow(protected val tx: TransactionBuilder, fun tracker(fixName: String) = ProgressTracker(QUERYING(fixName), WORKING, SIGNING) } - @CordaSerializable - class FixOutOfRange(@Suppress("unused") val byAmount: BigDecimal) : Exception("Fix out of range by $byAmount") + class FixOutOfRange(@Suppress("unused") val byAmount: BigDecimal) : FlowException("Fix out of range by $byAmount") @CordaSerializable data class QueryRequest(val queries: List) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index c279e0aa4d..64645569a4 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -9,6 +9,7 @@ import net.corda.client.rpc.CordaRPCClient import net.corda.cordform.CordformContext import net.corda.cordform.CordformNode import net.corda.cordform.NodeDefinition +import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.firstOf import net.corda.core.identity.CordaX500Name @@ -411,7 +412,7 @@ fun getTimestampAsDirectoryName(): String { return DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(UTC).format(Instant.now()) } -class ListenProcessDeathException(hostAndPort: NetworkHostAndPort, listenProcess: Process) : Exception("The process that was expected to listen on $hostAndPort has died with status: ${listenProcess.exitValue()}") +class ListenProcessDeathException(hostAndPort: NetworkHostAndPort, listenProcess: Process) : CordaException("The process that was expected to listen on $hostAndPort has died with status: ${listenProcess.exitValue()}") /** * @throws ListenProcessDeathException if [listenProcess] dies before the check succeeds, i.e. the check can't succeed as intended. diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt index 6db7662380..d157583c2e 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt @@ -4,6 +4,7 @@ import net.corda.core.contracts.* import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.* import net.corda.core.crypto.NullKeys.NULL_SIGNATURE +import net.corda.core.flows.FlowException import net.corda.core.identity.Party import net.corda.core.internal.uncheckedCast import net.corda.core.node.ServiceHub @@ -52,9 +53,9 @@ sealed class EnforceVerifyOrFail { internal object Token : EnforceVerifyOrFail() } -class DuplicateOutputLabel(label: String) : Exception("Output label '$label' already used") -class DoubleSpentInputs(ids: List) : Exception("Transactions spend the same input. Conflicting transactions ids: '$ids'") -class AttachmentResolutionException(attachmentId: SecureHash) : Exception("Attachment with id $attachmentId not found") +class DuplicateOutputLabel(label: String) : FlowException("Output label '$label' already used") +class DoubleSpentInputs(ids: List) : FlowException("Transactions spend the same input. Conflicting transactions ids: '$ids'") +class AttachmentResolutionException(attachmentId: SecureHash) : FlowException("Attachment with id $attachmentId not found") /** * This interpreter builds a transaction, and [TransactionDSL.verifies] that the resolved transaction is correct. Note From bdc33892062958b4dc9cc6ef34693a33cee7d118 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Fri, 29 Sep 2017 12:23:40 +0100 Subject: [PATCH 040/185] CORDA-540: Fix exception type thrown in AMQP mode (#1680) --- .../corda/core/transactions/SignedTransaction.kt | 9 ++++++++- .../serialization/amqp/PropertySerializer.kt | 14 ++++++++++++-- .../serialization/amqp/SerializationHelper.kt | 2 +- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt index 31857af7e9..2705e02ef9 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -1,5 +1,7 @@ package net.corda.core.transactions +import net.corda.core.CordaException +import net.corda.core.CordaThrowable import net.corda.core.contracts.* import net.corda.core.crypto.* import net.corda.core.identity.Party @@ -187,7 +189,12 @@ data class SignedTransaction(val txBits: SerializedBytes, override fun toString(): String = "${javaClass.simpleName}(id=$id)" + companion object { + private fun missingSignatureMsg(missing: Set, descriptions: List, id: SecureHash): String = + "Missing signatures for $descriptions on transaction ${id.prefixChars()} for ${missing.joinToString()}" + } + @CordaSerializable class SignaturesMissingException(val missing: Set, val descriptions: List, override val id: SecureHash) - : NamedByHash, SignatureException("Missing signatures for $descriptions on transaction ${id.prefixChars()} for ${missing.joinToString()}") + : NamedByHash, SignatureException(missingSignatureMsg(missing, descriptions, id)), CordaThrowable by CordaException(missingSignatureMsg(missing, descriptions, id)) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt index 2decd00d37..7c7714b9e9 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.utilities.loggerFor import org.apache.qpid.proton.amqp.Binary import org.apache.qpid.proton.codec.Data import java.lang.reflect.Method @@ -48,11 +49,20 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r } private fun Method.returnsNullable(): Boolean { - val returnTypeString = this.declaringClass.kotlin.memberProperties.firstOrNull { it.javaGetter == this }?.returnType?.toString() ?: "?" - return returnTypeString.endsWith('?') || returnTypeString.endsWith('!') + try { + val returnTypeString = this.declaringClass.kotlin.memberProperties.firstOrNull { it.javaGetter == this }?.returnType?.toString() ?: "?" + return returnTypeString.endsWith('?') || returnTypeString.endsWith('!') + } catch(e: kotlin.reflect.jvm.internal.KotlinReflectionInternalError) { + // This might happen for some types, e.g. kotlin.Throwable? - the root cause of the issue is: https://youtrack.jetbrains.com/issue/KT-13077 + // TODO: Revisit this when Kotlin issue is fixed. + logger.error("Unexpected internal Kotlin error", e) + return true + } } companion object { + private val logger = loggerFor() + fun make(name: String, readMethod: Method?, resolvedType: Type, factory: SerializerFactory): PropertySerializer { readMethod?.isAccessible = true if (SerializerFactory.isPrimitive(resolvedType)) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt index ab218fb247..6a1377d083 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt @@ -82,7 +82,7 @@ private fun propertiesForSerializationFromConstructor(kotlinConstructo for (param in kotlinConstructor.parameters) { val name = param.name ?: throw NotSerializableException("Constructor parameter of $clazz has no name.") val matchingProperty = properties[name] ?: - throw NotSerializableException("No property matching constructor parameter named $name of $clazz." + + throw NotSerializableException("No property matching constructor parameter named '$name' of '$clazz'." + " If using Java, check that you have the -parameters option specified in the Java compiler.") // Check that the method has a getter in java. val getter = matchingProperty.readMethod ?: throw NotSerializableException("Property has no getter method for $name of $clazz." + From ddee4b94e2e87638e36f428a9a6e0bdb20f3ff39 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Fri, 29 Sep 2017 12:27:48 +0100 Subject: [PATCH 041/185] CORDA-594 - SIMM Demo doc update (#1723) * CORDA-594 - SIMM Demo doc update For V1 write a series of JSON / curl commands a user can follow to run the demo * Review Comments * Updated the rationale behind as to why SIMM was introduced. * typo --- docs/source/running-the-demos.rst | 214 +++++++++++++++++++++++ samples/simm-valuation-demo/build.gradle | 16 +- 2 files changed, 226 insertions(+), 4 deletions(-) diff --git a/docs/source/running-the-demos.rst b/docs/source/running-the-demos.rst index cd19580688..32a99967c6 100644 --- a/docs/source/running-the-demos.rst +++ b/docs/source/running-the-demos.rst @@ -218,3 +218,217 @@ Using the following login details: See https://docs.corda.net/node-explorer.html for further details on usage. +.. _simm-demo: + +SIMM and Portfolio Demo - aka the Initial Margin Agreement Demo +--------------------------------------------------------------- + +Background and SIMM Introduction +******************************** + +This app is a demonstration of how Corda can be used for the real world requirement of initial margin calculation and +agreement; featuring the integration of complex and industry proven third party libraries into Corda nodes. + +SIMM is an acronym for "Standard Initial Margin Model". It is effectively the calculation of a "margin" that is paid +by one party to another when they agree a trade on certain types of transaction. + +The SIMM was introduced to standardise the calculation of how much margin counterparties charge each other on their +bilateral transactions. Before SIMM, each counterparty computed margins according to its own model and it was made it very + difficult to agree the exact margin with the counterparty that faces the same trade on the other side. + +To enact this, in September 2016, the ISDA committee - with full backing from various governing bodies - +`issued a ruling on what is known as the ISDA SIMM ™ model `_, +a way of fairly and consistently calculating this margin. Any parties wishing to trade a financial product that is +covered under this ruling would, independently, use this model and calculate their margin payment requirement, +agree it with their trading counterparty and then pay (or receive, depending on the results of this calculation) +this amount. In the case of disagreement that is not resolved in a timely fashion, this payment would increase +and so therefore it is in the parties' interest to reach agreement in as short as time frame as possible. + +To be more accurate, the SIMM calculation is not performed on just one trade - it is calculated on an aggregate of +intermediary values (which in this model are sensitivities to risk factors) from a portfolio of trades; therefore +the input to a SIMM is actually this data, not the individual trades themselves. + +Also note that implementations of the SIMM are actually protected and subject to license restrictions by ISDA +(this is due to the model itself being protected). We were fortunate enough to technically partner with +`OpenGamma `_ who allowed us to demonstrate the SIMM process using their proprietary model. +In the source code released, we have replaced their analytics engine with very simple stub functions that allow +the process to run without actually calculating correct values, and can easily be swapped out in place for their real libraries. + +What happens in the demo (notionally) +************************************* + +Preliminaries + - Ensure that there are a number of live trades with another party based on financial products that are covered under the + ISDA SIMM agreement (if none, then use the demo to enter some simple trades as described below). + +Initial Margin Agreement Process + - Agree that one will be performing the margining calculation against a portfolio of trades with another party, and agree the trades in that portfolio. In practice, one node will start the flow but it does not matter which node does. + - Individually (at the node level), identify the data (static, reference etc) one will need in order to be able to calculate the metrics on those trades + - Confirm with the other counterparty the dataset from the above set + - Calculate any intermediary steps and values needed for the margin calculation (ie sensitivities to risk factors) + - Agree on the results of these steps + - Calculate the initial margin + - Agree on the calculation of the above with the other party + - In practice, pay (or receive) this margin (omitted for the sake of complexity for this example) + +Demo execution (step by step) +***************************** + +**Setting up the Corda infrastructure** + +To run from the command line in Unix: + +1. Deploy the nodes using ``./gradlew samples:simm-valuation-demo:deployNodes`` +2. Run the nodes using ``./samples/simm-valuation-demo/build/nodes/runnodes`` + +To run from the command line in Windows: + +1. Deploy the nodes using ``gradlew samples:simm-valuation-demo:deployNodes`` +2. Run the nodes using ``samples\simm-valuation-demo\build\nodes\runnodes`` + +**Getting Bank A's details** + +From the command line run + +.. sourcecode:: bash + + curl http://localhost:10005/api/simmvaluationdemo/whoami + +The response should be something like + +.. sourcecode:: none + + { + "self" : { + "id" : "8Kqd4oWdx4KQGHGQW3FwXHQpjiv7cHaSsaAWMwRrK25bBJj792Z4rag7EtA", + "text" : "C=GB,L=London,O=Bank A" + }, + "counterparties" : [ + { + "id" : "8Kqd4oWdx4KQGHGL1DzULumUmZyyokeSGJDY1n5M6neUfAj2sjbf65wYwQM", + "text" : "C=JP,L=Tokyo,O=Bank C" + }, + { + "id" : "8Kqd4oWdx4KQGHGTBm34eCM2nrpcWKeM1ZG3DUYat3JTFUQTwB3Lv2WbPM8", + "text" : "C=US,L=New York,O=Bank B" + } + ] + } + +Now, if we ask the same question of Bank C we will see that it's id matches the id for Bank C as a counter +party to Bank A and Bank A will appear as a counter party + +.. sourcecode:: bash + + curl -i -H "Content-Type: application/json" -X GET http://localhost:10011/api/simmvaluationdemo/whoami + +**Creating a trade with Bank C** + +In what follows, we assume we are Bank A (which is listening on port 10005) + +Notice the id field in the output of the ``whoami`` command. We are going to use the id assocatied +with Bank C, one of our counter parties, to create a trade. The general command for this is: + +.. sourcecode:: bash + + curl -i -H "Content-Type: application/json" -X PUT -d <<>> http://localhost:10005/api/simmvaluationdemo/<<>>/trades + +where the representation of the trade is + +.. sourcecode:: none + + { + "id" : "trade1", + "description" : "desc", + "tradeDate" : [ 2016, 6, 6 ], + "convention" : "EUR_FIXED_1Y_EURIBOR_3M", + "startDate" : [ 2016, 6, 6 ], + "endDate" : [ 2020, 1, 2 ], + "buySell" : "BUY", + "notional" : "1000", + "fixedRate" : "0.1" + } + +Continuing our example, the specific command we would run is + +.. sourcecode:: bash + + curl -i -H "Content-Type: application/json" \ + -X PUT \ + -d '{"id":"trade1","description" : "desc","tradeDate" : [ 2016, 6, 6 ], "convention" : "EUR_FIXED_1Y_EURIBOR_3M", "startDate" : [ 2016, 6, 6 ], "endDate" : [ 2020, 1, 2 ], "buySell" : "BUY", "notional" : "1000", "fixedRate" : "0.1"}' \ + http://localhost:10005/api/simmvaluationdemo/8Kqd4oWdx4KQGHGL1DzULumUmZyyokeSGJDY1n5M6neUfAj2sjbf65wYwQM/trades + +With an expected response of + +.. sourcecode:: none + + HTTP/1.1 202 Accepted + Date: Thu, 28 Sep 2017 17:19:39 GMT + Content-Type: text/plain + Access-Control-Allow-Origin: * + Content-Length: 2 + Server: Jetty(9.3.9.v20160517) + +**Verifying trade completion** + +With the trade completed and stored by both parties, the complete list of trades with our couterparty can be seen with the following command + +.. sourcecode:: bash + + curl -X GET http://localhost:10005/api/simmvaluationdemo/<<>>/trades + +The command for our example, using Bank A, would thus be + +.. sourcecode:: bash + + curl -X GET http://localhost:10005/api/simmvaluationdemo/8Kqd4oWdx4KQGHGL1DzULumUmZyyokeSGJDY1n5M6neUfAj2sjbf65wYwQM/trades + +whilst a specific trade can be seen with + +.. sourcecode:: bash + + curl -X GET http://localhost:10005/api/simmvaluationdemo/<<>>/trades/<<>> + +If we look at the trade we created above, we assigned it the id "trade1", the complete command in this case would be + +.. sourcecode:: bash + + curl -X GET http://localhost:10005/api/simmvaluationdemo/8Kqd4oWdx4KQGHGL1DzULumUmZyyokeSGJDY1n5M6neUfAj2sjbf65wYwQM/trades/trade1 + +**Generating a valuation** + +.. sourcecode:: bash + + curl -i -H "Content-Type: application/json" \ + -X POST \ + -d <<>> + http://localhost:10005/api/simmvaluationdemo/<<>>/portfolio/valuations/calculate + +Again, the specific command to continue our example would be + +.. sourcecode:: bash + + curl -i -H "Content-Type: application/json" \ + -X POST \ + -d '{"valuationDate":[2016,6,6]}' \ + http://localhost:10005/api/simmvaluationdemo/8Kqd4oWdx4KQGHGL1DzLumUmZyyokeSGJDY1n5M6neUfAj2sjbf65wYwQM/portfolio/valuations/calculate + +**Viewing a valuation** + +In the same way we can ask for specific instances of trades with a counter party, we can request details of valuations + +.. sourcecode:: bash + + curl -i -H "Content-Type: application/json" -X GET http://localhost:10005/api/simmvaluationdemo/<<>>/portfolio/valuations + +The specific command for out Bank A example is + +.. sourcecode:: bash + + curl -i -H "Content-Type: application/json" \ + -X GET http://localhost:10005/api/simmvaluationdemo/8Kqd4oWdx4KQGHGL1DzULumUmZyyokeSGJDY1n5M6neUfAj2sjbf65YwQM/portfolio/valuations + + + + + diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index b06cef6212..1d7d654ee8 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -62,6 +62,8 @@ dependencies { } task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { + ext.rpcUsers = [['username': "default", 'password': "default", 'permissions': [ 'ALL' ]]] + directory "./build/nodes" networkMap "O=Notary Service,L=Zurich,C=CH" node { @@ -75,21 +77,27 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { advertisedServices = [] p2pPort 10004 webPort 10005 + rpcPort 10006 cordapps = ["net.corda:finance:$corda_release_version"] + rpcUsers = ext.rpcUsers } node { name "O=Bank B,L=New York,C=US" advertisedServices = [] - p2pPort 10006 - webPort 10007 + p2pPort 10007 + webPort 10008 + rpcPort 10009 cordapps = ["net.corda:finance:$corda_release_version"] + rpcUsers = ext.rpcUsers } node { name "O=Bank C,L=Tokyo,C=JP" advertisedServices = [] - p2pPort 10008 - webPort 10009 + p2pPort 10010 + webPort 10011 + rpcPort 10012 cordapps = ["net.corda:finance:$corda_release_version"] + rpcUsers = ext.rpcUsers } } From 7787896cbbd917a81fdc277015835b963fa66f98 Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Fri, 29 Sep 2017 13:13:38 +0100 Subject: [PATCH 042/185] Hide SerializationContext from public API on TransactionBuilder (#1715) --- .../net/corda/core/internal/InternalUtils.kt | 21 ++++++++++++++++++- .../core/transactions/TransactionBuilder.kt | 9 +++++--- .../internal/AttachmentsClassLoaderTests.kt | 2 ++ .../node/services/AttachmentLoadingTests.kt | 1 + 4 files changed, 29 insertions(+), 4 deletions(-) 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 5d3eafdca1..ccc6cee84c 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -2,6 +2,11 @@ package net.corda.core.internal import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 +import net.corda.core.node.ServiceHub +import net.corda.core.node.ServicesForResolution +import net.corda.core.serialization.SerializationContext +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.transactions.WireTransaction import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter import org.slf4j.Logger @@ -273,4 +278,18 @@ annotation class VisibleForTesting @Suppress("UNCHECKED_CAST") fun uncheckedCast(obj: T) = obj as U -fun Iterable>.toMultiMap(): Map> = this.groupBy({ it.first }) { it.second } \ No newline at end of file +fun Iterable>.toMultiMap(): Map> = this.groupBy({ it.first }) { it.second } + +/** + * Provide access to internal method for AttachmentClassLoaderTests + * @suppress + */ +fun TransactionBuilder.toWireTransaction(services: ServicesForResolution, serializationContext: SerializationContext): WireTransaction { + return toWireTransactionWithContext(services, serializationContext) +} + +/** + * Provide access to internal method for AttachmentClassLoaderTests + * @suppress + */ +fun TransactionBuilder.toLedgerTransaction(services: ServiceHub, serializationContext: SerializationContext) = toLedgerTransactionWithContext(services, serializationContext) diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 9f972ee155..ad6c538451 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -81,9 +81,10 @@ open class TransactionBuilder( * * @returns A new [WireTransaction] that will be unaffected by further changes to this [TransactionBuilder]. */ - @JvmOverloads @Throws(MissingContractAttachments::class) - fun toWireTransaction(services: ServicesForResolution, serializationContext: SerializationContext? = null): WireTransaction { + fun toWireTransaction(services: ServicesForResolution): WireTransaction = toWireTransactionWithContext(services) + + internal fun toWireTransactionWithContext(services: ServicesForResolution, serializationContext: SerializationContext? = null): WireTransaction { // Resolves the AutomaticHashConstraints to HashAttachmentConstraints for convenience. The AutomaticHashConstraint // allows for less boiler plate when constructing transactions since for the typical case the named contract // will be available when building the transaction. In exceptional cases the TransactionStates must be created @@ -103,7 +104,9 @@ open class TransactionBuilder( } @Throws(AttachmentResolutionException::class, TransactionResolutionException::class) - fun toLedgerTransaction(services: ServiceHub, serializationContext: SerializationContext? = null) = toWireTransaction(services, serializationContext).toLedgerTransaction(services) + fun toLedgerTransaction(services: ServiceHub) = toWireTransaction(services).toLedgerTransaction(services) + + internal fun toLedgerTransactionWithContext(services: ServiceHub, serializationContext: SerializationContext) = toWireTransactionWithContext(services, serializationContext).toLedgerTransaction(services) @Throws(AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class) fun verify(services: ServiceHub) { diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt index 5fc205e666..9d756a8812 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt @@ -5,6 +5,8 @@ import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.internal.declaredField +import net.corda.core.internal.toLedgerTransaction +import net.corda.core.internal.toWireTransaction import net.corda.core.node.ServiceHub import net.corda.core.node.services.AttachmentStorage import net.corda.core.serialization.* 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 4a344aec4e..353203e557 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 @@ -10,6 +10,7 @@ import net.corda.core.identity.Party import net.corda.core.internal.concurrent.transpose import net.corda.core.internal.createDirectories import net.corda.core.internal.div +import net.corda.core.internal.toLedgerTransaction import net.corda.core.serialization.SerializationFactory import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes From c2b0ed6ed5b7f152f1f8281669f6df1670036790 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Fri, 29 Sep 2017 13:43:42 +0100 Subject: [PATCH 043/185] CORDA-650: Eliminate unnecessary dependency on "graphstream" library (#1732) This library has one usage in ClientRPC example and: * Uses Scala 2.10 - which is quite dated * Has a transitive dependency on JUnit --- docs/source/example-code/build.gradle | 1 + node/build.gradle | 5 ----- samples/network-visualiser/build.gradle | 2 -- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/source/example-code/build.gradle b/docs/source/example-code/build.gradle index 46336ef9f0..cf0c3046e8 100644 --- a/docs/source/example-code/build.gradle +++ b/docs/source/example-code/build.gradle @@ -39,6 +39,7 @@ dependencies { compile "org.graphstream:gs-core:1.3" compile("org.graphstream:gs-ui:1.3") { exclude group: "bouncycastle" + exclude group: "junit" } cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts') diff --git a/node/build.gradle b/node/build.gradle index f793837c13..6a0c142c9f 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -108,11 +108,6 @@ dependencies { // Manifests: for reading stuff from the manifest file compile "com.jcabi:jcabi-manifests:1.1" - // GraphStream: For visualisation - testCompile "org.graphstream:gs-core:1.3" - testCompile("org.graphstream:gs-ui:1.3") { - exclude group: "bouncycastle" - } compile("com.intellij:forms_rt:7.0.3") { exclude group: "asm" } diff --git a/samples/network-visualiser/build.gradle b/samples/network-visualiser/build.gradle index d472b1e5b3..72ba1207c3 100644 --- a/samples/network-visualiser/build.gradle +++ b/samples/network-visualiser/build.gradle @@ -21,8 +21,6 @@ dependencies { // Cordapp dependencies // GraphStream: For visualisation compileOnly "co.paralleluniverse:capsule:$capsule_version" - compile "org.graphstream:gs-core:1.3" - compile "org.graphstream:gs-ui:1.3" } idea { From 2704165d7a358eca6b0fc9705ab6cc2a77e31625 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Fri, 29 Sep 2017 14:57:47 +0100 Subject: [PATCH 044/185] CORDA-649: Disable unstable test (#1733) --- .../node/services/network/PersistentNetworkMapCacheTest.kt | 2 ++ 1 file changed, 2 insertions(+) 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 0e69839461..4da002b539 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 @@ -17,6 +17,7 @@ import net.corda.testing.* import net.corda.testing.node.NodeBasedTest import org.assertj.core.api.Assertions.assertThat import org.junit.Before +import org.junit.Ignore import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertFails @@ -113,6 +114,7 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { assertFails { startNode(CHARLIE.name, noNetworkMap = true).getOrThrow(2.seconds) } } + @Ignore("Unstable test that needs more work") @Test fun `new node joins network without network map started`() { From cac739e48d349369e5329ed2f215d7450344b1eb Mon Sep 17 00:00:00 2001 From: Katarzyna Streich Date: Fri, 29 Sep 2017 15:38:05 +0100 Subject: [PATCH 045/185] Add explanation to NetworkMapCache docs. (#1727) NodeInfo lookup can return more than one node for distribute services. --- .../main/kotlin/net/corda/core/messaging/CordaRPCOps.kt | 2 ++ .../net/corda/core/node/services/NetworkMapCache.kt | 8 +++++++- .../node/services/network/PersistentNetworkMapCache.kt | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) 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 50664c52e3..60979b02cb 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -271,6 +271,8 @@ interface CordaRPCOps : RPCOps { /** * Returns a node's info from the network map cache, where known. + * Notice that when there are more than one node for a given name (in case of distributed services) first service node + * found will be returned. * * @return the node info if available. */ diff --git a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt index 8eb0a52a07..428a656220 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt @@ -50,6 +50,8 @@ interface NetworkMapCache { * Look up the node info for a specific party. Will attempt to de-anonymise the party if applicable; if the party * is anonymised and the well known party cannot be resolved, it is impossible ot identify the node and therefore this * returns null. + * Notice that when there are more than one node for a given party (in case of distributed services) first service node + * found will be returned. See also: [getNodesByLegalIdentityKey]. * * @param party party to retrieve node information for. * @return the node for the identity, or null if the node could not be found. This does not necessarily mean there is @@ -57,7 +59,11 @@ interface NetworkMapCache { */ fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? - /** Look up the node info for a legal name. */ + /** + * Look up the node info for a legal name. + * Notice that when there are more than one node for a given name (in case of distributed services) first service node + * found will be returned. + */ fun getNodeByLegalName(name: CordaX500Name): NodeInfo? /** Look up the node info for a host and port. */ 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 3260da43ca..400db60ef3 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 @@ -112,7 +112,7 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) override fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? { val wellKnownParty = serviceHub.identityService.wellKnownPartyFromAnonymous(party) return wellKnownParty?.let { - getNodesByLegalIdentityKey(it.owningKey).singleOrNull() + getNodesByLegalIdentityKey(it.owningKey).firstOrNull() } } From de88f3ecba6e7d2c725919a905b08c179646cfaf Mon Sep 17 00:00:00 2001 From: Clinton Date: Fri, 29 Sep 2017 16:29:09 +0100 Subject: [PATCH 046/185] Contract constraints documentation (#1703) * Added a contract constraints section to the key concepts doc. * Documentation for contract constraints. * Added to index. * Review fixes round 1. * More review fixes. * Review fixes. * Explained package contents. * review fixes. * Addressed RGB's final review comments. * Updated source code type to 'java' --- docs/packages.md | 6 + docs/source/cordapp-overview.rst | 2 +- .../key-concepts-contract-constraints.rst | 149 ++++++++++++++++++ docs/source/key-concepts.rst | 1 + 4 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 docs/source/key-concepts-contract-constraints.rst diff --git a/docs/packages.md b/docs/packages.md index 61a4b2cb93..2c2090e790 100644 --- a/docs/packages.md +++ b/docs/packages.md @@ -24,6 +24,12 @@ RPC client interface to Corda, for use both by user-facing client and integratio Internal, do not use. These APIs and implementations which are currently being revised and are subject to future change. +# Package net.corda.core.cordapp + +This package contains the interface to CorDapps from within a node. A CorDapp can access its own context by using +the CordappProvider.getAppContext() class. These classes are not intended to be constructed manually and no interface +to do this will be provided. + # Package net.corda.core.concurrent Provides a simplified [java.util.concurrent.Future] class that allows registration of a callback to execute when the future diff --git a/docs/source/cordapp-overview.rst b/docs/source/cordapp-overview.rst index 2999489ef6..6ac7bbedd3 100644 --- a/docs/source/cordapp-overview.rst +++ b/docs/source/cordapp-overview.rst @@ -22,4 +22,4 @@ CorDapps are made up of definitions for the following components: * Contracts * Flows * Web APIs and static web content -* Services +* Services \ No newline at end of file diff --git a/docs/source/key-concepts-contract-constraints.rst b/docs/source/key-concepts-contract-constraints.rst new file mode 100644 index 0000000000..6c3e536264 --- /dev/null +++ b/docs/source/key-concepts-contract-constraints.rst @@ -0,0 +1,149 @@ +Contract Constraints +==================== + +A basic understanding of contract key concepts, which can be found :doc:`here `, +is required reading for this page. + +Transaction states specify a constraint over the contract that will be used to verify it. For a transaction to be +valid, the verify() function associated with each state must run successfully. However, for this to be secure, it is +not sufficient to specify the verify() function by name as there may exist multiple different implementations with the +same method signature and enclosing class. Contract constraints solve this problem by allowing a contract developer to +constrain which verify() functions out of the universe of implementations can be used. +(ie the universe is everything that matches the signature and contract constraints restricts this universe to a subset.) + +A typical constraint is the hash of the CorDapp JAR that contains the contract and states but will in future releases +include constraints that require specific signers of the JAR, or both the signer and the hash. Constraints can be +specified when constructing a transaction; if unspecified, an automatic constraint is used. + +``TransactionState``s have a ``constraint`` field that represents that state's attachment constraint. When a party +constructs a ``TransactionState`` without specifying the constraint parameter a default value +(``AutomaticHashConstraint``) is used. This default will be automatically resolved to a specific +``HashAttachmentConstraint`` that contains the hash of the attachment which contains the contract of that +``TransactionState``. This automatic resolution occurs when a ``TransactionBuilder`` is converted to a +``WireTransaction``. This reduces the boilerplate involved in finding a specific hash constraint when building a transaction. + +It is possible to specify the constraint explicitly with any other class that implements the ``AttachmentConstraint`` +interface. To specify a hash manually the ``HashAttachmentConstraint`` can be used and to not provide any constraint +the ``AlwaysAcceptAttachmentConstraint`` can be used - though this is intended for testing only. An example below +shows how to construct a ``TransactionState`` with an explicitly specified hash constraint from within a flow; + +.. sourcecode:: java + + // Constructing a transaction with a custom hash constraint on a state + TransactionBuilder tx = new TransactionBuilder() + + Party notaryParty = ... // a notary party + DummyState contractState = new DummyState() + SecureHash myAttachmentsHash = serviceHub.cordappProvider.getContractAttachmentID(DummyContract.PROGRAM_ID) + TransactionState transactionState = new TransactionState(contractState, DummyContract.Companion.getPROGRAMID(), notaryParty, new AttachmentHashConstraint(myAttachmentsHash)) + + tx.addOutputState(transactionState) + WireTransaction wtx = tx.toWireTransaction(serviceHub) // This is where an automatic constraint would be resolved + LedgerTransaction ltx = wtx.toLedgerTransaction(serviceHub) + ltx.verify() // Verifies both the attachment constraints and contracts + + +This mechanism exists both for integrity and security reasons. It is important not to verify against the wrong contract, +which could happen if the wrong version of the contract is attached. More importantly when resolving transaction chains +there will, in a future release, be attachments loaded from the network into the attachment sandbox that are used +to verify the transaction chain. Ensuring the attachment used is the correct one ensures that the verification will +not be tamperable by providing a fake contract. + +CorDapps as attachments +----------------------- + +CorDapp JARs (:doc:`cordapp-overview`) that are installed to the node and contain classes implementing the ``Contract`` +interface are automatically loaded into the ``AttachmentStorage`` of a node at startup. + +After CorDapps are loaded into the attachment store the node creates a link between contract classes and the +attachment that they were loaded from. This makes it possible to find the attachment for any given contract. +This is how the automatic resolution of attachments is done by the ``TransactionBuilder`` and how, when verifying +the constraints and contracts, attachments are associated with their respective contracts. + +Implementations +--------------- + +There are three implementations of ``AttachmentConstraints`` with more planned in the future. + +``AlwaysAcceptAttachmentConstraint``: Any attachment (except a missing one) will satisfy this constraint. + +``AutomaticHashConstraint``: This will be resolved to a ``HashAttachmentConstraint`` when a ``TransactionBuilder`` is +converted to a ``WireTransaction``. The ``HashAttachmentConstraint`` will include the attachment hash of the CorDapp +that contains the ``ContractState`` on the ``TransactionState.contract`` field. + +``HashAttachmentConstraint``: Will require that the hash of the attachment containing the contract matches the hash +stored in the constraint. + +We plan to add a future ``AttachmentConstraint`` that will only be satisfied by the presence of signatures on the +attachment JAR. This allows for trusting of attachments from trusted entities. + +Limitations +----------- + +``AttachmentConstraint``s are verified by running the ``AttachmentConstraint.isSatisfiedBy`` method. When this is called +it is provided only the relevant attachment by the transaction that is verifying it. + +Testing +------- + +Since all tests involving transactions now require attachments it is also required to load the correct attachments +for tests. Unit test environments in JVM ecosystems tend to use class directories rather than JARs, and so CorDapp JARs +typically aren't built for testing. Requiring this would add significant complexity to the build systems of Corda +and CorDapps, so the test suite has a set of convenient functions to generate CorDapps from package names or +to specify JAR URLs in the case that the CorDapp(s) involved in testing already exist. + +MockNetwork/MockNode +******************** + +The most simple way to ensure that a vanilla instance of a MockNode generates the correct CorDapps is to make a call +to ``setCordappPackages`` before the MockNetwork/Node are created and then ``unsetCordappPackages`` after the test +has finished. These calls will cause the ``AbstractNode`` to use the named packages as sources for CorDapps. All files +within those packages will be zipped into a JAR and added to the attachment store and loaded as CorDapps by the +``CordappLoader``. An example of this usage would be: + +.. sourcecode:: java + + class SomeTestClass { + MockNetwork network = null + + @Before + void setup() { + // The ordering of the two below lines is important - if the MockNetwork is created before the nodes and network + // are created the CorDapps will not be loaded into the MockNodes correctly. + setCordappPackages(Arrays.asList("com.domain.cordapp")) + network = new MockNetwork() + } + + @After + void teardown() { + // This must be called at the end otherwise the global state set by setCordappPackages may leak into future + // tests in the same test runner environment. + unsetCordappPackages() + } + + ... // Your tests go here + } + +MockServices +************ + +If your test uses a ``MockServices`` directly you can instantiate it using a constructor that takes a list of packages +to use as CorDapps using the ``cordappPackages`` parameter. + +.. sourcecode:: java + + MockServices mockServices = new MockServices(Arrays.asList("com.domain.cordapp")) + +Driver +****** + +The driver takes a parameter called ``extraCordappPackagesToScan`` which is a list of packages to use as CorDapps. + +.. sourcecode:: java + + driver(new DriverParameters().setExtraCordappPackagesToScan(Arrays.asList("com.domain.cordapp"))) ... + +Full Nodes +********** + +When testing against full nodes simply place your CorDapp into the plugins directory of the node. diff --git a/docs/source/key-concepts.rst b/docs/source/key-concepts.rst index fdf2b0231c..e83d04d093 100644 --- a/docs/source/key-concepts.rst +++ b/docs/source/key-concepts.rst @@ -15,6 +15,7 @@ This section should be read in order: key-concepts-ledger key-concepts-states key-concepts-contracts + key-concepts-contract-constraints key-concepts-transactions key-concepts-flows key-concepts-consensus From 7861135403fb3b9c2f15912745e52f375dd132dd Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Fri, 29 Sep 2017 16:46:59 +0100 Subject: [PATCH 047/185] Hide companion object completely from external visibility (#1742) --- .../kotlin/net/corda/core/transactions/SignedTransaction.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt index 2705e02ef9..af811c25b3 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -189,7 +189,7 @@ data class SignedTransaction(val txBits: SerializedBytes, override fun toString(): String = "${javaClass.simpleName}(id=$id)" - companion object { + private companion object { private fun missingSignatureMsg(missing: Set, descriptions: List, id: SecureHash): String = "Missing signatures for $descriptions on transaction ${id.prefixChars()} for ${missing.joinToString()}" } From 1e12aebe0a22ed692b66cb37a9479bd889dba61b Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Fri, 29 Sep 2017 16:55:26 +0100 Subject: [PATCH 048/185] Gradle API Scanner plugin (#1669) * Skeleton plugin. * Implement Gradle api-scanner plugin, and apply it. * Generate API documentation for any jar without a classifier. * Fix usage of smokeTests classifier. * Tweak Gradle API usage. * Upgrade to fast-classpath-scanner 2.7.0 * Include interfaces and more modifiers in the class description. * Allow system classes to be supertypes and implemented interfaces. * Make API Scanner plugin configuration tweakable via build.gradle. * Add a miserable amount of unit testing. * Sort methods and fields using their natural comparators. Way easier! * Add README for api-scanner plugin. * Add @OutputFiles to ScanApiTask. * Rename ScanApiTask to ScanApi. * Allow the ScanApi task to be disabled. * WIP: Create a top-level GenerateApi task to collate the ScanApi output. * Exclude package-private classes, as well as bridge/synthetic methods. * Replace "End of Class" delimiter with '##'. * Don't scan modules whose API is still "in flux". * Include constructors in the API definitions. * Finish implementation of GenerateApi task. * Update README to include GenerateApi task. * Filter out Kotlin's "internal" methods. * Assign "fatjar" classifier to the fat jar artifact. * Enhance README for GenerateApi. * Explain effect of api-scanner plugin, and link to Corda's API strategy. * Tweak README * Exclude synthetic Kotlin classes by analysing @Metadata. * Allow us to exclude some classes explicitly from the API. --- build.gradle | 5 + client/jackson/build.gradle | 1 + client/jfx/build.gradle | 2 +- client/mock/build.gradle | 2 +- client/rpc/build.gradle | 1 + constants.properties | 2 +- core/build.gradle | 8 + gradle-plugins/api-scanner/README.md | 79 +++++ gradle-plugins/api-scanner/build.gradle | 18 + .../java/net/corda/plugins/ApiScanner.java | 70 ++++ .../java/net/corda/plugins/GenerateApi.java | 61 ++++ .../main/java/net/corda/plugins/ScanApi.java | 311 ++++++++++++++++++ .../net/corda/plugins/ScannerExtension.java | 37 +++ .../net.corda.plugins.api-scanner.properties | 1 + gradle-plugins/build.gradle | 4 +- gradle-plugins/cordformation/build.gradle | 2 +- gradle-plugins/settings.gradle | 3 +- node/build.gradle | 2 +- .../kotlin/net/corda/node/CordappSmokeTest.kt | 2 +- 19 files changed, 602 insertions(+), 9 deletions(-) create mode 100644 gradle-plugins/api-scanner/README.md create mode 100644 gradle-plugins/api-scanner/build.gradle create mode 100644 gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ApiScanner.java create mode 100644 gradle-plugins/api-scanner/src/main/java/net/corda/plugins/GenerateApi.java create mode 100644 gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java create mode 100644 gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScannerExtension.java create mode 100644 gradle-plugins/api-scanner/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.api-scanner.properties diff --git a/build.gradle b/build.gradle index 1a624d147a..63cce612c6 100644 --- a/build.gradle +++ b/build.gradle @@ -60,6 +60,7 @@ buildscript { classpath "net.corda.plugins:publish-utils:$gradle_plugins_version" classpath "net.corda.plugins:quasar-utils:$gradle_plugins_version" classpath "net.corda.plugins:cordformation:$gradle_plugins_version" + classpath "net.corda.plugins:api-scanner:$gradle_plugins_version" classpath 'com.github.ben-manes:gradle-versions-plugin:0.15.0' classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version" classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}" @@ -302,3 +303,7 @@ artifactory { } } } + +task generateApi(type: net.corda.plugins.GenerateApi){ + baseName = "api-corda" +} diff --git a/client/jackson/build.gradle b/client/jackson/build.gradle index 4e6c9b1f69..bcff947dfb 100644 --- a/client/jackson/build.gradle +++ b/client/jackson/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'java' apply plugin: 'kotlin' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'net.corda.plugins.api-scanner' apply plugin: 'com.jfrog.artifactory' dependencies { diff --git a/client/jfx/build.gradle b/client/jfx/build.gradle index 7dd7878175..867ba2b9f0 100644 --- a/client/jfx/build.gradle +++ b/client/jfx/build.gradle @@ -61,4 +61,4 @@ jar { publish { name jar.baseName -} \ No newline at end of file +} diff --git a/client/mock/build.gradle b/client/mock/build.gradle index 7432fdd312..1080cf9b7a 100644 --- a/client/mock/build.gradle +++ b/client/mock/build.gradle @@ -25,4 +25,4 @@ jar { publish { name jar.baseName -} \ No newline at end of file +} diff --git a/client/rpc/build.gradle b/client/rpc/build.gradle index 2f36d1c315..d8c61b4a99 100644 --- a/client/rpc/build.gradle +++ b/client/rpc/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'kotlin' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'net.corda.plugins.api-scanner' apply plugin: 'com.jfrog.artifactory' description 'Corda client RPC modules' diff --git a/constants.properties b/constants.properties index 93971c05df..a9ce732db8 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=1.0.0 +gradlePluginsVersion=1.0.1 kotlinVersion=1.1.50 guavaVersion=21.0 bouncycastleVersion=1.57 diff --git a/core/build.gradle b/core/build.gradle index 655067d680..d821ec7d37 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -2,6 +2,7 @@ apply plugin: 'kotlin' apply plugin: 'kotlin-jpa' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'net.corda.plugins.api-scanner' apply plugin: 'com.jfrog.artifactory' description 'Corda core' @@ -94,6 +95,13 @@ jar { baseName 'corda-core' } +scanApi { + excludeClasses = [ + // Kotlin should probably have declared this class as "synthetic". + "net.corda.core.Utils\$toFuture\$1\$subscription\$1" + ] +} + publish { name jar.baseName } diff --git a/gradle-plugins/api-scanner/README.md b/gradle-plugins/api-scanner/README.md new file mode 100644 index 0000000000..9f8fc4f674 --- /dev/null +++ b/gradle-plugins/api-scanner/README.md @@ -0,0 +1,79 @@ +# API Scanner + +Generates a text summary of Corda's public API that we can check for API-breaking changes. + +```bash +$ gradlew generateApi +``` + +See [here](../../docs/source/api-index.rst) for Corda's public API strategy. We will need to +apply this plugin to other modules in future Corda releases as those modules' APIs stabilise. + +Basically, this plugin will document a module's `public` and `protected` classes/methods/fields, +excluding those from our `*.internal.*` packgages, any synthetic methods, bridge methods, or methods +identified as having Kotlin's `internal` scope. (Kotlin doesn't seem to have implemented `internal` +scope for classes or fields yet as these are currently `public` inside the `.class` file.) + +## Usage +Include this line in the `build.gradle` file of every Corda module that exports public API: + +```gradle +apply plugin: 'net.corda.plugins.api-scanner' +``` + +This will create a Gradle task called `scanApi` which will analyse that module's Jar artifacts. More precisely, +it will analyse all of the Jar artifacts that have not been assigned a Maven classifier, on the basis +that these should be the module's main artifacts. + +The `scanApi` task supports the following configuration options: +```gradle +scanApi { + // Make the classpath-scanning phase more verbose. + verbose = {true|false} + + // Enable / disable the task within this module. + enabled = {true|false} + + // Names of classes that should be excluded from the output. + excludeClasses = [ + ... + ] +} +``` + +All of the `ScanApi` tasks write their output files to their own `$buildDir/api` directory, where they +are collated into a single output file by the `GenerateApi` task. The `GenerateApi` task is declared +in the root project's `build.gradle` file: + +```gradle +task generateApi(type: net.corda.plugins.GenerateApi){ + baseName = "api-corda" +} +``` + +The final API file is written to `$buildDir/api/$baseName-$project.version.txt` + +### Sample Output +``` +public interface net.corda.core.contracts.Attachment extends net.corda.core.contracts.NamedByHash + public abstract void extractFile(String, java.io.OutputStream) + @org.jetbrains.annotations.NotNull public abstract List getSigners() + @org.jetbrains.annotations.NotNull public abstract java.io.InputStream open() + @org.jetbrains.annotations.NotNull public abstract jar.JarInputStream openAsJAR() +## +public interface net.corda.core.contracts.AttachmentConstraint + public abstract boolean isSatisfiedBy(net.corda.core.contracts.Attachment) +## +public final class net.corda.core.contracts.AttachmentResolutionException extends net.corda.core.flows.FlowException + public (net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getHash() +## +``` + +#### Notes +The `GenerateApi` task will collate the output of every `ScanApi` task found either in the same project, +or in any of that project's subprojects. So it is _theoretically_ possible also to collate the API output +from subtrees of modules simply by defining a new `GenerateApi` task at the root of that subtree. + +## Plugin Installation +See [here](../README.rst) for full installation instructions. diff --git a/gradle-plugins/api-scanner/build.gradle b/gradle-plugins/api-scanner/build.gradle new file mode 100644 index 0000000000..85f7b4d0f9 --- /dev/null +++ b/gradle-plugins/api-scanner/build.gradle @@ -0,0 +1,18 @@ +apply plugin: 'java' +apply plugin: 'net.corda.plugins.publish-utils' + +description "Generates a summary of the artifact's public API" + +repositories { + mavenCentral() +} + +dependencies { + compile gradleApi() + compile "io.github.lukehutch:fast-classpath-scanner:2.7.0" + testCompile "junit:junit:4.12" +} + +publish { + name project.name +} diff --git a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ApiScanner.java b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ApiScanner.java new file mode 100644 index 0000000000..0861605258 --- /dev/null +++ b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ApiScanner.java @@ -0,0 +1,70 @@ +package net.corda.plugins; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.file.FileCollection; +import org.gradle.api.tasks.TaskCollection; +import org.gradle.jvm.tasks.Jar; + +public class ApiScanner implements Plugin { + + /** + * Identify the Gradle Jar tasks creating jars + * without Maven classifiers, and generate API + * documentation for them. + * @param p Current project. + */ + @Override + public void apply(Project p) { + p.getLogger().info("Applying API scanner to {}", p.getName()); + + ScannerExtension extension = p.getExtensions().create("scanApi", ScannerExtension.class); + + p.afterEvaluate(project -> { + TaskCollection jarTasks = project.getTasks() + .withType(Jar.class) + .matching(jarTask -> jarTask.getClassifier().isEmpty() && jarTask.isEnabled()); + if (jarTasks.isEmpty()) { + return; + } + + project.getLogger().info("Adding scanApi task to {}", project.getName()); + project.getTasks().create("scanApi", ScanApi.class, scanTask -> { + scanTask.setClasspath(compilationClasspath(project.getConfigurations())); + scanTask.setSources(project.files(jarTasks)); + scanTask.setExcludeClasses(extension.getExcludeClasses()); + scanTask.setVerbose(extension.isVerbose()); + scanTask.setEnabled(extension.isEnabled()); + scanTask.dependsOn(jarTasks); + + // Declare this ScanApi task to be a dependency of any + // GenerateApi tasks belonging to any of our ancestors. + project.getRootProject().getTasks() + .withType(GenerateApi.class) + .matching(generateTask -> isAncestorOf(generateTask.getProject(), project)) + .forEach(generateTask -> generateTask.dependsOn(scanTask)); + }); + }); + } + + /* + * Recurse through a child project's parents until we reach the root, + * and return true iff we find our target project along the way. + */ + private static boolean isAncestorOf(Project target, Project child) { + Project p = child; + while (p != null) { + if (p == target) { + return true; + } + p = p.getParent(); + } + return false; + } + + private static FileCollection compilationClasspath(ConfigurationContainer configurations) { + return configurations.getByName("compile") + .plus(configurations.getByName("compileOnly")); + } +} diff --git a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/GenerateApi.java b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/GenerateApi.java new file mode 100644 index 0000000000..59dbeca87b --- /dev/null +++ b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/GenerateApi.java @@ -0,0 +1,61 @@ +package net.corda.plugins; + +import org.gradle.api.DefaultTask; +import org.gradle.api.file.FileCollection; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; + +import java.io.*; +import java.nio.file.Files; + +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.toList; + +@SuppressWarnings("unused") +public class GenerateApi extends DefaultTask { + + private final File outputDir; + private String baseName; + + public GenerateApi() { + outputDir = new File(getProject().getBuildDir(), "api"); + baseName = "api-" + getProject().getName(); + } + + public void setBaseName(String baseName) { + this.baseName = baseName; + } + + @InputFiles + public FileCollection getSources() { + return getProject().files(getProject().getAllprojects().stream() + .flatMap(project -> project.getTasks() + .withType(ScanApi.class) + .matching(ScanApi::isEnabled) + .stream()) + .flatMap(scanTask -> scanTask.getTargets().getFiles().stream()) + .sorted(comparing(File::getName)) + .collect(toList()) + ); + } + + @OutputFile + public File getTarget() { + return new File(outputDir, String.format("%s-%s.txt", baseName, getProject().getVersion())); + } + + @TaskAction + public void generate() { + FileCollection apiFiles = getSources(); + if (!apiFiles.isEmpty() && (outputDir.isDirectory() || outputDir.mkdirs())) { + try (OutputStream output = new BufferedOutputStream(new FileOutputStream(getTarget()))) { + for (File apiFile : apiFiles) { + Files.copy(apiFile.toPath(), output); + } + } catch (IOException e) { + getLogger().error("Failed to generate API file", e); + } + } + } +} diff --git a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java new file mode 100644 index 0000000000..667feb860c --- /dev/null +++ b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java @@ -0,0 +1,311 @@ +package net.corda.plugins; + +import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner; +import io.github.lukehutch.fastclasspathscanner.scanner.ClassInfo; +import io.github.lukehutch.fastclasspathscanner.scanner.FieldInfo; +import io.github.lukehutch.fastclasspathscanner.scanner.MethodInfo; +import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult; +import org.gradle.api.DefaultTask; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.FileCollection; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.OutputFiles; +import org.gradle.api.tasks.TaskAction; + +import java.io.*; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.*; +import java.util.stream.StreamSupport; + +import static java.util.Collections.unmodifiableSet; +import static java.util.stream.Collectors.*; + +@SuppressWarnings("unused") +public class ScanApi extends DefaultTask { + private static final int CLASS_MASK = Modifier.classModifiers(); + private static final int INTERFACE_MASK = Modifier.interfaceModifiers() & ~Modifier.ABSTRACT; + private static final int METHOD_MASK = Modifier.methodModifiers(); + private static final int FIELD_MASK = Modifier.fieldModifiers(); + private static final int VISIBILITY_MASK = Modifier.PUBLIC | Modifier.PROTECTED; + + /** + * This information has been lifted from: + * @link Metadata.kt + */ + private static final String KOTLIN_METADATA = "kotlin.Metadata"; + private static final String KOTLIN_CLASSTYPE_METHOD = "k"; + private static final int KOTLIN_SYNTHETIC = 3; + + private final ConfigurableFileCollection sources; + private final ConfigurableFileCollection classpath; + private final Set excludeClasses; + private final File outputDir; + private boolean verbose; + + public ScanApi() { + sources = getProject().files(); + classpath = getProject().files(); + excludeClasses = new LinkedHashSet<>(); + outputDir = new File(getProject().getBuildDir(), "api"); + } + + @Input + public FileCollection getSources() { + return sources; + } + + void setSources(FileCollection sources) { + this.sources.setFrom(sources); + } + + @Input + public FileCollection getClasspath() { + return classpath; + } + + void setClasspath(FileCollection classpath) { + this.classpath.setFrom(classpath); + } + + @Input + public Collection getExcludeClasses() { + return unmodifiableSet(excludeClasses); + } + + void setExcludeClasses(Collection excludeClasses) { + this.excludeClasses.clear(); + this.excludeClasses.addAll(excludeClasses); + } + + @OutputFiles + public FileCollection getTargets() { + return getProject().files( + StreamSupport.stream(sources.spliterator(), false) + .map(this::toTarget) + .collect(toList()) + ); + } + + public boolean isVerbose() { + return verbose; + } + + void setVerbose(boolean verbose) { + this.verbose = verbose; + } + + private File toTarget(File source) { + return new File(outputDir, source.getName().replaceAll(".jar$", ".txt")); + } + + @TaskAction + public void scan() { + if (outputDir.isDirectory() || outputDir.mkdirs()) { + try (Scanner scanner = new Scanner(classpath)) { + for (File source : sources) { + scanner.scan(source); + } + } catch (IOException e) { + getLogger().error("Failed to write API file", e); + } + } else { + getLogger().error("Cannot create directory '{}'", outputDir.getAbsolutePath()); + } + } + + class Scanner implements Closeable { + private final URLClassLoader classpathLoader; + private final Class metadataClass; + private final Method classTypeMethod; + + @SuppressWarnings("unchecked") + Scanner(URLClassLoader classpathLoader) { + this.classpathLoader = classpathLoader; + + Class kClass; + Method kMethod; + try { + kClass = (Class) Class.forName(KOTLIN_METADATA, true, classpathLoader); + kMethod = kClass.getDeclaredMethod(KOTLIN_CLASSTYPE_METHOD); + } catch (ClassNotFoundException | NoSuchMethodException e) { + kClass = null; + kMethod = null; + } + + metadataClass = kClass; + classTypeMethod = kMethod; + } + + Scanner(FileCollection classpath) throws MalformedURLException { + this(new URLClassLoader(toURLs(classpath))); + } + + @Override + public void close() throws IOException { + classpathLoader.close(); + } + + void scan(File source) { + File target = toTarget(source); + try ( + URLClassLoader appLoader = new URLClassLoader(new URL[]{ toURL(source) }, classpathLoader); + PrintWriter writer = new PrintWriter(target, "UTF-8") + ) { + scan(writer, appLoader); + } catch (IOException e) { + getLogger().error("API scan has failed", e); + } + } + + void scan(PrintWriter writer, ClassLoader appLoader) { + ScanResult result = new FastClasspathScanner(getScanSpecification()) + .overrideClassLoaders(appLoader) + .ignoreParentClassLoaders() + .ignoreMethodVisibility() + .ignoreFieldVisibility() + .enableMethodInfo() + .enableFieldInfo() + .verbose(verbose) + .scan(); + writeApis(writer, result); + } + + private String[] getScanSpecification() { + String[] spec = new String[2 + excludeClasses.size()]; + spec[0] = "!"; // Don't blacklist system classes from the output. + spec[1] = "-dir:"; // Ignore classes on the filesystem. + + int i = 2; + for (String excludeClass : excludeClasses) { + spec[i++] = '-' + excludeClass; + } + return spec; + } + + private void writeApis(PrintWriter writer, ScanResult result) { + Map allInfo = result.getClassNameToClassInfo(); + result.getNamesOfAllClasses().forEach(className -> { + if (className.contains(".internal.")) { + // These classes belong to internal Corda packages. + return; + } + ClassInfo classInfo = allInfo.get(className); + if (classInfo.getClassLoaders() == null) { + // Ignore classes that belong to one of our target ClassLoader's parents. + return; + } + + Class javaClass = result.classNameToClassRef(className); + if (!isVisible(javaClass.getModifiers())) { + // Excludes private and package-protected classes + return; + } + + int kotlinClassType = getKotlinClassType(javaClass); + if (kotlinClassType == KOTLIN_SYNTHETIC) { + // Exclude classes synthesised by the Kotlin compiler. + return; + } + + writeClass(writer, classInfo, javaClass.getModifiers()); + writeMethods(writer, classInfo.getMethodAndConstructorInfo()); + writeFields(writer, classInfo.getFieldInfo()); + writer.println("##"); + }); + } + + private void writeClass(PrintWriter writer, ClassInfo classInfo, int modifiers) { + if (classInfo.isAnnotation()) { + writer.append(Modifier.toString(modifiers & INTERFACE_MASK)); + writer.append(" @interface ").print(classInfo); + } else if (classInfo.isStandardClass()) { + writer.append(Modifier.toString(modifiers & CLASS_MASK)); + writer.append(" class ").print(classInfo); + Set superclasses = classInfo.getDirectSuperclasses(); + if (!superclasses.isEmpty()) { + writer.append(" extends ").print(stringOf(superclasses)); + } + Set interfaces = classInfo.getDirectlyImplementedInterfaces(); + if (!interfaces.isEmpty()) { + writer.append(" implements ").print(stringOf(interfaces)); + } + } else { + writer.append(Modifier.toString(modifiers & INTERFACE_MASK)); + writer.append(" interface ").print(classInfo); + Set superinterfaces = classInfo.getDirectSuperinterfaces(); + if (!superinterfaces.isEmpty()) { + writer.append(" extends ").print(stringOf(superinterfaces)); + } + } + writer.println(); + } + + private void writeMethods(PrintWriter writer, List methods) { + Collections.sort(methods); + for (MethodInfo method : methods) { + if (isVisible(method.getAccessFlags()) // Only public and protected methods + && isValid(method.getAccessFlags(), METHOD_MASK) // Excludes bridge and synthetic methods + && !isKotlinInternalScope(method)) { + writer.append(" ").println(method); + } + } + } + + private void writeFields(PrintWriter output, List fields) { + Collections.sort(fields); + for (FieldInfo field : fields) { + if (isVisible(field.getAccessFlags()) && isValid(field.getAccessFlags(), FIELD_MASK)) { + output.append(" ").println(field); + } + } + } + + private int getKotlinClassType(Class javaClass) { + if (metadataClass != null) { + Annotation metadata = javaClass.getAnnotation(metadataClass); + if (metadata != null) { + try { + return (int) classTypeMethod.invoke(metadata); + } catch (IllegalAccessException | InvocationTargetException e) { + getLogger().error("Failed to read Kotlin annotation", e); + } + } + } + return 0; + } + } + + private static boolean isKotlinInternalScope(MethodInfo method) { + return method.getMethodName().indexOf('$') >= 0; + } + + private static boolean isValid(int modifiers, int mask) { + return (modifiers & mask) == modifiers; + } + + private static boolean isVisible(int accessFlags) { + return (accessFlags & VISIBILITY_MASK) != 0; + } + + private static String stringOf(Collection items) { + return items.stream().map(ClassInfo::toString).collect(joining(", ")); + } + + private static URL toURL(File file) throws MalformedURLException { + return file.toURI().toURL(); + } + + private static URL[] toURLs(Iterable files) throws MalformedURLException { + List urls = new LinkedList<>(); + for (File file : files) { + urls.add(toURL(file)); + } + return urls.toArray(new URL[urls.size()]); + } +} diff --git a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScannerExtension.java b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScannerExtension.java new file mode 100644 index 0000000000..4e77437cfc --- /dev/null +++ b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScannerExtension.java @@ -0,0 +1,37 @@ +package net.corda.plugins; + +import java.util.List; + +import static java.util.Collections.emptyList; + +@SuppressWarnings("unused") +public class ScannerExtension { + + private boolean verbose; + private boolean enabled = true; + private List excludeClasses = emptyList(); + + public boolean isVerbose() { + return verbose; + } + + public void setVerbose(boolean verbose) { + this.verbose = verbose; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public List getExcludeClasses() { + return excludeClasses; + } + + public void setExcludeClasses(List excludeClasses) { + this.excludeClasses = excludeClasses; + } +} diff --git a/gradle-plugins/api-scanner/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.api-scanner.properties b/gradle-plugins/api-scanner/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.api-scanner.properties new file mode 100644 index 0000000000..fc9e2277a5 --- /dev/null +++ b/gradle-plugins/api-scanner/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.api-scanner.properties @@ -0,0 +1 @@ +implementation-class=net.corda.plugins.ApiScanner diff --git a/gradle-plugins/build.gradle b/gradle-plugins/build.gradle index 9825dc1106..acfdbb2b05 100644 --- a/gradle-plugins/build.gradle +++ b/gradle-plugins/build.gradle @@ -25,7 +25,7 @@ buildscript { apply plugin: 'net.corda.plugins.publish-utils' allprojects { - version "$gradle_plugins_version" + version gradle_plugins_version group 'net.corda.plugins' } @@ -39,7 +39,7 @@ bintrayConfig { projectUrl = 'https://github.com/corda/corda' gpgSign = true gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE') - publications = ['cordformation', 'quasar-utils', 'cordform-common'] + publications = ['cordformation', 'quasar-utils', 'cordform-common', 'api-scanner'] license { name = 'Apache-2.0' url = 'https://www.apache.org/licenses/LICENSE-2.0' diff --git a/gradle-plugins/cordformation/build.gradle b/gradle-plugins/cordformation/build.gradle index 390ea7c8ba..414aa34b30 100644 --- a/gradle-plugins/cordformation/build.gradle +++ b/gradle-plugins/cordformation/build.gradle @@ -51,7 +51,7 @@ task createNodeRunner(type: Jar, dependsOn: [classes]) { manifest { attributes('Main-Class': 'net.corda.plugins.NodeRunnerKt') } - baseName = project.name + '-fatjar' + classifier = 'fatjar' from { configurations.noderunner.collect { it.isDirectory() ? it : zipTree(it) } } from sourceSets.runnodes.output } diff --git a/gradle-plugins/settings.gradle b/gradle-plugins/settings.gradle index f71f2da269..64349cda39 100644 --- a/gradle-plugins/settings.gradle +++ b/gradle-plugins/settings.gradle @@ -2,4 +2,5 @@ rootProject.name = 'corda-gradle-plugins' include 'publish-utils' include 'quasar-utils' include 'cordformation' -include 'cordform-common' \ No newline at end of file +include 'cordform-common' +include 'api-scanner' diff --git a/node/build.gradle b/node/build.gradle index 6a0c142c9f..bcdc43d571 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -188,7 +188,7 @@ task integrationTest(type: Test) { } task smokeTestJar(type: Jar) { - baseName = project.name + '-smoke-test' + classifier 'smokeTests' from sourceSets.smokeTest.output } diff --git a/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt b/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt index bd2d072d64..a4bfc6d12b 100644 --- a/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt +++ b/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt @@ -42,7 +42,7 @@ class CordappSmokeTest { val pluginsDir = (factory.baseDirectory(aliceConfig) / "plugins").createDirectories() // Find the jar file for the smoke tests of this module val selfCordapp = Paths.get("build", "libs").list { - it.filter { "-smoke-test" in it.toString() }.toList().single() + it.filter { "-smokeTests" in it.toString() }.toList().single() } selfCordapp.copyToDirectory(pluginsDir) From e9ec8f96500b79befb8db8ab4846ea6809c4f6d0 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Fri, 29 Sep 2017 17:42:49 +0100 Subject: [PATCH 049/185] Fixes docsite dead links. --- docs/source/json.rst | 4 ++-- docs/source/shell.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/json.rst b/docs/source/json.rst index 126c9f5791..773ad9bccc 100644 --- a/docs/source/json.rst +++ b/docs/source/json.rst @@ -19,8 +19,8 @@ connection to the node (see ":doc:`clientrpc`") then your JSON mapper can resolv The API is described in detail here: -* `Kotlin API docs `_ -* `JavaDoc `_ +* `Kotlin API docs `_ +* `JavaDoc `_ .. container:: codeset diff --git a/docs/source/shell.rst b/docs/source/shell.rst index 8cc68a4313..5d00d73890 100644 --- a/docs/source/shell.rst +++ b/docs/source/shell.rst @@ -139,6 +139,6 @@ The shell will be enhanced over time. The currently known limitations include: * The ``jul`` command advertises access to logs, but it doesn't work with the logging framework we're using. .. _Yaml: http://www.yaml.org/spec/1.2/spec.html -.. _the defined parsers: api/kotlin/corda/net.corda.jackson/-jackson-support/index.html +.. _the defined parsers: api/kotlin/corda/net.corda.client.jackson/-jackson-support/index.html .. _Groovy: http://groovy-lang.org/ .. _CRaSH: http://www.crashub.org/ From d15eeaa2b97dbe16ee4918de2f18d04de2495a8d Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Fri, 29 Sep 2017 18:11:15 +0100 Subject: [PATCH 050/185] Updates readme for v1.0 (#1747) * Updates readme for v1.0 * Addresses review comments --- README.md | 55 ++++++------------------------------------------------- 1 file changed, 6 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index d490c71ae2..25b9d8e2e5 100644 --- a/README.md +++ b/README.md @@ -17,63 +17,20 @@ Corda is a decentralised database system in which nodes trust each other as litt ## Getting started -Firstly, read the [Getting started](https://docs.corda.net/getting-set-up.html) documentation. - -Next, use the following guides to set up your dev environment: - -* If you are on **Windows** [use this getting started guide](https://www.corda.net/wp-content/uploads/2017/01/Corda-Windows-Quick-start-guide-1.pdf) which also explains through how to run the sample apps. - -* Alternatively if you are on **Mac/Linux**, [watch this brief Webinar](https://vimeo.com/200167665) which walks through getting Corda, installing it, building it, running nodes and opening projects in IntelliJ. - -After the above, watching the following webinars will give you a great introduction to Corda: - -### Webinar 1 – [Introduction to Corda](https://vimeo.com/192757743/c2ec39c1e1) - -Richard Brown, R3 Chief Technology Officer, explains Corda's unique architecture, the only distributed ledger platform designed by and for the financial industry's unique requirements. You may want to read the [Corda non-technical whitepaper](https://www.r3cev.com/s/corda-introductory-whitepaper-final.pdf) as pre-reading for this session. - -### Webinar 2 – [Corda Developers’ Tutorial](https://vimeo.com/192797322/aab499b152) - -Roger Willis, R3 Developer Relations Lead, provides an overview of Corda from a developer’s perspective and guidance on how to start building CorDapps. You may want to view [Webinar 1 - Introduction to Corda](https://vimeo.com/192757743/c2ec39c1e1) as preparation for this session. **NB. This was recorded for the M5 release.** - -## Building on Corda - -To build your own CorDapps: - -1. Clone the [CorDapp Template repository](https://github.com/corda/cordapp-template) -2. Read the [README](https://github.com/corda/cordapp-template/blob/master/README.md) (**IMPORTANT!**) -3. Read the [Writing a CorDapp](https://docs.corda.net/tutorial-cordapp.html) documentation - -To look at the Corda source and run some sample applications: - -1. Clone this repository -2. To run some sample CorDapps, read the [running the demos documentation](https://docs.corda.r3cev.com/running-the-demos.html) -3. Start hacking and [contribute](./CONTRIBUTING.md)! +1. Read the [Getting started](https://docs.corda.net/getting-set-up.html) documentation +2. Run the [example CorDapp](https://docs.corda.net/tutorial-cordapp.html) +3. Read about Corda's [Key Concepts](https://docs.corda.net/key-concepts.html) +3. Follow the [Hello, World! tutorial](https://docs.corda.net/hello-world-index.html) ## Useful links * [Project website](https://corda.net) * [Documentation](https://docs.corda.net) * [Slack channel](https://slack.corda.net/) +* [Stack Overflow tag](https://stackoverflow.com/questions/tagged/corda) * [Forum](https://discourse.corda.net) * [Meetups](https://www.meetup.com/pro/corda/) -* [Training Course](https://www.corda.net/corda-training/) - - -## Development State - -Corda is under active development and is maturing rapidly. We are targeting -production-readiness in 2017. The API will continue to evolve throughout 2017; -backwards compatibility not assured until version 1.0. - -Pull requests, experiments, and contributions are encouraged and welcomed. - -## Background - -The project is supported by R3, a financial industry consortium, which is why it -contains some code for financial use cases and why the documentation focuses on finance. The goal is to use it -to construct a global ledger, simplifying finance and reducing the overheads of banking. But it is run as -an open source project and the basic technology of a peer-to-peer decentralised database may be useful -for many different projects. +* [Training Courses](https://www.corda.net/corda-training/) ## Contributing From f36ca78f8803c5a0f44581a0e337adaaf8a9f79c Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Fri, 29 Sep 2017 20:41:12 +0100 Subject: [PATCH 051/185] optimise checkNoDuplicateInputs (#1741) --- .../kotlin/net/corda/core/transactions/BaseTransaction.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt index 3ff6773082..0d5a798d9f 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt @@ -2,8 +2,8 @@ package net.corda.core.transactions import net.corda.core.contracts.* import net.corda.core.identity.Party -import net.corda.core.internal.indexOfOrThrow import net.corda.core.internal.castIfPossible +import net.corda.core.internal.indexOfOrThrow import net.corda.core.internal.uncheckedCast import java.util.function.Predicate @@ -34,8 +34,7 @@ abstract class BaseTransaction : NamedByHash { } private fun checkNoDuplicateInputs() { - val duplicates = inputs.groupBy { it }.filter { it.value.size > 1 }.keys - check(duplicates.isEmpty()) { "Duplicate input states detected" } + check(inputs.size == inputs.toSet().size) { "Duplicate input states detected" } } /** From 4d4027947b80c715eb70b50078868ce5b50b6907 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Sat, 30 Sep 2017 21:55:51 +0100 Subject: [PATCH 052/185] Annotate Gradle task inputs correctly. (#1752) --- .../java/net/corda/plugins/GenerateApi.java | 2 +- .../main/java/net/corda/plugins/ScanApi.java | 21 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/GenerateApi.java b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/GenerateApi.java index 59dbeca87b..9c87224075 100644 --- a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/GenerateApi.java +++ b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/GenerateApi.java @@ -48,7 +48,7 @@ public class GenerateApi extends DefaultTask { @TaskAction public void generate() { FileCollection apiFiles = getSources(); - if (!apiFiles.isEmpty() && (outputDir.isDirectory() || outputDir.mkdirs())) { + if (!apiFiles.isEmpty()) { try (OutputStream output = new BufferedOutputStream(new FileOutputStream(getTarget()))) { for (File apiFile : apiFiles) { Files.copy(apiFile.toPath(), output); diff --git a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java index 667feb860c..53f7ee9fc0 100644 --- a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java +++ b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java @@ -8,7 +8,9 @@ import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult; import org.gradle.api.DefaultTask; import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.FileCollection; +import org.gradle.api.tasks.CompileClasspath; import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.OutputFiles; import org.gradle.api.tasks.TaskAction; @@ -55,7 +57,7 @@ public class ScanApi extends DefaultTask { outputDir = new File(getProject().getBuildDir(), "api"); } - @Input + @InputFiles public FileCollection getSources() { return sources; } @@ -64,7 +66,8 @@ public class ScanApi extends DefaultTask { this.sources.setFrom(sources); } - @Input + @CompileClasspath + @InputFiles public FileCollection getClasspath() { return classpath; } @@ -106,16 +109,12 @@ public class ScanApi extends DefaultTask { @TaskAction public void scan() { - if (outputDir.isDirectory() || outputDir.mkdirs()) { - try (Scanner scanner = new Scanner(classpath)) { - for (File source : sources) { - scanner.scan(source); - } - } catch (IOException e) { - getLogger().error("Failed to write API file", e); + try (Scanner scanner = new Scanner(classpath)) { + for (File source : sources) { + scanner.scan(source); } - } else { - getLogger().error("Cannot create directory '{}'", outputDir.getAbsolutePath()); + } catch (IOException e) { + getLogger().error("Failed to write API file", e); } } From 4641d3c4dd76bb370886b84c0621049f3bcea927 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Sun, 1 Oct 2017 23:33:15 +0100 Subject: [PATCH 053/185] Moves code sections in tutorials to code files. --- .../corda/core/flows/ContractUpgradeFlow.kt | 4 + .../core/node/services/NetworkMapCache.kt | 5 + .../internal/ResolveTransactionsFlowTest.kt | 3 + docs/source/_static/corda-cheat-sheet.pdf | Bin 140780 -> 141625 bytes docs/source/api-index.rst | 2 + docs/source/api-rpc.rst | 10 +- docs/source/api-service-hub.rst | 6 +- docs/source/contract-upgrade.rst | 124 +- docs/source/corda-repo-layout.rst | 6 +- docs/source/event-scheduling.rst | 22 +- .../corda/docs/IntegrationTestingTutorial.kt | 6 +- .../java/net/corda/docs/FlowCookbookJava.java | 32 +- .../tutorial/contract/CommercialPaper.java | 101 ++ .../docs/java/tutorial/contract/State.java | 90 ++ .../TutorialFlowStateMachines.java | 38 + .../tutorial/testdsl/CommercialPaperTest.java | 270 ++++ .../net/corda/docs/ClientRpcTutorial.kt | 5 +- .../kotlin/net/corda/docs/FlowCookbook.kt | 1171 +++++++++-------- .../tutorial/contract/TutorialContract.kt | 136 ++ .../TutorialFlowStateMachines.kt | 64 + .../tutorial/tearoffs/TutorialTearOffs.kt | 45 + .../docs/tutorial/testdsl/TutorialTestDSL.kt | 247 ++++ docs/source/flow-state-machines.rst | 197 +-- docs/source/flow-testing.rst | 58 +- docs/source/getting-set-up.rst | 22 +- docs/source/hello-world-running.rst | 2 +- docs/source/key-concepts-node.rst | 2 +- docs/source/oracles.rst | 115 +- docs/source/running-a-notary.rst | 9 +- docs/source/tutorial-attachments.rst | 55 +- .../source/tutorial-building-transactions.rst | 180 ++- docs/source/tutorial-clientrpc-api.rst | 62 +- docs/source/tutorial-contract.rst | 414 ++---- docs/source/tutorial-integration-testing.rst | 59 +- docs/source/tutorial-tear-offs.rst | 91 +- docs/source/tutorial-test-dsl.rst | 538 ++------ docs/source/tutorials-index.rst | 1 - docs/source/using-a-notary.rst | 138 -- .../contracts/JavaCommercialPaper.java | 2 +- .../corda/finance/contracts/FinanceTypes.kt | 4 + .../corda/attachmentdemo/AttachmentDemo.kt | 4 + .../main/kotlin/net/corda/irs/contract/IRS.kt | 2 + 42 files changed, 2264 insertions(+), 2078 deletions(-) create mode 100644 docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/CommercialPaper.java create mode 100644 docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/State.java create mode 100644 docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/TutorialFlowStateMachines.java create mode 100644 docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java create mode 100644 docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt create mode 100644 docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/flowstatemachines/TutorialFlowStateMachines.kt create mode 100644 docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/tearoffs/TutorialTearOffs.kt create mode 100644 docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt delete mode 100644 docs/source/using-a-notary.rst diff --git a/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt b/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt index e5eb960ac5..d1217909da 100644 --- a/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt @@ -22,11 +22,13 @@ object ContractUpgradeFlow { * * This flow will NOT initiate the upgrade process. To start the upgrade process, see [Initiate]. */ + // DOCSTART 1 @StartableByRPC class Authorise( val stateAndRef: StateAndRef<*>, private val upgradedContractClass: Class> ) : FlowLogic() { + // DOCEND 1 @Suspendable override fun call(): Void? { val upgrade = upgradedContractClass.newInstance() @@ -43,10 +45,12 @@ object ContractUpgradeFlow { * Deauthorise a contract state upgrade. * This will remove the upgrade authorisation from persistent store (and prevent any further upgrade) */ + // DOCSTART 2 @StartableByRPC class Deauthorise(val stateRef: StateRef) : FlowLogic() { @Suspendable override fun call(): Void? { + //DOCEND 2 serviceHub.contractUpgradeService.removeAuthorisedContractUpgrade(stateRef) return null } diff --git a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt index 428a656220..2bb1dc0bb4 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt @@ -28,6 +28,7 @@ interface NetworkMapCache { data class Modified(override val node: NodeInfo, val previousNode: NodeInfo) : MapChange() } + // DOCSTART 1 /** * A list of notary services available on the network. * @@ -35,6 +36,8 @@ interface NetworkMapCache { */ // TODO this list will be taken from NetworkParameters distributed by NetworkMap. val notaryIdentities: List + // DOCEND 1 + /** Tracks changes to the network map cache. */ val changed: Observable /** Future to track completion of the NetworkMapService registration. */ @@ -87,8 +90,10 @@ interface NetworkMapCache { /** Returns information about the party, which may be a specific node or a service */ fun getPartyInfo(party: Party): PartyInfo? + // DOCSTART 2 /** Gets a notary identity by the given name. */ fun getNotary(name: CordaX500Name): Party? = notaryIdentities.firstOrNull { it.name == name } + // DOCEND 2 /** Checks whether a given party is an advertised notary identity. */ fun isNotary(party: Party): Boolean = party in notaryIdentities diff --git a/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt b/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt index 1576852791..6e446d0bd8 100644 --- a/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt @@ -24,6 +24,7 @@ import kotlin.test.assertFailsWith import kotlin.test.assertNotNull import kotlin.test.assertNull +// DOCSTART 3 class ResolveTransactionsFlowTest { lateinit var mockNet: MockNetwork lateinit var notaryNode: StartedNode @@ -53,6 +54,8 @@ class ResolveTransactionsFlowTest { mockNet.stopNodes() unsetCordappPackages() } +// DOCEND 3 + // DOCSTART 1 @Test diff --git a/docs/source/_static/corda-cheat-sheet.pdf b/docs/source/_static/corda-cheat-sheet.pdf index cc00820e046ac75ba2de652a5e29d9d65cf6367f..b7c3b7d1cb02d15e81828a053ece7ba5f9c0cd7e 100644 GIT binary patch delta 63384 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 delta 62406 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�)!@X0~2YlLf5y&?h}d)mVz=+mdzGR=y^-I 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%0DYq7 zq)a=$G2DUPoqyF2@lJiMf;K*)qqRbM4xPZ0ZiH_0Gd7&6sFo z3Z1^1JE{0)?T;*j$cwiF{g0pJhcb9ApId5;ot(vD@YDE=9z=WVSVQ1~O&)v>17?)q z3qAPGb_)6eJ=Aq*XRdu(5EY1aGL*p~Cq4q(i$C=8ySePZ2P$lLVqr0yeLjs)HMvSS5EMavG0L zUzp7`gK)7}SucXS7TX&JN%FM7jwL3$({hV2`C>}Vz5I-T%5Huu%Ht-USXa`|^oR{E z%0q=FrlilsF3v>|gXr98K}APe++X1G1F>O-Ds7CiR8>`-#Pm2tJ}Z;ZqB(e+kW>Yg zB^-ndGgM5a;vjZw+_$=qMZLg>c5&jPRb02){>G(oDnBY${=1RYRLJLCEipvEOesl@fljd_=+9to0HAlp+Os?3p-Pr;c(T(-@0h zlv69X;2o&19H#p(1trR;|Jrz@f}!{z_sz*rc1PmuGBr<;MS}>iB7*B^S$Oh` zjiVJho0GxQh1QhQfsOPP@6@zo)9&hVBUH`xj6!uJRme?+^BY$RPp!vc;>XrEX=jlJ zqLt#s4aP5NS=g$Yu2}?1^;SHbY60caHRU`!($KH*h5(Q_$lP$Cy!eKLI1u9Hjn4oNWGIb!00?=A0NdvW8hYW=Iwiz#Y z8Ef}F_a%(h&JQErUarO9}o1%!q#Wv*QTAMc?p4(^(4$4 zxJSB2>s`2A45~P&S;1>lZRNEper}Ruw!(yMD&{c1J1A>G^%Wp5kB=ZX;3DVAMCohJR zjd%m|VeD>J_pae_F*?IueW`p8;ktS6wHm$C8pBLb^XC0$ZfLQmrC$=`L-cVK?X=yV z$VKrr>gWTymx3+@Gcn@L9_^lL!%$C#d;FEt<3Vy=X54h)s=*sy(A$M6Oyx<9%9W4LKTZTKBG zgOSM-Y{n{&<~4!lx68GK(fj;IFl>X_@I0nC5MC5wgH>2Dgk?Pfnl}C-jeMY ztH-cupH+I9f33>}q2#mXAa+)I6lMPa`!q){I3e5pD+S|$#^T|a98~q3iN2=uZbKRI ztdeYF^Ut@hdV0co*#xvu%!3o!#gXHB%}<4{`5oJFL^+>xVw=a4-r|#Uy2fwg z_l6-P&vdxqtdu&GrWb{PZ#3!W4fp45-QPk;e8cAH;JX+L2(LMvvP%s^HoXIFzQbL{ zf;pk)ZzB2V3>KadW`iOV(N`~3xj>%{wC5lY_$MW25u=AoIiHGRe<% zQUTjeU{`~O+MA-Y?%b;A0#ddv7N{!IA0iN%gqtP;LUnzb;p&&T(HiQNROvI_C zCf9K@+-#qI?)Gea9aUZF^=f!MF_GRrR@XcGI?u)~?R)ksW6#@uea?LZoAcS}k1Rs= z#0%z_KpQF#q8^kob--PZ{0Pg>v)f2WmHPqWsZ{9#-pr;$%@6bMMF!8lWHA+d)Tsh2 zz05R}G?yCqavzp;r7#E3M}`+=!71I1na^6sTj*n5>E46RBe}&@FI!UAJiD82&MQNZ zu`WJNku!c}b=R9taD^@u(m(KruE-&v0f~0h&aWtRPW45qB?V<@OdJ7?wqEsXxyqK2 z1=<=Q=Ds5z2m0in|9qpL*F=zxoq(C9Lb3!ooF+rR=wIN*jsMIiWEbG*0<(4j)H7(l z_b2??b~7+Eu8*`4bx z!Jqs4`K(gGpuvb29bgi)&I zq6c9UP)x#yXw0=2fQc}wle7xZB;%zxZc1o%ZQy?sEX&H*a0qYsB151^ zGcNF$=yNfG7Oz?$QKpy!Y?hz=<(UQByG+hFr1gt~^*iidjVGQa4I|$=r_J{=;79kz zN^kIWpQ#!7{lR?8?$0+xyF1|PW$gwn@N%JJ&+H4v@JB z$5=d4)V*&Jb0v38oZzx)S_l*{NHFt}V8oveCiAndEA4glu)O!5>uvakW{#0pUh7l! zc0CO17_h!|hQ^zpr--(=Ioc^_dY$2KQ~@88vEZnN$>+va_t;dQxl>f*{ieh(@L%+g zSFg4gUxA%Eqg^IyXD(cDeZk_<(#!$Ub?+}Iv>C~wUXxQ6)we{1-vGLbMpAGDosWmj zi*~o?ct2@;VkU~v@$56q+aYu_P{a`(M|&({d0=%d4wb7ashV1Wq9#a^UO|(&L7R6% zKrarO1!P7cCe{w1n@ghWQ^n6hq^1n>yI(1$Y?DoA-mB;Jy$H7sjmuNbl!6>4B+6A< zp0KsZRY`IGBZe;YFTa&NHqlgdxE1 zq5!w1$SD}oZ-H*qmDznbYKkq>NRDph-LEr{E_y@!g%q?K0?y119@4g^Zu*{mC@Isz@r*v3$2&649UWam@K zK)J7!4ulSEu{{zqW$k>55R{J8=@0t#NJibyLelBUV6>tcn$?FTSgrK|9Pa+ZBd{Za zevyu4fAo1CXt>i0(-`@jKL7fP$x(@ONXqUNWxT!c?7h{%N`M5l~ zk)-dNl5;+eu9;mnQBfcv$!W+T^?hJp`+)l>5{TwcctI;d$^cgoaKq-5R!r~fwVSbb z%)Y(2O%t%EZ?}Hichndoz(lY?WYuMZFZ;HQBFo6AyWD;`Ztm4=14d#oy8dp@d~sVB z6l~t|n>15`U#4}~Ie&k>AWQPklD5GmAPTHi<to~7?s^wq`M%xDBb)oV_45ny zHS9I>o*z7&>x4*OA(*PXIBKKWQ^ev{7o6YfgC76V*=SCOLdQtUyN0_bnVmQan`WFU zdT>1`^OK#g^iaQ`5r8=_lUPB61UHD3aP<~nBel_Nxgo8wsfjq;2{lYj{^}CL@&#A= zH%Kxswt-k1$txLuhgWC7AeGms>KEzplS9K0Dp& z-p0xDin`r6u1Tj|bERTvx&GlP?JJ)*rZjwURX;xL{lJGAoQkGY1i!eFXg{6cc)zfJ z=w{HP5R1w61Eqi11rlYEJ=aJQ*Cc9u1fmtm6=~4Q&h0ErtR_8JK<2GCa==GZ2y8jU zJri3X84mdZJFxyd5WH>iG#1WNW!HJhoqpHPiN$`OmLu>@G{aFbV6uP<2!W87Z5x(H zb~{y#C7ByX%Z5uKWTSB7`( z8a$@rB5Ld3@rI;*@HgB_MAqu#?N8ekI?WN;Uc4Y^4R(ptE+Ibo3fX@tSq?D1W-3G~P zHo04J;DG~xYz^vn*>3}I;pH9{<02!U)AUg)+4pqL9n*8@d|q;JJ~z+uEeHg!KR%u+ zSL?Ey#b{FR47-rtAkZHDy5CZxxFgslh$ zSr}mm%r^Rv$x=J#=twlqM8lbhJ+5Wq#)X|S13n*hl^XwXoJ)JBAd)=$2^gku0LxJ& zuT6$;XqxpI=l$ieGjnQ|i*IpOLjrNHm|j~8-$qLfm_HSm357u*)Vwdm$@9>8S<16k74;#0 zCwP<&*f=ZMWMziVt@Uc)7(mKt(|=F4VV$Zz=&is<#m`IwWo3}16#X#<&Dk+4I?`w< zYVoMD5`h1ku@F8&?FgiCo7F=a!1D0X~mKb>LO~M$zj@Nz@VbJ%r0MRhKp^Xvqtevt}_dSuy zh|vicm~U=Uz+`=2N$bjfGiEm#?M{!?-v(@%KLSU+P`Ucax7p?;vUc)Xq>8B2kHBd5 z2|DS?le1f-;s95{VL??*r9B(M+Ta1LTC9idSn337>g?jI2ci|)v(C!Bg_SqMGro(# z70&bZrK!sYInOQN^>O9O=*+#+V{shh97wU2l_AVWzb9U8YJtrcHXlr zL}bGZbsJD^*TFsj;dX5rsAMAqpJOV<*f1zPhS_{g$nS2nsK^bjvnG|yOT@1I!m)&z zqxck4!C{J7G&*vNtrGv3X0ys-ZMO_r&$*EWnE$xHiMp)E0Dqaup3k`( zQpPMQhJ{g|J_-B+&Ab%79L>EhcRC{==i0McGG;ThI=m&hqX1Rln_7$yVk;D#b~-e( ztS`{9GcAl$>g+QvU=JgSTThac`jly2GCKJj)*8mE_|v&`tIA=aO?#Edu5eNo>Yj^p z|LiNLzz+YB?}X1VWBV6fG;kBJ5RnWrmR0~PJYUTeWW&Uvx(M8y1VkN0AH}4nU(L`2 zUfZ_Py>L`kR@(Y-z?!oO7x&&r4;Th2-A{e5XSIo+q;*hpvN-r&&FbPlvpV&g4Jn1U z&H3fv z!6$pTPZBeze0%zb@7Atq|Avqxg;jVsj;F(*>1;tW@u`n_qCW6R-oC@v%1~$=m~9iy z_SRJKJDMotSQ3hut2TEWhFDI^UPb}ux*zHO;w!AaQ8>}JaImd^Kuis)>jk}bVZcO< ztoy}KlO!7dYVs}uxeTr=tQuMi^a8>4L>sj06d=k0_ULsNY4&9VyT*hw<#h~OA;0ST z3$MBXV$Zc<*~Y#|iw&YuYVqmyQe1Na(c?PR$9xM;DILHAY74aP$;W5r5p^pemcVUA z!nE439<<=a6fjiRKRB*Tq#_p;axCAJJ~F8S#2CsS5DY4d97=}&SU&~5FVVbEw*wAV z@~C#H5$qG?zGhXv!N-u{2Uyse+Ts?`OhiwpUEnKO>Xe0wa(#>@g`CueF5z3>1TJ5*2aw$WTZzvGE$;w zHJiL-^2t9DbeMU8ElM^=eJoQygjTe4875b*Oeguxrn_#bkkTfhB%;WOD2OR(h>S|H zEU^-;v6m(>d2((VsZFvi2`C|u2T%FAb3+m^Oh1cu8%tj*?8Z&XGgo{fw4Yqk68OF1 zQycI|OB8(GQ;#8q!;KTe^}Hz)MthosVAX)+kAN=KQu%p`3i2wbJQ{Tq`r=Z(?w4C5w&ays|z3V2G_W?Oyzni-H(E(|EpWzp)}OG?l9%^ zI5XW?_PoPs`0?_)%~Yq+VcnCWatbH2;Ty~-?H8{UL`rCa4DDd69O)Q%MCKQ36=fn1=!Fc23P2TRI2)q8PK zb|CQGLglXrP7wQF*n0%advVhL*^Y%31YrNG9SeXHz{>hpyZ0?s|BC~}1p@t>75Kf! z|4x+y!18x8oB-h8BmfWxi1RNJ00;3?H6-mA6#7xphd;NM`* z|EfUZxRrQo&E2mLGSbMSAYL117P`Y;(ux1F))zxe`NEYAc*B}S>GjO|7#>H ztN?bdzf^d~INAP=eV-;)w!es3K|tVt#J-RJzvO?PPY}!B8M1-cIsXp~^fxgZh~s|| zzhhi~J&i2yPbka(90<|BGQ5Kve@XZ+#>ttIP7BR%$ZiH;2bll?rpD~1W&jpbGZ2V_ z!xUi324v$h1F@O$Bk}%!n~>^~^7A7(yBIpTcsQAwA-(tiZcl_nMI|aPhV=gcT$tO) diff --git a/docs/source/api-index.rst b/docs/source/api-index.rst index 04e64c7c50..76e1689b89 100644 --- a/docs/source/api-index.rst +++ b/docs/source/api-index.rst @@ -12,6 +12,8 @@ This section describes the APIs that are available for the development of CorDap api-vault-query api-transactions api-flows + api-service-hub + api-rpc api-core-types Before reading this page, you should be familiar with the :doc:`key concepts of Corda `. diff --git a/docs/source/api-rpc.rst b/docs/source/api-rpc.rst index 8c7d79d9a5..442c675a36 100644 --- a/docs/source/api-rpc.rst +++ b/docs/source/api-rpc.rst @@ -9,9 +9,7 @@ The key RPC operations exposed by the node are: * Extract states from the node's vault based on a query criteria * ``CordaRPCOps.vaultTrackBy`` * As above, but also returns an observable of future states matching the query -* ``CordaRPCOps.verifiedTransactions`` - * Extract all transactions from the node's local storage, as well as an observable of all future transactions -* ``CordaRPCOps.networkMapUpdates`` +* ``CordaRPCOps.networkMapFeed`` * A list of network nodes, and an observable of changes to the network map * ``CordaRPCOps.registeredFlows`` * See a list of registered flows on the node @@ -19,10 +17,10 @@ The key RPC operations exposed by the node are: * Start one of the node's registered flows * ``CordaRPCOps.startTrackedFlowDynamic`` * As above, but also returns a progress handle for the flow -* ``CordaRPCOps.nodeIdentity`` - * Returns the node's identity +* ``CordaRPCOps.nodeInfo`` + * Returns information about the node * ``CordaRPCOps.currentNodeTime`` - * Returns the node's current time + * Returns the current time according to the node's clock * ``CordaRPCOps.partyFromKey/CordaRPCOps.wellKnownPartyFromX500Name`` * Retrieves a party on the network based on a public key or X500 name * ``CordaRPCOps.uploadAttachment``/``CordaRPCOps.openAttachment``/``CordaRPCOps.attachmentExists`` diff --git a/docs/source/api-service-hub.rst b/docs/source/api-service-hub.rst index e6804f8cca..edd4874c40 100644 --- a/docs/source/api-service-hub.rst +++ b/docs/source/api-service-hub.rst @@ -24,7 +24,5 @@ Additional, ``ServiceHub`` exposes the following properties: * ``ServiceHub.loadState`` and ``ServiceHub.toStateAndRef`` to resolve a ``StateRef`` into a ``TransactionState`` or a ``StateAndRef`` -* ``ServiceHub.toSignedTransaction`` to sign a ``TransactionBuilder`` and convert it into a ``SignedTransaction`` -* ``ServiceHub.createSignature`` and ``ServiceHub.addSignature`` to create and add signatures to a ``SignedTransaction`` - -Finally, ``ServiceHub`` exposes notary identity key via ``ServiceHub.notaryIdentityKey``. \ No newline at end of file +* ``ServiceHub.signInitialTransaction`` to sign a ``TransactionBuilder`` and convert it into a ``SignedTransaction`` +* ``ServiceHub.createSignature`` and ``ServiceHub.addSignature`` to create and add signatures to a ``SignedTransaction`` \ No newline at end of file diff --git a/docs/source/contract-upgrade.rst b/docs/source/contract-upgrade.rst index a00b6b0870..13322bdc4e 100644 --- a/docs/source/contract-upgrade.rst +++ b/docs/source/contract-upgrade.rst @@ -7,95 +7,93 @@ Upgrading contracts =================== -While every care is taken in development of contract code, -inevitably upgrades will be required to fix bugs (in either design or implementation). -Upgrades can involve a substitution of one version of the contract code for another or changing -to a different contract that understands how to migrate the existing state objects. State objects -refer to the contract code (by hash) they are intended for, and even where state objects can be used -with different contract versions, changing this value requires issuing a new state object. +While every care is taken in development of contract code, inevitably upgrades will be required to fix bugs (in either +design or implementation). Upgrades can involve a substitution of one version of the contract code for another or +changing to a different contract that understands how to migrate the existing state objects. When state objects are +added as outputs to transactions, they are linked to the contract code they are intended for via the +``StateAndContract`` type. Changing a state's contract only requires substituting one ``ContractClassName`` for another. Workflow -------- - Here's the workflow for contract upgrades: -1. Two banks, A and B negotiate a trade, off-platform +1. Banks A and B negotiate a trade, off-platform -2. Banks A and B execute a protocol to construct a state object representing the trade, using contract X, and include it in a transaction (which is then signed and sent to the consensus service). +2. Banks A and B execute a flow to construct a state object representing the trade, using contract X, and include it in + a transaction (which is then signed and sent to the consensus service) -3. Time passes. +3. Time passes -4. The developer of contract X discovers a bug in the contract code, and releases a new version, contract Y. The developer will then notify all existing users (e.g. via a mailing list or CorDapp store) to stop their nodes from issuing further states with contract X. +4. The developer of contract X discovers a bug in the contract code, and releases a new version, contract Y. The + developer will then notify all existing users (e.g. via a mailing list or CorDapp store) to stop their nodes from + issuing further states with contract X -5. Banks A and B review the new contract via standard change control processes and identify the contract states they agree to upgrade (they may decide not to upgrade some contract states as these might be needed for some other obligation contract). +5. Banks A and B review the new contract via standard change control processes and identify the contract states they + agree to upgrade (they may decide not to upgrade some contract states as these might be needed for some other + obligation contract) -6. Banks A and B instruct their Corda nodes (via RPC) to be willing to upgrade state objects of contract X, to state objects for contract Y using agreed upgrade path. +6. Banks A and B instruct their Corda nodes (via RPC) to be willing to upgrade state objects with contract X to state + objects with contract Y using the agreed upgrade path -7. One of the parties initiates (``Initiator``) an upgrade of state objects referring to contract X, to a new state object referring to contract Y. +7. One of the parties (the ``Initiator``) initiates a flow to replace state objects referring to contract X with new + state objects referring to contract Y -8. A proposed transaction ``Proposal``, taking in the old state and outputting the reissued version, is created and signed with the node's private key. +8. A proposed transaction (the ``Proposal``), with the old states as input and the reissued states as outputs, is + created and signed with the node's private key -9. The ``Initiator`` node sends the proposed transaction, along with details of the new contract upgrade path its proposing, to all participants of the state object. +9. The ``Initiator`` node sends the proposed transaction, along with details of the new contract upgrade path that it + is proposing, to all participants of the state object -10. Each counterparty ``Acceptor`` verifies the proposal, signs or rejects the state reissuance accordingly, and sends a signature or rejection notification back to the initiating node. +10. Each counterparty (the ``Acceptor``s) verifies the proposal, signs or rejects the state reissuance accordingly, and + sends a signature or rejection notification back to the initiating node -11. If signatures are received from all parties, the initiating node assembles the complete signed transaction and sends it to the consensus service. +11. If signatures are received from all parties, the ``Initiator`` assembles the complete signed transaction and sends + it to the notary +Authorising an upgrade +---------------------- +Each of the participants in the state for which the contract is being upgraded will have to instruct their node that +they agree to the upgrade before the upgrade can take place. The ``ContractUpgradeFlow`` is used to manage the +authorisation process. Each node administrator can use RPC to trigger either an ``Authorise`` or a ``Deauthorise`` flow +for the state in question. -Authorising upgrade -------------------- +.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + :dedent: 4 -Each of the participants in the upgrading contract will have to instruct their node that they are willing to upgrade the state object before the upgrade. -The ``ContractUpgradeFlow`` is used to manage the authorisation records. The administrator can use RPC to trigger either an ``Authorise`` or ``Deauthorise`` flow. - -.. container:: codeset - - .. sourcecode:: kotlin - - /** - * Authorise a contract state upgrade. - * This will store the upgrade authorisation in persistent store, and will be queried by [ContractUpgradeFlow.Acceptor] during contract upgrade process. - * Invoking this flow indicates the node is willing to upgrade the [StateAndRef] using the [UpgradedContract] class. - * This method will NOT initiate the upgrade process. To start the upgrade process, see [Initiator]. - */ - @StartableByRPC - class Authorise( - val stateAndRef: StateAndRef<*>, - private val upgradedContractClass: Class> - ) : FlowLogic() - - /** - * Deauthorise a contract state upgrade. - * This will remove the upgrade authorisation from persistent store (and prevent any further upgrade) - */ - @StartableByRPC - class Deauthorise( - val stateRef: StateRef - ) : FlowLogic< Void?>() +.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt + :language: kotlin + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + :dedent: 4 Proposing an upgrade -------------------- +After all parties have authorised the contract upgrade for the state, one of the contract participants can initiate the +upgrade process by triggering the ``ContractUpgradeFlow.Initiate`` flow. ``Initiate`` creates a transaction including +the old state and the updated state, and sends it to each of the participants. Each participant will verify the +transaction, create a signature over it, and send the signature back to the initiator. Once all the signatures are +collected, the transaction will be notarised and persisted to every participant's vault. -After all parties have registered the intention of upgrading the contract state, one of the contract participants can initiate the upgrade process by triggering the ``Initiator`` contract upgrade flow. -The ``Initiator`` will create a new state and sent to each participant for signatures, each of the participants (Acceptor) will verify, sign the proposal and return to the initiator. -The transaction will be notarised and persisted once every participant verified and signed the upgrade proposal. +Example +------- +Suppose Bank A has entered into an agreement with Bank B which is represented by the state object +``DummyContractState`` and governed by the contract code ``DummyContract``. A few days after the exchange of contracts, +the developer of the contract code discovers a bug in the contract code. -Examples --------- +Bank A and Bank B decide to upgrade the contract to ``DummyContractV2``: -Lets assume Bank A has entered into an agreement with Bank B, and the contract is translated into contract code ``DummyContract`` with state object ``DummyContractState``. +1. The developer creates a new contract ``DummyContractV2`` extending the ``UpgradedContract`` class, and a new state + object ``DummyContractV2.State`` referencing the new contract. -A few days after the exchange of contracts, the developer of the contract code discovered a bug/misrepresentation in the contract code. -Bank A and Bank B decided to upgrade the contract to ``DummyContractV2`` - -1. Developer will create a new contract extending the ``UpgradedContract`` class, and a new state object ``DummyContractV2.State`` referencing the new contract. - -.. literalinclude:: /../../test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt +.. literalinclude:: /../../testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 -2. Bank A will instruct its node to accept the contract upgrade to ``DummyContractV2`` for the contract state. +2. Bank A instructs its node to accept the contract upgrade to ``DummyContractV2`` for the contract state. .. container:: codeset @@ -105,9 +103,9 @@ Bank A and Bank B decided to upgrade the contract to ``DummyContractV2`` val rpcA = rpcClient.proxy() rpcA.startFlow(ContractUpgradeFlow.Authorise(<>, DummyContractV2::class.java)) -3. Bank B now initiate the upgrade Flow, this will send a upgrade proposal to all contract participants. -Each of the participants of the contract state will sign and return the contract state upgrade proposal once they have validated and agreed with the upgrade. -The upgraded transaction state will be recorded in every participant's node at the end of the flow. +3. Bank B initiates the upgrade flow, which will send an upgrade proposal to all contract participants. Each of the + participants of the contract state will sign and return the contract state upgrade proposal once they have validated + and agreed with the upgrade. The upgraded transaction will be recorded in every participant's node by the flow. .. container:: codeset diff --git a/docs/source/corda-repo-layout.rst b/docs/source/corda-repo-layout.rst index 9c8fc9cb1f..bbec123ab4 100644 --- a/docs/source/corda-repo-layout.rst +++ b/docs/source/corda-repo-layout.rst @@ -6,11 +6,11 @@ The Corda repository comprises the following folders: * **buildSrc** contains necessary gradle plugins to build Corda * **client** contains libraries for connecting to a node, working with it remotely and binding server-side data to JavaFX UI +* **confidential-identities** contains experimental support for confidential identities on the ledger * **config** contains logging configurations and the default node configuration file * **core** containing the core Corda libraries such as crypto functions, types for Corda's building blocks: states, contracts, transactions, attachments, etc. and some interfaces for nodes and protocols -* **docs** contains the Corda docsite in restructured text format as well as the built docs in html. The docs can be - accessed via ``/docs/index.html`` from the root of the repo +* **docs** contains the Corda docsite in restructured text format * **experimental** contains platform improvements that are still in the experimental stage * **finance** defines a range of elementary contracts (and associated schemas) and protocols, such as abstract fungible assets, cash, obligation and commercial paper @@ -20,7 +20,7 @@ The Corda repository comprises the following folders: * **node** contains the core code of the Corda node (eg: node driver, node services, messaging, persistence) * **node-api** contains data structures shared between the node and the client module, e.g. types sent via RPC * **samples** contains all our Corda demos and code samples -* **test-utils** contains some utilities for unit testing contracts ( the contracts testing DSL) and protocols (the +* **testing** contains some utilities for unit testing contracts (the contracts testing DSL) and flows (the mock network) implementation * **tools** contains the explorer which is a GUI front-end for Corda, and also the DemoBench which is a GUI tool that allows you to run Corda nodes locally for demonstrations diff --git a/docs/source/event-scheduling.rst b/docs/source/event-scheduling.rst index b5df1bcf1c..3ba100a3ef 100644 --- a/docs/source/event-scheduling.rst +++ b/docs/source/event-scheduling.rst @@ -42,7 +42,7 @@ There are two main steps to implementing scheduled events: ``nextScheduledActivity`` to be implemented which returns an optional ``ScheduledActivity`` instance. ``ScheduledActivity`` captures what ``FlowLogic`` instance each node will run, to perform the activity, and when it will run is described by a ``java.time.Instant``. Once your state implements this interface and is tracked by the - wallet, it can expect to be queried for the next activity when committed to the wallet. The ``FlowLogic`` must be + vault, it can expect to be queried for the next activity when committed to the vault. The ``FlowLogic`` must be annotated with ``@SchedulableFlow``. * If nothing suitable exists, implement a ``FlowLogic`` to be executed by each node as the activity itself. The important thing to remember is that in the current implementation, each node that is party to the transaction @@ -58,7 +58,7 @@ handler to help with obtaining a unique and secure random session. An example i The production and consumption of ``ContractStates`` is observed by the scheduler and the activities associated with any consumed states are unscheduled. Any newly produced states are then queried via the ``nextScheduledActivity`` method and if they do not return ``null`` then that activity is scheduled based on the content of the -``ScheduledActivity`` object returned. Be aware that this *only* happens if the wallet considers the state +``ScheduledActivity`` object returned. Be aware that this *only* happens if the vault considers the state "relevant", for instance, because the owner of the node also owns that state. States that your node happens to encounter but which aren't related to yourself will not have any activities scheduled. @@ -68,21 +68,13 @@ An example Let's take an example of the interest rate swap fixings for our scheduled events. The first task is to implement the ``nextScheduledActivity`` method on the ``State``. - .. container:: codeset - .. sourcecode:: kotlin - - override fun nextScheduledActivity(thisStateRef: StateRef, - flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity? { - val nextFixingOf = nextFixingOf() ?: return null - - val (instant, duration) = suggestInterestRateAnnouncementTimeWindow(index = nextFixingOf.name, - source = floatingLeg.indexSource, - date = nextFixingOf.forDay) - return ScheduledActivity(flowLogicRefFactory.create(TwoPartyDealFlow.FixingRoleDecider::class.java, - thisStateRef, duration), instant) - } + .. literalinclude:: ../../samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + :dedent: 8 The first thing this does is establish if there are any remaining fixings. If there are none, then it returns ``null`` to indicate that there is no activity to schedule. Otherwise it calculates the ``Instant`` at which the interest rate diff --git a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt index e4de7b4864..07ef6d6983 100644 --- a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt +++ b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt @@ -24,7 +24,7 @@ class IntegrationTestingTutorial { fun `alice bob cash exchange example`() { // START 1 driver(startNodesInProcess = true, - extraCordappPackagesToScan = listOf("net.corda.finance.contracts.asset")) { + extraCordappPackagesToScan = listOf("net.corda.finance.contracts.asset")) { val aliceUser = User("aliceUser", "testPassword1", permissions = setOf( startFlowPermission(), startFlowPermission() @@ -104,7 +104,7 @@ class IntegrationTestingTutorial { } ) } + // END 5 } } -} -// END 5 +} \ No newline at end of file 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/FlowCookbookJava.java index 8d92b01786..c9d708540d 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/FlowCookbookJava.java @@ -34,8 +34,6 @@ import java.util.Set; import static net.corda.core.contracts.ContractsDSL.requireThat; import static net.corda.testing.TestConstants.getALICE_KEY; -// We group our two flows inside a singleton object to indicate that they work -// together. @SuppressWarnings("unused") public class FlowCookbookJava { // ``InitiatorFlow`` is our first flow, and will communicate with @@ -121,20 +119,24 @@ public class FlowCookbookJava { // A transaction generally needs a notary: // - To prevent double-spends if the transaction has inputs - // - To serve as a timestamping authority if the transaction has a time-window + // - To serve as a timestamping authority if the transaction has a + // time-window // We retrieve a notary from the network map. // DOCSTART 1 - Party specificNotary = getServiceHub().getNetworkMapCache().getNotary(new CordaX500Name("Notary Service", "London", "UK")); - // Alternatively, we can pick an arbitrary notary from the notary list. However, it is always preferable to - // specify which notary to use explicitly, as the notary list might change when new notaries are introduced, - // or old ones decommissioned. + CordaX500Name notaryName = new CordaX500Name("Notary Service", "London", "GB"); + Party specificNotary = getServiceHub().getNetworkMapCache().getNotary(notaryName); + // Alternatively, we can pick an arbitrary notary from the notary + // list. However, it is always preferable to specify the notary + // explicitly, as the notary list might change when new notaries are + // introduced, or old ones decommissioned. Party firstNotary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0); // DOCEND 1 - // We may also need to identify a specific counterparty. - // Again, we do so using the network map. + // We may also need to identify a specific counterparty. We do so + // using the identity service. // DOCSTART 2 - Party namedCounterparty = getServiceHub().getIdentityService().wellKnownPartyFromX500Name(new CordaX500Name("NodeA", "London", "UK")); + CordaX500Name counterPartyName = new CordaX500Name("NodeA", "London", "GB"); + Party namedCounterparty = getServiceHub().getIdentityService().wellKnownPartyFromX500Name(counterPartyName); Party keyedCounterparty = getServiceHub().getIdentityService().partyFromKey(dummyPubKey); // DOCEND 2 @@ -143,6 +145,13 @@ public class FlowCookbookJava { ------------------------------*/ progressTracker.setCurrentStep(SENDING_AND_RECEIVING_DATA); + // We start by initiating a flow session with the counterparty. We + // will use this session to send and receive messages from the + // counterparty. + // DOCSTART initiateFlow + FlowSession counterpartySession = initiateFlow(counterparty); + // DOCEND initiateFlow + // We can send arbitrary data to a counterparty. // If this is the first ``send``, the counterparty will either: // 1. Ignore the message if they are not registered to respond @@ -154,7 +163,6 @@ public class FlowCookbookJava { // registered to respond to this flow, and has a corresponding // ``receive`` call. // DOCSTART 4 - FlowSession counterpartySession = initiateFlow(counterparty); counterpartySession.send(new Object()); // DOCEND 4 @@ -617,7 +625,7 @@ public class FlowCookbookJava { progressTracker.setCurrentStep(RECEIVING_AND_SENDING_DATA); // We need to respond to the messages sent by the initiator: - // 1. They sent us an ``Any`` instance + // 1. They sent us an ``Object`` instance // 2. They waited to receive an ``Integer`` instance back // 3. They sent a ``String`` instance and waited to receive a // ``Boolean`` instance back diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/CommercialPaper.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/CommercialPaper.java new file mode 100644 index 0000000000..8159838082 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/CommercialPaper.java @@ -0,0 +1,101 @@ +package net.corda.docs.java.tutorial.contract; + +import net.corda.core.contracts.*; +import net.corda.core.transactions.LedgerTransaction; +import net.corda.core.transactions.LedgerTransaction.InOutGroup; + +import java.time.Instant; +import java.util.Currency; +import java.util.List; + +import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; +import static net.corda.core.contracts.ContractsDSL.requireThat; +import static net.corda.finance.utils.StateSumming.sumCashBy; + +public class CommercialPaper implements Contract { + // DOCSTART 1 + public static final String IOU_CONTRACT_ID = "com.example.contract.IOUContract"; + // DOCEND 1 + + // DOCSTART 3 + @Override + public void verify(LedgerTransaction tx) { + List> groups = tx.groupStates(State.class, State::withoutOwner); + CommandWithParties cmd = requireSingleCommand(tx.getCommands(), Commands.class); + // DOCEND 3 + + // DOCSTART 4 + TimeWindow timeWindow = tx.getTimeWindow(); + + for (InOutGroup group : groups) { + List inputs = group.getInputs(); + List outputs = group.getOutputs(); + + if (cmd.getValue() instanceof Commands.Move) { + State input = inputs.get(0); + requireThat(require -> { + require.using("the transaction is signed by the owner of the CP", cmd.getSigners().contains(input.getOwner().getOwningKey())); + require.using("the state is propagated", outputs.size() == 1); + // Don't need to check anything else, as if outputs.size == 1 then the output is equal to + // the input ignoring the owner field due to the grouping. + return null; + }); + + } else if (cmd.getValue() instanceof Commands.Redeem) { + // Redemption of the paper requires movement of on-ledger cash. + State input = inputs.get(0); + Amount> received = sumCashBy(tx.getOutputStates(), input.getOwner()); + if (timeWindow == null) throw new IllegalArgumentException("Redemptions must be timestamped"); + Instant time = timeWindow.getFromTime(); + requireThat(require -> { + require.using("the paper must have matured", time.isAfter(input.getMaturityDate())); + require.using("the received amount equals the face value", received == input.getFaceValue()); + require.using("the paper must be destroyed", outputs.size() == 0); + require.using("the transaction is signed by the owner of the CP", cmd.getSigners().contains(input.getOwner().getOwningKey())); + return null; + }); + } else if (cmd.getValue() instanceof Commands.Issue) { + State output = outputs.get(0); + if (timeWindow == null) throw new IllegalArgumentException("Issuances must be timestamped"); + Instant time = timeWindow.getUntilTime(); + requireThat(require -> { + // Don't allow people to issue commercial paper under other entities identities. + require.using("output states are issued by a command signer", cmd.getSigners().contains(output.getIssuance().getParty().getOwningKey())); + require.using("output values sum to more than the inputs", output.getFaceValue().getQuantity() > 0); + require.using("the maturity date is not in the past", time.isBefore(output.getMaturityDate())); + // Don't allow an existing CP state to be replaced by this issuance. + require.using("can't reissue an existing state", inputs.isEmpty()); + return null; + }); + } else { + throw new IllegalArgumentException("Unrecognised command"); + } + } + // DOCEND 4 + } + + // DOCSTART 2 + public static class Commands implements CommandData { + public static class Move extends Commands { + @Override + public boolean equals(Object obj) { + return obj instanceof Move; + } + } + + public static class Redeem extends Commands { + @Override + public boolean equals(Object obj) { + return obj instanceof Redeem; + } + } + + public static class Issue extends Commands { + @Override + public boolean equals(Object obj) { + return obj instanceof Issue; + } + } + } + // DOCEND 2 +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/State.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/State.java new file mode 100644 index 0000000000..295fba8700 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/State.java @@ -0,0 +1,90 @@ +package net.corda.docs.java.tutorial.contract; + +import com.google.common.collect.ImmutableList; +import net.corda.core.contracts.*; +import net.corda.core.crypto.NullKeys; +import net.corda.core.identity.AbstractParty; +import net.corda.core.identity.AnonymousParty; +import org.jetbrains.annotations.NotNull; + +import java.time.Instant; +import java.util.Currency; +import java.util.List; + +// DOCSTART 1 +public class State implements OwnableState { + private PartyAndReference issuance; + private AbstractParty owner; + private Amount> faceValue; + private Instant maturityDate; + + public State() { + } // For serialization + + public State(PartyAndReference issuance, AbstractParty owner, Amount> faceValue, + Instant maturityDate) { + this.issuance = issuance; + this.owner = owner; + this.faceValue = faceValue; + this.maturityDate = maturityDate; + } + + public State copy() { + return new State(this.issuance, this.owner, this.faceValue, this.maturityDate); + } + + public State withoutOwner() { + return new State(this.issuance, new AnonymousParty(NullKeys.NullPublicKey.INSTANCE), this.faceValue, this.maturityDate); + } + + @NotNull + @Override + public CommandAndState withNewOwner(@NotNull AbstractParty newOwner) { + return new CommandAndState(new CommercialPaper.Commands.Move(), new State(this.issuance, newOwner, this.faceValue, this.maturityDate)); + } + + public PartyAndReference getIssuance() { + return issuance; + } + + public AbstractParty getOwner() { + return owner; + } + + public Amount> getFaceValue() { + return faceValue; + } + + public Instant getMaturityDate() { + return maturityDate; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + State state = (State) o; + + if (issuance != null ? !issuance.equals(state.issuance) : state.issuance != null) return false; + if (owner != null ? !owner.equals(state.owner) : state.owner != null) return false; + if (faceValue != null ? !faceValue.equals(state.faceValue) : state.faceValue != null) return false; + return !(maturityDate != null ? !maturityDate.equals(state.maturityDate) : state.maturityDate != null); + } + + @Override + public int hashCode() { + int result = issuance != null ? issuance.hashCode() : 0; + result = 31 * result + (owner != null ? owner.hashCode() : 0); + result = 31 * result + (faceValue != null ? faceValue.hashCode() : 0); + result = 31 * result + (maturityDate != null ? maturityDate.hashCode() : 0); + return result; + } + + @NotNull + @Override + public List getParticipants() { + return ImmutableList.of(this.owner); + } +} +// DOCEND 1 \ No newline at end of file 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 new file mode 100644 index 0000000000..ac7f387ee7 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/TutorialFlowStateMachines.java @@ -0,0 +1,38 @@ +package net.corda.docs.java.tutorial.flowstatemachines; + +import net.corda.core.flows.SignTransactionFlow; +import net.corda.core.utilities.ProgressTracker; +import org.jetbrains.annotations.Nullable; + +public class TutorialFlowStateMachines { + // DOCSTART 1 + private final ProgressTracker progressTracker = new ProgressTracker( + RECEIVING, + VERIFYING, + SIGNING, + COLLECTING_SIGNATURES, + RECORDING + ); + + private static final ProgressTracker.Step RECEIVING = new ProgressTracker.Step( + "Waiting for seller trading info"); + private static final ProgressTracker.Step VERIFYING = new ProgressTracker.Step( + "Verifying seller assets"); + private static final ProgressTracker.Step SIGNING = new ProgressTracker.Step( + "Generating and signing transaction proposal"); + private static final ProgressTracker.Step COLLECTING_SIGNATURES = new ProgressTracker.Step( + "Collecting signatures from other parties"); + private static final ProgressTracker.Step RECORDING = new ProgressTracker.Step( + "Recording completed transaction"); + // DOCEND 1 + + // DOCSTART 2 + private static final ProgressTracker.Step VERIFYING_AND_SIGNING = new ProgressTracker.Step("Verifying and signing transaction proposal") { + @Nullable + @Override + public ProgressTracker childProgressTracker() { + return SignTransactionFlow.Companion.tracker(); + } + }; + // DOCEND 2 +} diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java new file mode 100644 index 0000000000..ee91484dbf --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java @@ -0,0 +1,270 @@ +package net.corda.docs.java.tutorial.testdsl; + +import kotlin.Unit; +import net.corda.core.contracts.PartyAndReference; +import net.corda.core.utilities.OpaqueBytes; +import net.corda.finance.contracts.ICommercialPaperState; +import net.corda.finance.contracts.JavaCommercialPaper; +import net.corda.finance.contracts.asset.Cash; +import org.junit.Test; + +import java.time.temporal.ChronoUnit; + +import static net.corda.finance.Currencies.DOLLARS; +import static net.corda.finance.Currencies.issuedBy; +import static net.corda.finance.contracts.JavaCommercialPaper.JCP_PROGRAM_ID; +import static net.corda.testing.CoreTestUtils.*; +import static net.corda.testing.NodeTestUtils.ledger; +import static net.corda.testing.NodeTestUtils.transaction; +import static net.corda.testing.TestConstants.*; + +public class CommercialPaperTest { + private final OpaqueBytes defaultRef = new OpaqueBytes(new byte[]{123}); + + // DOCSTART 1 + private ICommercialPaperState getPaper() { + return new JavaCommercialPaper.State( + getMEGA_CORP().ref(defaultRef), + getMEGA_CORP(), + issuedBy(DOLLARS(1000), getMEGA_CORP().ref(defaultRef)), + getTEST_TX_TIME().plus(7, ChronoUnit.DAYS) + ); + } + // DOCEND 1 + + // DOCSTART 2 + @Test + public void simpleCP() { + ICommercialPaperState inState = getPaper(); + ledger(l -> { + l.transaction(tx -> { + tx.attachments(JCP_PROGRAM_ID); + tx.input(JCP_PROGRAM_ID, inState); + return tx.verifies(); + }); + return Unit.INSTANCE; + }); + } + // DOCEND 2 + + // DOCSTART 3 + @Test + public void simpleCPMove() { + ICommercialPaperState inState = getPaper(); + ledger(l -> { + l.transaction(tx -> { + tx.input(JCP_PROGRAM_ID, inState); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); + tx.attachments(JCP_PROGRAM_ID); + return tx.verifies(); + }); + return Unit.INSTANCE; + }); + } + // DOCEND 3 + + // DOCSTART 4 + @Test + public void simpleCPMoveFails() { + ICommercialPaperState inState = getPaper(); + ledger(l -> { + l.transaction(tx -> { + tx.input(JCP_PROGRAM_ID, inState); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); + tx.attachments(JCP_PROGRAM_ID); + return tx.failsWith("the state is propagated"); + }); + return Unit.INSTANCE; + }); + } + // DOCEND 4 + + // DOCSTART 5 + @Test + public void simpleCPMoveSuccess() { + ICommercialPaperState inState = getPaper(); + ledger(l -> { + l.transaction(tx -> { + tx.input(JCP_PROGRAM_ID, inState); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); + tx.attachments(JCP_PROGRAM_ID); + tx.failsWith("the state is propagated"); + tx.output(JCP_PROGRAM_ID, "alice's paper", inState.withOwner(getALICE())); + return tx.verifies(); + }); + return Unit.INSTANCE; + }); + } + // DOCEND 5 + + // DOCSTART 6 + @Test + public void simpleIssuanceWithTweak() { + ledger(l -> { + l.transaction(tx -> { + tx.output(JCP_PROGRAM_ID, "paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp. + tx.attachments(JCP_PROGRAM_ID); + tx.tweak(tw -> { + tw.command(getBIG_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); + tw.timeWindow(getTEST_TX_TIME()); + return tw.failsWith("output states are issued by a command signer"); + }); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); + tx.timeWindow(getTEST_TX_TIME()); + return tx.verifies(); + }); + return Unit.INSTANCE; + }); + } + // DOCEND 6 + + // DOCSTART 7 + @Test + public void simpleIssuanceWithTweakTopLevelTx() { + transaction(tx -> { + tx.output(JCP_PROGRAM_ID, "paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp. + tx.attachments(JCP_PROGRAM_ID); + tx.tweak(tw -> { + tw.command(getBIG_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); + tw.timeWindow(getTEST_TX_TIME()); + return tw.failsWith("output states are issued by a command signer"); + }); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); + tx.timeWindow(getTEST_TX_TIME()); + return tx.verifies(); + }); + } + // DOCEND 7 + + // DOCSTART 8 + @Test + public void chainCommercialPaper() { + PartyAndReference issuer = getMEGA_CORP().ref(defaultRef); + ledger(l -> { + l.unverifiedTransaction(tx -> { + tx.output(Cash.PROGRAM_ID, "alice's $900", + new Cash.State(issuedBy(DOLLARS(900), issuer), getALICE())); + tx.attachments(Cash.PROGRAM_ID); + return Unit.INSTANCE; + }); + + // Some CP is issued onto the ledger by MegaCorp. + l.transaction("Issuance", tx -> { + tx.output(JCP_PROGRAM_ID, "paper", getPaper()); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); + tx.attachments(JCP_PROGRAM_ID); + tx.timeWindow(getTEST_TX_TIME()); + return tx.verifies(); + }); + + l.transaction("Trade", tx -> { + tx.input("paper"); + tx.input("alice's $900"); + tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getMEGA_CORP())); + JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); + tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(getALICE())); + tx.command(getALICE_PUBKEY(), new Cash.Commands.Move()); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); + return tx.verifies(); + }); + return Unit.INSTANCE; + }); + } + // DOCEND 8 + + // DOCSTART 9 + @Test + public void chainCommercialPaperDoubleSpend() { + PartyAndReference issuer = getMEGA_CORP().ref(defaultRef); + ledger(l -> { + l.unverifiedTransaction(tx -> { + tx.output(Cash.PROGRAM_ID, "alice's $900", + new Cash.State(issuedBy(DOLLARS(900), issuer), getALICE())); + tx.attachments(Cash.PROGRAM_ID); + return Unit.INSTANCE; + }); + + // Some CP is issued onto the ledger by MegaCorp. + l.transaction("Issuance", tx -> { + tx.output(Cash.PROGRAM_ID, "paper", getPaper()); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); + tx.attachments(JCP_PROGRAM_ID); + tx.timeWindow(getTEST_TX_TIME()); + return tx.verifies(); + }); + + l.transaction("Trade", tx -> { + tx.input("paper"); + tx.input("alice's $900"); + tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getMEGA_CORP())); + JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); + tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(getALICE())); + tx.command(getALICE_PUBKEY(), new Cash.Commands.Move()); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); + return tx.verifies(); + }); + + l.transaction(tx -> { + tx.input("paper"); + JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); + // We moved a paper to other pubkey. + tx.output(JCP_PROGRAM_ID, "bob's paper", inputPaper.withOwner(getBOB())); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); + return tx.verifies(); + }); + l.fails(); + return Unit.INSTANCE; + }); + } + // DOCEND 9 + + // DOCSTART 10 + @Test + public void chainCommercialPaperTweak() { + PartyAndReference issuer = getMEGA_CORP().ref(defaultRef); + ledger(l -> { + l.unverifiedTransaction(tx -> { + tx.output(Cash.PROGRAM_ID, "alice's $900", + new Cash.State(issuedBy(DOLLARS(900), issuer), getALICE())); + tx.attachments(Cash.PROGRAM_ID); + return Unit.INSTANCE; + }); + + // Some CP is issued onto the ledger by MegaCorp. + l.transaction("Issuance", tx -> { + tx.output(Cash.PROGRAM_ID, "paper", getPaper()); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); + tx.attachments(JCP_PROGRAM_ID); + tx.timeWindow(getTEST_TX_TIME()); + return tx.verifies(); + }); + + l.transaction("Trade", tx -> { + tx.input("paper"); + tx.input("alice's $900"); + tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getMEGA_CORP())); + JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); + tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(getALICE())); + tx.command(getALICE_PUBKEY(), new Cash.Commands.Move()); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); + return tx.verifies(); + }); + + l.tweak(lw -> { + lw.transaction(tx -> { + tx.input("paper"); + JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); + // We moved a paper to another pubkey. + tx.output(JCP_PROGRAM_ID, "bob's paper", inputPaper.withOwner(getBOB())); + tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); + return tx.verifies(); + }); + lw.fails(); + return Unit.INSTANCE; + }); + l.verifies(); + return Unit.INSTANCE; + }); + } + // DOCEND 10 +} \ No newline at end of file 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/ClientRpcTutorial.kt index b2ffa4b3f2..bc21a3b5f2 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/ClientRpcTutorial.kt @@ -49,7 +49,7 @@ fun main(args: Array) { startFlowPermission(), startFlowPermission())) - driver(driverDirectory = baseDirectory) { + driver(driverDirectory = baseDirectory, extraCordappPackagesToScan = listOf("net.corda.finance")) { startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))) val node = startNode(providedName = ALICE.name, rpcUsers = listOf(user)).get() // END 1 @@ -100,10 +100,9 @@ fun main(args: Array) { } } waitForAllNodesToFinish() + // END 5 } - } -// END 5 // START 6 fun generateTransactions(proxy: CordaRPCOps) { 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/FlowCookbook.kt index 6fb9df18b2..8aa248f709 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/FlowCookbook.kt @@ -28,606 +28,615 @@ import net.corda.testing.contracts.DummyState import java.security.PublicKey import java.time.Instant -// We group our two flows inside a singleton object to indicate that they work -// together. -object FlowCookbook { - // ``InitiatorFlow`` is our first flow, and will communicate with - // ``ResponderFlow``, below. - // We mark ``InitiatorFlow`` as an ``InitiatingFlow``, allowing it to be - // started directly by the node. - @InitiatingFlow - // We also mark ``InitiatorFlow`` as ``StartableByRPC``, allowing the - // node's owner to start the flow via RPC. - @StartableByRPC - // Every flow must subclass ``FlowLogic``. The generic indicates the - // flow's return type. - class InitiatorFlow(val arg1: Boolean, val arg2: Int, private val counterparty: Party, val regulator: Party) : FlowLogic() { +// ``InitiatorFlow`` is our first flow, and will communicate with +// ``ResponderFlow``, below. +// We mark ``InitiatorFlow`` as an ``InitiatingFlow``, allowing it to be +// started directly by the node. +@InitiatingFlow +// We also mark ``InitiatorFlow`` as ``StartableByRPC``, allowing the +// node's owner to start the flow via RPC. +@StartableByRPC +// Every flow must subclass ``FlowLogic``. The generic indicates the +// flow's return type. +class InitiatorFlow(val arg1: Boolean, val arg2: Int, private val counterparty: Party, val regulator: Party) : FlowLogic() { - /**--------------------------------- - * WIRING UP THE PROGRESS TRACKER * - ---------------------------------**/ - // Giving our flow a progress tracker allows us to see the flow's - // progress visually in our node's CRaSH shell. - // DOCSTART 17 - companion object { - object ID_OTHER_NODES : Step("Identifying other nodes on the network.") - object SENDING_AND_RECEIVING_DATA : Step("Sending data between parties.") - object EXTRACTING_VAULT_STATES : Step("Extracting states from the vault.") - object OTHER_TX_COMPONENTS : Step("Gathering a transaction's other components.") - object TX_BUILDING : Step("Building a transaction.") - object TX_SIGNING : Step("Signing a transaction.") - object TX_VERIFICATION : Step("Verifying a transaction.") - object SIGS_GATHERING : Step("Gathering a transaction's signatures.") { - // Wiring up a child progress tracker allows us to see the - // subflow's progress steps in our flow's progress tracker. - override fun childProgressTracker() = CollectSignaturesFlow.tracker() - } - - object VERIFYING_SIGS : Step("Verifying a transaction's signatures.") - object FINALISATION : Step("Finalising a transaction.") { - override fun childProgressTracker() = FinalityFlow.tracker() - } - - fun tracker() = ProgressTracker( - ID_OTHER_NODES, - SENDING_AND_RECEIVING_DATA, - EXTRACTING_VAULT_STATES, - OTHER_TX_COMPONENTS, - TX_BUILDING, - TX_SIGNING, - TX_VERIFICATION, - SIGS_GATHERING, - VERIFYING_SIGS, - FINALISATION - ) + /**--------------------------------- + * WIRING UP THE PROGRESS TRACKER * + ---------------------------------**/ + // Giving our flow a progress tracker allows us to see the flow's + // progress visually in our node's CRaSH shell. + // DOCSTART 17 + companion object { + object ID_OTHER_NODES : Step("Identifying other nodes on the network.") + object SENDING_AND_RECEIVING_DATA : Step("Sending data between parties.") + object EXTRACTING_VAULT_STATES : Step("Extracting states from the vault.") + object OTHER_TX_COMPONENTS : Step("Gathering a transaction's other components.") + object TX_BUILDING : Step("Building a transaction.") + object TX_SIGNING : Step("Signing a transaction.") + object TX_VERIFICATION : Step("Verifying a transaction.") + object SIGS_GATHERING : Step("Gathering a transaction's signatures.") { + // Wiring up a child progress tracker allows us to see the + // subflow's progress steps in our flow's progress tracker. + override fun childProgressTracker() = CollectSignaturesFlow.tracker() } - // DOCEND 17 - override val progressTracker: ProgressTracker = tracker() - - @Suppress("RemoveExplicitTypeArguments") - @Suspendable - override fun call() { - // We'll be using a dummy public key for demonstration purposes. - // These are built in to Corda, and are generally used for writing - // tests. - val dummyPubKey: PublicKey = ALICE_PUBKEY - - /**-------------------------- - * IDENTIFYING OTHER NODES * - --------------------------**/ - // DOCSTART 18 - progressTracker.currentStep = ID_OTHER_NODES - // DOCEND 18 - - // A transaction generally needs a notary: - // - To prevent double-spends if the transaction has inputs - // - To serve as a timestamping authority if the transaction has a time-window - // We retrieve the notary from the network map. - // DOCSTART 1 - val specificNotary: Party = serviceHub.networkMapCache.getNotary(CordaX500Name(organisation = "Notary Service", locality = "London", country = "UK"))!! - // Alternatively, we can pick an arbitrary notary from the notary list. However, it is always preferable to - // specify which notary to use explicitly, as the notary list might change when new notaries are introduced, - // or old ones decommissioned. - val firstNotary: Party = serviceHub.networkMapCache.notaryIdentities.first() - // DOCEND 1 - - // We may also need to identify a specific counterparty. We - // do so using identity service. - // DOCSTART 2 - val namedCounterparty: Party = serviceHub.identityService.wellKnownPartyFromX500Name(CordaX500Name(organisation = "NodeA", locality = "London", country = "UK")) ?: - throw IllegalArgumentException("Couldn't find counterparty for NodeA in identity service") - val keyedCounterparty: Party = serviceHub.identityService.partyFromKey(dummyPubKey) ?: - throw IllegalArgumentException("Couldn't find counterparty with key: $dummyPubKey in identity service") - // DOCEND 2 - - // DOCSTART initiateFlow - val counterpartySession = initiateFlow(counterparty) - // DOCEND initiateFlow - - /**----------------------------- - * SENDING AND RECEIVING DATA * - -----------------------------**/ - progressTracker.currentStep = SENDING_AND_RECEIVING_DATA - - // We can send arbitrary data to a counterparty. - // If this is the first ``send``, the counterparty will either: - // 1. Ignore the message if they are not registered to respond - // to messages from this flow. - // 2. Start the flow they have registered to respond to this flow, - // and run the flow until the first call to ``receive``, at - // which point they process the message. - // In other words, we are assuming that the counterparty is - // registered to respond to this flow, and has a corresponding - // ``receive`` call. - // DOCSTART 4 - counterpartySession.send(Any()) - // DOCEND 4 - - // We can wait to receive arbitrary data of a specific type from a - // counterparty. Again, this implies a corresponding ``send`` call - // in the counterparty's flow. A few scenarios: - // - We never receive a message back. In the current design, the - // flow is paused until the node's owner kills the flow. - // - Instead of sending a message back, the counterparty throws a - // ``FlowException``. This exception is propagated back to us, - // and we can use the error message to establish what happened. - // - We receive a message back, but it's of the wrong type. In - // this case, a ``FlowException`` is thrown. - // - We receive back a message of the correct type. All is good. - // - // Upon calling ``receive()`` (or ``sendAndReceive()``), the - // ``FlowLogic`` is suspended until it receives a response. - // - // We receive the data wrapped in an ``UntrustworthyData`` - // instance. This is a reminder that the data we receive may not - // be what it appears to be! We must unwrap the - // ``UntrustworthyData`` using a lambda. - // DOCSTART 5 - val packet1: UntrustworthyData = counterpartySession.receive() - val int: Int = packet1.unwrap { data -> - // Perform checking on the object received. - // T O D O: Check the received object. - // Return the object. - data - } - // DOCEND 5 - - // We can also use a single call to send data to a counterparty - // and wait to receive data of a specific type back. The type of - // data sent doesn't need to match the type of the data received - // back. - // DOCSTART 7 - val packet2: UntrustworthyData = counterpartySession.sendAndReceive("You can send and receive any class!") - val boolean: Boolean = packet2.unwrap { data -> - // Perform checking on the object received. - // T O D O: Check the received object. - // Return the object. - data - } - // DOCEND 7 - - // We're not limited to sending to and receiving from a single - // counterparty. A flow can send messages to as many parties as it - // likes, and each party can invoke a different response flow. - // DOCSTART 6 - val regulatorSession = initiateFlow(regulator) - regulatorSession.send(Any()) - val packet3: UntrustworthyData = regulatorSession.receive() - // DOCEND 6 - - /**----------------------------------- - * EXTRACTING STATES FROM THE VAULT * - -----------------------------------**/ - progressTracker.currentStep = EXTRACTING_VAULT_STATES - - // Let's assume there are already some ``DummyState``s in our - // node's vault, stored there as a result of running past flows, - // and we want to consume them in a transaction. There are many - // ways to extract these states from our vault. - - // For example, we would extract any unconsumed ``DummyState``s - // from our vault as follows: - val criteria: VaultQueryCriteria = VaultQueryCriteria() // default is UNCONSUMED - val results: Page = serviceHub.vaultService.queryBy(criteria) - val dummyStates: List> = results.states - - // For a full list of the available ways of extracting states from - // the vault, see the Vault Query docs page. - - // When building a transaction, input states are passed in as - // ``StateRef`` instances, which pair the hash of the transaction - // that generated the state with the state's index in the outputs - // of that transaction. In practice, we'd pass the transaction hash - // or the ``StateRef`` as a parameter to the flow, or extract the - // ``StateRef`` from our vault. - // DOCSTART 20 - val ourStateRef: StateRef = StateRef(SecureHash.sha256("DummyTransactionHash"), 0) - // DOCEND 20 - // A ``StateAndRef`` pairs a ``StateRef`` with the state it points to. - // DOCSTART 21 - val ourStateAndRef: StateAndRef = serviceHub.toStateAndRef(ourStateRef) - // DOCEND 21 - - /**----------------------------------------- - * GATHERING OTHER TRANSACTION COMPONENTS * - -----------------------------------------**/ - progressTracker.currentStep = OTHER_TX_COMPONENTS - - // Output states are constructed from scratch. - // DOCSTART 22 - val ourOutputState: DummyState = DummyState() - // DOCEND 22 - // Or as copies of other states with some properties changed. - // DOCSTART 23 - val ourOtherOutputState: DummyState = ourOutputState.copy(magicNumber = 77) - // DOCEND 23 - - // We then need to pair our output state with a contract. - // DOCSTART 47 - val contractName: String = "net.corda.testing.contracts.DummyContract" - val ourOutput: StateAndContract = StateAndContract(ourOutputState, contractName) - // DOCEND 47 - - // Commands pair a ``CommandData`` instance with a list of - // public keys. To be valid, the transaction requires a signature - // matching every public key in all of the transaction's commands. - // DOCSTART 24 - val commandData: DummyContract.Commands.Create = DummyContract.Commands.Create() - val ourPubKey: PublicKey = serviceHub.myInfo.legalIdentitiesAndCerts.first().owningKey - val counterpartyPubKey: PublicKey = counterparty.owningKey - val requiredSigners: List = listOf(ourPubKey, counterpartyPubKey) - val ourCommand: Command = Command(commandData, requiredSigners) - // DOCEND 24 - - // ``CommandData`` can either be: - // 1. Of type ``TypeOnlyCommandData``, in which case it only - // serves to attach signers to the transaction and possibly - // fork the contract's verification logic. - val typeOnlyCommandData: TypeOnlyCommandData = DummyContract.Commands.Create() - // 2. Include additional data which can be used by the contract - // during verification, alongside fulfilling the roles above. - val commandDataWithData: CommandData = Cash.Commands.Issue() - - // Attachments are identified by their hash. - // The attachment with the corresponding hash must have been - // uploaded ahead of time via the node's RPC interface. - // DOCSTART 25 - val ourAttachment: SecureHash = SecureHash.sha256("DummyAttachment") - // DOCEND 25 - - // Time windows can have a start and end time, or be open at either end. - // DOCSTART 26 - val ourTimeWindow: TimeWindow = TimeWindow.between(Instant.MIN, Instant.MAX) - val ourAfter: TimeWindow = TimeWindow.fromOnly(Instant.MIN) - val ourBefore: TimeWindow = TimeWindow.untilOnly(Instant.MAX) - // DOCEND 26 - - // We can also define a time window as an ``Instant`` +/- a time - // tolerance (e.g. 30 seconds): - // DOCSTART 42 - val ourTimeWindow2: TimeWindow = TimeWindow.withTolerance(serviceHub.clock.instant(), 30.seconds) - // DOCEND 42 - // Or as a start-time plus a duration: - // DOCSTART 43 - val ourTimeWindow3: TimeWindow = TimeWindow.fromStartAndDuration(serviceHub.clock.instant(), 30.seconds) - // DOCEND 43 - - /**----------------------- - * TRANSACTION BUILDING * - -----------------------**/ - progressTracker.currentStep = TX_BUILDING - - // If our transaction has input states or a time-window, we must instantiate it with a - // notary. - // DOCSTART 19 - val txBuilder: TransactionBuilder = TransactionBuilder(specificNotary) - // DOCEND 19 - - // Otherwise, we can choose to instantiate it without one: - // DOCSTART 46 - val txBuilderNoNotary: TransactionBuilder = TransactionBuilder() - // DOCEND 46 - - // We add items to the transaction builder using ``TransactionBuilder.withItems``: - // DOCSTART 27 - txBuilder.withItems( - // Inputs, as ``StateAndRef``s that reference the outputs of previous transactions - ourStateAndRef, - // Outputs, as ``StateAndContract``s - ourOutput, - // Commands, as ``Command``s - ourCommand, - // Attachments, as ``SecureHash``es - ourAttachment, - // A time-window, as ``TimeWindow`` - ourTimeWindow - ) - // DOCEND 27 - - // We can also add items using methods for the individual components. - - // The individual methods for adding input states and attachments: - // DOCSTART 28 - txBuilder.addInputState(ourStateAndRef) - txBuilder.addAttachment(ourAttachment) - // DOCEND 28 - - // An output state can be added as a ``ContractState``, contract class name and notary. - // DOCSTART 49 - txBuilder.addOutputState(ourOutputState, DummyContract.PROGRAM_ID, specificNotary) - // DOCEND 49 - // We can also leave the notary field blank, in which case the transaction's default - // notary is used. - // DOCSTART 50 - txBuilder.addOutputState(ourOutputState, DummyContract.PROGRAM_ID) - // DOCEND 50 - // Or we can add the output state as a ``TransactionState``, which already specifies - // the output's contract and notary. - // DOCSTART 51 - val txState: TransactionState = TransactionState(ourOutputState, DummyContract.PROGRAM_ID, specificNotary) - // DOCEND 51 - - // Commands can be added as ``Command``s. - // DOCSTART 52 - txBuilder.addCommand(ourCommand) - // DOCEND 52 - // Or as ``CommandData`` and a ``vararg PublicKey``. - // DOCSTART 53 - txBuilder.addCommand(commandData, ourPubKey, counterpartyPubKey) - // DOCEND 53 - - // We can set a time-window directly. - // DOCSTART 44 - txBuilder.setTimeWindow(ourTimeWindow) - // DOCEND 44 - // Or as a start time plus a duration (e.g. 45 seconds). - // DOCSTART 45 - txBuilder.setTimeWindow(serviceHub.clock.instant(), 45.seconds) - // DOCEND 45 - - /**---------------------- - * TRANSACTION SIGNING * - ----------------------**/ - progressTracker.currentStep = TX_SIGNING - - // We finalise the transaction by signing it, converting it into a - // ``SignedTransaction``. - // DOCSTART 29 - val onceSignedTx: SignedTransaction = serviceHub.signInitialTransaction(txBuilder) - // DOCEND 29 - // We can also sign the transaction using a different public key: - // DOCSTART 30 - val otherKey: PublicKey = serviceHub.keyManagementService.freshKey() - val onceSignedTx2: SignedTransaction = serviceHub.signInitialTransaction(txBuilder, otherKey) - // DOCEND 30 - - // If instead this was a ``SignedTransaction`` that we'd received - // from a counterparty and we needed to sign it, we would add our - // signature using: - // DOCSTART 38 - val twiceSignedTx: SignedTransaction = serviceHub.addSignature(onceSignedTx) - // DOCEND 38 - // Or, if we wanted to use a different public key: - val otherKey2: PublicKey = serviceHub.keyManagementService.freshKey() - // DOCSTART 39 - val twiceSignedTx2: SignedTransaction = serviceHub.addSignature(onceSignedTx, otherKey2) - // DOCEND 39 - - // We can also generate a signature over the transaction without - // adding it to the transaction itself. We may do this when - // sending just the signature in a flow instead of returning the - // entire transaction with our signature. This way, the receiving - // node does not need to check we haven't changed anything in the - // transaction. - // DOCSTART 40 - val sig: TransactionSignature = serviceHub.createSignature(onceSignedTx) - // DOCEND 40 - // And again, if we wanted to use a different public key: - // DOCSTART 41 - val sig2: TransactionSignature = serviceHub.createSignature(onceSignedTx, otherKey2) - // DOCEND 41 - - // In practice, however, the process of gathering every signature - // but the first can be automated using ``CollectSignaturesFlow``. - // See the "Gathering Signatures" section below. - - /**--------------------------- - * TRANSACTION VERIFICATION * - ---------------------------**/ - progressTracker.currentStep = TX_VERIFICATION - - // Verifying a transaction will also verify every transaction in - // the transaction's dependency chain, which will require - // transaction data access on counterparty's node. The - // ``SendTransactionFlow`` can be used to automate the sending and - // data vending process. The ``SendTransactionFlow`` will listen - // for data request until the transaction is resolved and verified - // on the other side: - // DOCSTART 12 - subFlow(SendTransactionFlow(counterpartySession, twiceSignedTx)) - - // Optional request verification to further restrict data access. - subFlow(object : SendTransactionFlow(counterpartySession, twiceSignedTx) { - override fun verifyDataRequest(dataRequest: FetchDataFlow.Request.Data) { - // Extra request verification. - } - }) - // DOCEND 12 - - // We can receive the transaction using ``ReceiveTransactionFlow``, - // which will automatically download all the dependencies and verify - // the transaction - // DOCSTART 13 - val verifiedTransaction = subFlow(ReceiveTransactionFlow(counterpartySession)) - // DOCEND 13 - - // We can also send and receive a `StateAndRef` dependency chain - // and automatically resolve its dependencies. - // DOCSTART 14 - subFlow(SendStateAndRefFlow(counterpartySession, dummyStates)) - - // On the receive side ... - val resolvedStateAndRef = subFlow(ReceiveStateAndRefFlow(counterpartySession)) - // DOCEND 14 - - // We can now verify the transaction to ensure that it satisfies - // the contracts of all the transaction's input and output states. - // DOCSTART 33 - twiceSignedTx.verify(serviceHub) - // DOCEND 33 - - // We'll often want to perform our own additional verification - // too. Just because a transaction is valid based on the contract - // rules and requires our signature doesn't mean we have to - // sign it! We need to make sure the transaction represents an - // agreement we actually want to enter into. - - // To do this, we need to convert our ``SignedTransaction`` - // into a ``LedgerTransaction``. This will use our ServiceHub - // to resolve the transaction's inputs and attachments into - // actual objects, rather than just references. - // DOCSTART 32 - val ledgerTx: LedgerTransaction = twiceSignedTx.toLedgerTransaction(serviceHub) - // DOCEND 32 - - // We can now perform our additional verification. - // DOCSTART 34 - val outputState: DummyState = ledgerTx.outputsOfType().single() - if (outputState.magicNumber == 777) { - // ``FlowException`` is a special exception type. It will be - // propagated back to any counterparty flows waiting for a - // message from this flow, notifying them that the flow has - // failed. - throw FlowException("We expected a magic number of 777.") - } - // DOCEND 34 - - // Of course, if you are not a required signer on the transaction, - // you have no power to decide whether it is valid or not. If it - // requires signatures from all the required signers and is - // contractually valid, it's a valid ledger update. - - /**----------------------- - * GATHERING SIGNATURES * - -----------------------**/ - progressTracker.currentStep = SIGS_GATHERING - - // The list of parties who need to sign a transaction is dictated - // by the transaction's commands. Once we've signed a transaction - // ourselves, we can automatically gather the signatures of the - // other required signers using ``CollectSignaturesFlow``. - // The responder flow will need to call ``SignTransactionFlow``. - // DOCSTART 15 - val fullySignedTx: SignedTransaction = subFlow(CollectSignaturesFlow(twiceSignedTx, setOf(counterpartySession, regulatorSession), SIGS_GATHERING.childProgressTracker())) - // DOCEND 15 - - /**----------------------- - * VERIFYING SIGNATURES * - -----------------------**/ - progressTracker.currentStep = VERIFYING_SIGS - - // We can verify that a transaction has all the required - // signatures, and that they're all valid, by running: - // DOCSTART 35 - fullySignedTx.verifyRequiredSignatures() - // DOCEND 35 - - // If the transaction is only partially signed, we have to pass in - // a list of the public keys corresponding to the missing - // signatures, explicitly telling the system not to check them. - // DOCSTART 36 - onceSignedTx.verifySignaturesExcept(counterpartyPubKey) - // DOCEND 36 - - // We can also choose to only check the signatures that are - // present. BE VERY CAREFUL - this function provides no guarantees - // that the signatures are correct, or that none are missing. - // DOCSTART 37 - twiceSignedTx.checkSignaturesAreValid() - // DOCEND 37 - - /**----------------------------- - * FINALISING THE TRANSACTION * - -----------------------------**/ - progressTracker.currentStep = FINALISATION - - // We notarise the transaction and get it recorded in the vault of - // the participants of all the transaction's states. - // DOCSTART 9 - val notarisedTx1: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, FINALISATION.childProgressTracker())) - // DOCEND 9 - // We can also choose to send it to additional parties who aren't one - // of the state's participants. - // DOCSTART 10 - val additionalParties: Set = setOf(regulator) - val notarisedTx2: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, additionalParties, FINALISATION.childProgressTracker())) - // DOCEND 10 - - // DOCSTART FlowSession porting - send(regulator, Any()) // Old API - // becomes - val session = initiateFlow(regulator) - session.send(Any()) - // DOCEND FlowSession porting + object VERIFYING_SIGS : Step("Verifying a transaction's signatures.") + object FINALISATION : Step("Finalising a transaction.") { + override fun childProgressTracker() = FinalityFlow.tracker() } + + fun tracker() = ProgressTracker( + ID_OTHER_NODES, + SENDING_AND_RECEIVING_DATA, + EXTRACTING_VAULT_STATES, + OTHER_TX_COMPONENTS, + TX_BUILDING, + TX_SIGNING, + TX_VERIFICATION, + SIGS_GATHERING, + VERIFYING_SIGS, + FINALISATION + ) } + // DOCEND 17 - // ``ResponderFlow`` is our second flow, and will communicate with - // ``InitiatorFlow``. - // We mark ``ResponderFlow`` as an ``InitiatedByFlow``, meaning that it - // can only be started in response to a message from its initiating flow. - // That's ``InitiatorFlow`` in this case. - // Each node also has several flow pairs registered by default - see - // ``AbstractNode.installCoreFlows``. - @InitiatedBy(InitiatorFlow::class) - class ResponderFlow(val counterpartySession: FlowSession) : FlowLogic() { + override val progressTracker: ProgressTracker = tracker() - companion object { - object RECEIVING_AND_SENDING_DATA : Step("Sending data between parties.") - object SIGNING : Step("Responding to CollectSignaturesFlow.") - object FINALISATION : Step("Finalising a transaction.") + @Suppress("RemoveExplicitTypeArguments") + @Suspendable + override fun call() { + // We'll be using a dummy public key for demonstration purposes. + // These are built in to Corda, and are generally used for writing + // tests. + val dummyPubKey: PublicKey = ALICE_PUBKEY - fun tracker() = ProgressTracker( - RECEIVING_AND_SENDING_DATA, - SIGNING, - FINALISATION - ) + /**-------------------------- + * IDENTIFYING OTHER NODES * + --------------------------**/ + // DOCSTART 18 + progressTracker.currentStep = ID_OTHER_NODES + // DOCEND 18 + + // A transaction generally needs a notary: + // - To prevent double-spends if the transaction has inputs + // - To serve as a timestamping authority if the transaction has a + // time-window + // We retrieve the notary from the network map. + // DOCSTART 1 + val notaryName: CordaX500Name = CordaX500Name( + organisation = "Notary Service", + locality = "London", + country = "GB") + val specificNotary: Party = serviceHub.networkMapCache.getNotary(notaryName)!! + // Alternatively, we can pick an arbitrary notary from the notary + // list. However, it is always preferable to specify the notary + // explicitly, as the notary list might change when new notaries are + // introduced, or old ones decommissioned. + val firstNotary: Party = serviceHub.networkMapCache.notaryIdentities.first() + // DOCEND 1 + + // We may also need to identify a specific counterparty. We do so + // using the identity service. + // DOCSTART 2 + val counterpartyName: CordaX500Name = CordaX500Name( + organisation = "NodeA", + locality = "London", + country = "GB") + val namedCounterparty: Party = serviceHub.identityService.wellKnownPartyFromX500Name(counterpartyName) ?: + throw IllegalArgumentException("Couldn't find counterparty for NodeA in identity service") + val keyedCounterparty: Party = serviceHub.identityService.partyFromKey(dummyPubKey) ?: + throw IllegalArgumentException("Couldn't find counterparty with key: $dummyPubKey in identity service") + // DOCEND 2 + + /**----------------------------- + * SENDING AND RECEIVING DATA * + -----------------------------**/ + progressTracker.currentStep = SENDING_AND_RECEIVING_DATA + + // We start by initiating a flow session with the counterparty. We + // will use this session to send and receive messages from the + // counterparty. + // DOCSTART initiateFlow + val counterpartySession: FlowSession = initiateFlow(counterparty) + // DOCEND initiateFlow + + // We can send arbitrary data to a counterparty. + // If this is the first ``send``, the counterparty will either: + // 1. Ignore the message if they are not registered to respond + // to messages from this flow. + // 2. Start the flow they have registered to respond to this flow, + // and run the flow until the first call to ``receive``, at + // which point they process the message. + // In other words, we are assuming that the counterparty is + // registered to respond to this flow, and has a corresponding + // ``receive`` call. + // DOCSTART 4 + counterpartySession.send(Any()) + // DOCEND 4 + + // We can wait to receive arbitrary data of a specific type from a + // counterparty. Again, this implies a corresponding ``send`` call + // in the counterparty's flow. A few scenarios: + // - We never receive a message back. In the current design, the + // flow is paused until the node's owner kills the flow. + // - Instead of sending a message back, the counterparty throws a + // ``FlowException``. This exception is propagated back to us, + // and we can use the error message to establish what happened. + // - We receive a message back, but it's of the wrong type. In + // this case, a ``FlowException`` is thrown. + // - We receive back a message of the correct type. All is good. + // + // Upon calling ``receive()`` (or ``sendAndReceive()``), the + // ``FlowLogic`` is suspended until it receives a response. + // + // We receive the data wrapped in an ``UntrustworthyData`` + // instance. This is a reminder that the data we receive may not + // be what it appears to be! We must unwrap the + // ``UntrustworthyData`` using a lambda. + // DOCSTART 5 + val packet1: UntrustworthyData = counterpartySession.receive() + val int: Int = packet1.unwrap { data -> + // Perform checking on the object received. + // T O D O: Check the received object. + // Return the object. + data } + // DOCEND 5 - override val progressTracker: ProgressTracker = tracker() + // We can also use a single call to send data to a counterparty + // and wait to receive data of a specific type back. The type of + // data sent doesn't need to match the type of the data received + // back. + // DOCSTART 7 + val packet2: UntrustworthyData = counterpartySession.sendAndReceive("You can send and receive any class!") + val boolean: Boolean = packet2.unwrap { data -> + // Perform checking on the object received. + // T O D O: Check the received object. + // Return the object. + data + } + // DOCEND 7 - @Suspendable - override fun call() { - // The ``ResponderFlow` has all the same APIs available. It looks - // up network information, sends and receives data, and constructs - // transactions in exactly the same way. + // We're not limited to sending to and receiving from a single + // counterparty. A flow can send messages to as many parties as it + // likes, and each party can invoke a different response flow. + // DOCSTART 6 + val regulatorSession: FlowSession = initiateFlow(regulator) + regulatorSession.send(Any()) + val packet3: UntrustworthyData = regulatorSession.receive() + // DOCEND 6 - /**----------------------------- - * SENDING AND RECEIVING DATA * - -----------------------------**/ - progressTracker.currentStep = RECEIVING_AND_SENDING_DATA + /**----------------------------------- + * EXTRACTING STATES FROM THE VAULT * + -----------------------------------**/ + progressTracker.currentStep = EXTRACTING_VAULT_STATES - // We need to respond to the messages sent by the initiator: - // 1. They sent us an ``Any`` instance - // 2. They waited to receive an ``Integer`` instance back - // 3. They sent a ``String`` instance and waited to receive a - // ``Boolean`` instance back - // Our side of the flow must mirror these calls. - // DOCSTART 8 - val any: Any = counterpartySession.receive().unwrap { data -> data } - val string: String = counterpartySession.sendAndReceive(99).unwrap { data -> data } - counterpartySession.send(true) - // DOCEND 8 + // Let's assume there are already some ``DummyState``s in our + // node's vault, stored there as a result of running past flows, + // and we want to consume them in a transaction. There are many + // ways to extract these states from our vault. - /**---------------------------------------- - * RESPONDING TO COLLECT_SIGNATURES_FLOW * - ----------------------------------------**/ - progressTracker.currentStep = SIGNING + // For example, we would extract any unconsumed ``DummyState``s + // from our vault as follows: + val criteria: VaultQueryCriteria = VaultQueryCriteria() // default is UNCONSUMED + val results: Page = serviceHub.vaultService.queryBy(criteria) + val dummyStates: List> = results.states - // The responder will often need to respond to a call to - // ``CollectSignaturesFlow``. It does so my invoking its own - // ``SignTransactionFlow`` subclass. - // DOCSTART 16 - val signTransactionFlow: SignTransactionFlow = object : SignTransactionFlow(counterpartySession) { - override fun checkTransaction(stx: SignedTransaction) = requireThat { - // Any additional checking we see fit... - val outputState = stx.tx.outputsOfType().single() - assert(outputState.magicNumber == 777) - } + // For a full list of the available ways of extracting states from + // the vault, see the Vault Query docs page. + + // When building a transaction, input states are passed in as + // ``StateRef`` instances, which pair the hash of the transaction + // that generated the state with the state's index in the outputs + // of that transaction. In practice, we'd pass the transaction hash + // or the ``StateRef`` as a parameter to the flow, or extract the + // ``StateRef`` from our vault. + // DOCSTART 20 + val ourStateRef: StateRef = StateRef(SecureHash.sha256("DummyTransactionHash"), 0) + // DOCEND 20 + // A ``StateAndRef`` pairs a ``StateRef`` with the state it points to. + // DOCSTART 21 + val ourStateAndRef: StateAndRef = serviceHub.toStateAndRef(ourStateRef) + // DOCEND 21 + + /**----------------------------------------- + * GATHERING OTHER TRANSACTION COMPONENTS * + -----------------------------------------**/ + progressTracker.currentStep = OTHER_TX_COMPONENTS + + // Output states are constructed from scratch. + // DOCSTART 22 + val ourOutputState: DummyState = DummyState() + // DOCEND 22 + // Or as copies of other states with some properties changed. + // DOCSTART 23 + val ourOtherOutputState: DummyState = ourOutputState.copy(magicNumber = 77) + // DOCEND 23 + + // We then need to pair our output state with a contract. + // DOCSTART 47 + val contractName: String = "net.corda.testing.contracts.DummyContract" + val ourOutput: StateAndContract = StateAndContract(ourOutputState, contractName) + // DOCEND 47 + + // Commands pair a ``CommandData`` instance with a list of + // public keys. To be valid, the transaction requires a signature + // matching every public key in all of the transaction's commands. + // DOCSTART 24 + val commandData: DummyContract.Commands.Create = DummyContract.Commands.Create() + val ourPubKey: PublicKey = serviceHub.myInfo.legalIdentitiesAndCerts.first().owningKey + val counterpartyPubKey: PublicKey = counterparty.owningKey + val requiredSigners: List = listOf(ourPubKey, counterpartyPubKey) + val ourCommand: Command = Command(commandData, requiredSigners) + // DOCEND 24 + + // ``CommandData`` can either be: + // 1. Of type ``TypeOnlyCommandData``, in which case it only + // serves to attach signers to the transaction and possibly + // fork the contract's verification logic. + val typeOnlyCommandData: TypeOnlyCommandData = DummyContract.Commands.Create() + // 2. Include additional data which can be used by the contract + // during verification, alongside fulfilling the roles above. + val commandDataWithData: CommandData = Cash.Commands.Issue() + + // Attachments are identified by their hash. + // The attachment with the corresponding hash must have been + // uploaded ahead of time via the node's RPC interface. + // DOCSTART 25 + val ourAttachment: SecureHash = SecureHash.sha256("DummyAttachment") + // DOCEND 25 + + // Time windows can have a start and end time, or be open at either end. + // DOCSTART 26 + val ourTimeWindow: TimeWindow = TimeWindow.between(Instant.MIN, Instant.MAX) + val ourAfter: TimeWindow = TimeWindow.fromOnly(Instant.MIN) + val ourBefore: TimeWindow = TimeWindow.untilOnly(Instant.MAX) + // DOCEND 26 + + // We can also define a time window as an ``Instant`` +/- a time + // tolerance (e.g. 30 seconds): + // DOCSTART 42 + val ourTimeWindow2: TimeWindow = TimeWindow.withTolerance(serviceHub.clock.instant(), 30.seconds) + // DOCEND 42 + // Or as a start-time plus a duration: + // DOCSTART 43 + val ourTimeWindow3: TimeWindow = TimeWindow.fromStartAndDuration(serviceHub.clock.instant(), 30.seconds) + // DOCEND 43 + + /**----------------------- + * TRANSACTION BUILDING * + -----------------------**/ + progressTracker.currentStep = TX_BUILDING + + // If our transaction has input states or a time-window, we must instantiate it with a + // notary. + // DOCSTART 19 + val txBuilder: TransactionBuilder = TransactionBuilder(specificNotary) + // DOCEND 19 + + // Otherwise, we can choose to instantiate it without one: + // DOCSTART 46 + val txBuilderNoNotary: TransactionBuilder = TransactionBuilder() + // DOCEND 46 + + // We add items to the transaction builder using ``TransactionBuilder.withItems``: + // DOCSTART 27 + txBuilder.withItems( + // Inputs, as ``StateAndRef``s that reference the outputs of previous transactions + ourStateAndRef, + // Outputs, as ``StateAndContract``s + ourOutput, + // Commands, as ``Command``s + ourCommand, + // Attachments, as ``SecureHash``es + ourAttachment, + // A time-window, as ``TimeWindow`` + ourTimeWindow + ) + // DOCEND 27 + + // We can also add items using methods for the individual components. + + // The individual methods for adding input states and attachments: + // DOCSTART 28 + txBuilder.addInputState(ourStateAndRef) + txBuilder.addAttachment(ourAttachment) + // DOCEND 28 + + // An output state can be added as a ``ContractState``, contract class name and notary. + // DOCSTART 49 + txBuilder.addOutputState(ourOutputState, DummyContract.PROGRAM_ID, specificNotary) + // DOCEND 49 + // We can also leave the notary field blank, in which case the transaction's default + // notary is used. + // DOCSTART 50 + txBuilder.addOutputState(ourOutputState, DummyContract.PROGRAM_ID) + // DOCEND 50 + // Or we can add the output state as a ``TransactionState``, which already specifies + // the output's contract and notary. + // DOCSTART 51 + val txState: TransactionState = TransactionState(ourOutputState, DummyContract.PROGRAM_ID, specificNotary) + // DOCEND 51 + + // Commands can be added as ``Command``s. + // DOCSTART 52 + txBuilder.addCommand(ourCommand) + // DOCEND 52 + // Or as ``CommandData`` and a ``vararg PublicKey``. + // DOCSTART 53 + txBuilder.addCommand(commandData, ourPubKey, counterpartyPubKey) + // DOCEND 53 + + // We can set a time-window directly. + // DOCSTART 44 + txBuilder.setTimeWindow(ourTimeWindow) + // DOCEND 44 + // Or as a start time plus a duration (e.g. 45 seconds). + // DOCSTART 45 + txBuilder.setTimeWindow(serviceHub.clock.instant(), 45.seconds) + // DOCEND 45 + + /**---------------------- + * TRANSACTION SIGNING * + ----------------------**/ + progressTracker.currentStep = TX_SIGNING + + // We finalise the transaction by signing it, converting it into a + // ``SignedTransaction``. + // DOCSTART 29 + val onceSignedTx: SignedTransaction = serviceHub.signInitialTransaction(txBuilder) + // DOCEND 29 + // We can also sign the transaction using a different public key: + // DOCSTART 30 + val otherKey: PublicKey = serviceHub.keyManagementService.freshKey() + val onceSignedTx2: SignedTransaction = serviceHub.signInitialTransaction(txBuilder, otherKey) + // DOCEND 30 + + // If instead this was a ``SignedTransaction`` that we'd received + // from a counterparty and we needed to sign it, we would add our + // signature using: + // DOCSTART 38 + val twiceSignedTx: SignedTransaction = serviceHub.addSignature(onceSignedTx) + // DOCEND 38 + // Or, if we wanted to use a different public key: + val otherKey2: PublicKey = serviceHub.keyManagementService.freshKey() + // DOCSTART 39 + val twiceSignedTx2: SignedTransaction = serviceHub.addSignature(onceSignedTx, otherKey2) + // DOCEND 39 + + // We can also generate a signature over the transaction without + // adding it to the transaction itself. We may do this when + // sending just the signature in a flow instead of returning the + // entire transaction with our signature. This way, the receiving + // node does not need to check we haven't changed anything in the + // transaction. + // DOCSTART 40 + val sig: TransactionSignature = serviceHub.createSignature(onceSignedTx) + // DOCEND 40 + // And again, if we wanted to use a different public key: + // DOCSTART 41 + val sig2: TransactionSignature = serviceHub.createSignature(onceSignedTx, otherKey2) + // DOCEND 41 + + // In practice, however, the process of gathering every signature + // but the first can be automated using ``CollectSignaturesFlow``. + // See the "Gathering Signatures" section below. + + /**--------------------------- + * TRANSACTION VERIFICATION * + ---------------------------**/ + progressTracker.currentStep = TX_VERIFICATION + + // Verifying a transaction will also verify every transaction in + // the transaction's dependency chain, which will require + // transaction data access on counterparty's node. The + // ``SendTransactionFlow`` can be used to automate the sending and + // data vending process. The ``SendTransactionFlow`` will listen + // for data request until the transaction is resolved and verified + // on the other side: + // DOCSTART 12 + subFlow(SendTransactionFlow(counterpartySession, twiceSignedTx)) + + // Optional request verification to further restrict data access. + subFlow(object : SendTransactionFlow(counterpartySession, twiceSignedTx) { + override fun verifyDataRequest(dataRequest: FetchDataFlow.Request.Data) { + // Extra request verification. } + }) + // DOCEND 12 - subFlow(signTransactionFlow) - // DOCEND 16 + // We can receive the transaction using ``ReceiveTransactionFlow``, + // which will automatically download all the dependencies and verify + // the transaction + // DOCSTART 13 + val verifiedTransaction = subFlow(ReceiveTransactionFlow(counterpartySession)) + // DOCEND 13 - /**----------------------------- - * FINALISING THE TRANSACTION * - -----------------------------**/ - progressTracker.currentStep = FINALISATION + // We can also send and receive a `StateAndRef` dependency chain + // and automatically resolve its dependencies. + // DOCSTART 14 + subFlow(SendStateAndRefFlow(counterpartySession, dummyStates)) - // Nothing to do here! As long as some other party calls - // ``FinalityFlow``, the recording of the transaction on our node - // we be handled automatically. + // On the receive side ... + val resolvedStateAndRef = subFlow(ReceiveStateAndRefFlow(counterpartySession)) + // DOCEND 14 + + // We can now verify the transaction to ensure that it satisfies + // the contracts of all the transaction's input and output states. + // DOCSTART 33 + twiceSignedTx.verify(serviceHub) + // DOCEND 33 + + // We'll often want to perform our own additional verification + // too. Just because a transaction is valid based on the contract + // rules and requires our signature doesn't mean we have to + // sign it! We need to make sure the transaction represents an + // agreement we actually want to enter into. + + // To do this, we need to convert our ``SignedTransaction`` + // into a ``LedgerTransaction``. This will use our ServiceHub + // to resolve the transaction's inputs and attachments into + // actual objects, rather than just references. + // DOCSTART 32 + val ledgerTx: LedgerTransaction = twiceSignedTx.toLedgerTransaction(serviceHub) + // DOCEND 32 + + // We can now perform our additional verification. + // DOCSTART 34 + val outputState: DummyState = ledgerTx.outputsOfType().single() + if (outputState.magicNumber == 777) { + // ``FlowException`` is a special exception type. It will be + // propagated back to any counterparty flows waiting for a + // message from this flow, notifying them that the flow has + // failed. + throw FlowException("We expected a magic number of 777.") } + // DOCEND 34 + + // Of course, if you are not a required signer on the transaction, + // you have no power to decide whether it is valid or not. If it + // requires signatures from all the required signers and is + // contractually valid, it's a valid ledger update. + + /**----------------------- + * GATHERING SIGNATURES * + -----------------------**/ + progressTracker.currentStep = SIGS_GATHERING + + // The list of parties who need to sign a transaction is dictated + // by the transaction's commands. Once we've signed a transaction + // ourselves, we can automatically gather the signatures of the + // other required signers using ``CollectSignaturesFlow``. + // The responder flow will need to call ``SignTransactionFlow``. + // DOCSTART 15 + val fullySignedTx: SignedTransaction = subFlow(CollectSignaturesFlow(twiceSignedTx, setOf(counterpartySession, regulatorSession), SIGS_GATHERING.childProgressTracker())) + // DOCEND 15 + + /**----------------------- + * VERIFYING SIGNATURES * + -----------------------**/ + progressTracker.currentStep = VERIFYING_SIGS + + // We can verify that a transaction has all the required + // signatures, and that they're all valid, by running: + // DOCSTART 35 + fullySignedTx.verifyRequiredSignatures() + // DOCEND 35 + + // If the transaction is only partially signed, we have to pass in + // a list of the public keys corresponding to the missing + // signatures, explicitly telling the system not to check them. + // DOCSTART 36 + onceSignedTx.verifySignaturesExcept(counterpartyPubKey) + // DOCEND 36 + + // We can also choose to only check the signatures that are + // present. BE VERY CAREFUL - this function provides no guarantees + // that the signatures are correct, or that none are missing. + // DOCSTART 37 + twiceSignedTx.checkSignaturesAreValid() + // DOCEND 37 + + /**----------------------------- + * FINALISING THE TRANSACTION * + -----------------------------**/ + progressTracker.currentStep = FINALISATION + + // We notarise the transaction and get it recorded in the vault of + // the participants of all the transaction's states. + // DOCSTART 9 + val notarisedTx1: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, FINALISATION.childProgressTracker())) + // DOCEND 9 + // We can also choose to send it to additional parties who aren't one + // of the state's participants. + // DOCSTART 10 + val additionalParties: Set = setOf(regulator) + val notarisedTx2: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, additionalParties, FINALISATION.childProgressTracker())) + // DOCEND 10 + + // DOCSTART FlowSession porting + send(regulator, Any()) // Old API + // becomes + val session = initiateFlow(regulator) + session.send(Any()) + // DOCEND FlowSession porting + } +} + +// ``ResponderFlow`` is our second flow, and will communicate with +// ``InitiatorFlow``. +// We mark ``ResponderFlow`` as an ``InitiatedByFlow``, meaning that it +// can only be started in response to a message from its initiating flow. +// That's ``InitiatorFlow`` in this case. +// Each node also has several flow pairs registered by default - see +// ``AbstractNode.installCoreFlows``. +@InitiatedBy(InitiatorFlow::class) +class ResponderFlow(val counterpartySession: FlowSession) : FlowLogic() { + + companion object { + object RECEIVING_AND_SENDING_DATA : Step("Sending data between parties.") + object SIGNING : Step("Responding to CollectSignaturesFlow.") + object FINALISATION : Step("Finalising a transaction.") + + fun tracker() = ProgressTracker( + RECEIVING_AND_SENDING_DATA, + SIGNING, + FINALISATION + ) + } + + override val progressTracker: ProgressTracker = tracker() + + @Suspendable + override fun call() { + // The ``ResponderFlow` has all the same APIs available. It looks + // up network information, sends and receives data, and constructs + // transactions in exactly the same way. + + /**----------------------------- + * SENDING AND RECEIVING DATA * + -----------------------------**/ + progressTracker.currentStep = RECEIVING_AND_SENDING_DATA + + // We need to respond to the messages sent by the initiator: + // 1. They sent us an ``Any`` instance + // 2. They waited to receive an ``Integer`` instance back + // 3. They sent a ``String`` instance and waited to receive a + // ``Boolean`` instance back + // Our side of the flow must mirror these calls. + // DOCSTART 8 + val any: Any = counterpartySession.receive().unwrap { data -> data } + val string: String = counterpartySession.sendAndReceive(99).unwrap { data -> data } + counterpartySession.send(true) + // DOCEND 8 + + /**---------------------------------------- + * RESPONDING TO COLLECT_SIGNATURES_FLOW * + ----------------------------------------**/ + progressTracker.currentStep = SIGNING + + // The responder will often need to respond to a call to + // ``CollectSignaturesFlow``. It does so my invoking its own + // ``SignTransactionFlow`` subclass. + // DOCSTART 16 + val signTransactionFlow: SignTransactionFlow = object : SignTransactionFlow(counterpartySession) { + override fun checkTransaction(stx: SignedTransaction) = requireThat { + // Any additional checking we see fit... + val outputState = stx.tx.outputsOfType().single() + assert(outputState.magicNumber == 777) + } + } + + subFlow(signTransactionFlow) + // DOCEND 16 + + /**----------------------------- + * FINALISING THE TRANSACTION * + -----------------------------**/ + progressTracker.currentStep = FINALISATION + + // Nothing to do here! As long as some other party calls + // ``FinalityFlow``, the recording of the transaction on our node + // we be handled automatically. } } 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/tutorial/contract/TutorialContract.kt new file mode 100644 index 0000000000..8389e73a99 --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt @@ -0,0 +1,136 @@ +package net.corda.docs.tutorial.contract + +import net.corda.core.contracts.* +import net.corda.core.crypto.NullKeys +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub +import net.corda.core.transactions.LedgerTransaction +import net.corda.core.transactions.TransactionBuilder +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.utils.sumCashBy +import net.corda.testing.chooseIdentityAndCert +import java.time.Instant +import java.util.* + +class CommercialPaper : Contract { + // DOCSTART 8 + companion object { + const val CP_PROGRAM_ID: ContractClassName = "net.corda.finance.contracts.CommercialPaper" + } + // DOCEND 8 + + // DOCSTART 3 + override fun verify(tx: LedgerTransaction) { + // Group by everything except owner: any modification to the CP at all is considered changing it fundamentally. + val groups = tx.groupStates(State::withoutOwner) + + // There are two possible things that can be done with this CP. The first is trading it. The second is redeeming + // it for cash on or after the maturity date. + val command = tx.commands.requireSingleCommand() + // DOCEND 3 + + // DOCSTART 4 + val timeWindow: TimeWindow? = tx.timeWindow + + for ((inputs, outputs, _) in groups) { + when (command.value) { + is Commands.Move -> { + val input = inputs.single() + requireThat { + "the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers) + "the state is propagated" using (outputs.size == 1) + // Don't need to check anything else, as if outputs.size == 1 then the output is equal to + // the input ignoring the owner field due to the grouping. + } + } + + is Commands.Redeem -> { + // Redemption of the paper requires movement of on-ledger cash. + val input = inputs.single() + val received = tx.outputs.map { it.data }.sumCashBy(input.owner) + val time = timeWindow?.fromTime ?: throw IllegalArgumentException("Redemptions must be timestamped") + requireThat { + "the paper must have matured" using (time >= input.maturityDate) + "the received amount equals the face value" using (received == input.faceValue) + "the paper must be destroyed" using outputs.isEmpty() + "the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers) + } + } + + is Commands.Issue -> { + val output = outputs.single() + val time = timeWindow?.untilTime ?: throw IllegalArgumentException("Issuances must be timestamped") + requireThat { + // Don't allow people to issue commercial paper under other entities identities. + "output states are issued by a command signer" using (output.issuance.party.owningKey in command.signers) + "output values sum to more than the inputs" using (output.faceValue.quantity > 0) + "the maturity date is not in the past" using (time < output.maturityDate) + // Don't allow an existing CP state to be replaced by this issuance. + "can't reissue an existing state" using inputs.isEmpty() + } + } + + else -> throw IllegalArgumentException("Unrecognised command") + } + } + // DOCEND 4 + } + + // DOCSTART 2 + interface Commands : CommandData { + class Move : TypeOnlyCommandData(), Commands + class Redeem : TypeOnlyCommandData(), Commands + class Issue : TypeOnlyCommandData(), Commands + } + // DOCEND 2 + + // DOCSTART 5 + fun generateIssue(issuance: PartyAndReference, faceValue: Amount>, maturityDate: Instant, + notary: Party): TransactionBuilder { + val state = State(issuance, issuance.party, faceValue, maturityDate) + val stateAndContract = StateAndContract(state, CP_PROGRAM_ID) + return TransactionBuilder(notary = notary).withItems(stateAndContract, Command(Commands.Issue(), issuance.party.owningKey)) + } + // DOCEND 5 + + // DOCSTART 6 + fun generateMove(tx: TransactionBuilder, paper: StateAndRef, newOwner: AbstractParty) { + tx.addInputState(paper) + val outputState = paper.state.data.withNewOwner(newOwner).ownableState + tx.addOutputState(outputState, CP_PROGRAM_ID) + tx.addCommand(Command(Commands.Move(), paper.state.data.owner.owningKey)) + } + // DOCEND 6 + + // DOCSTART 7 + @Throws(InsufficientBalanceException::class) + fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, services: ServiceHub) { + // Add the cash movement using the states in our vault. + Cash.generateSpend( + services = services, + tx = tx, + amount = paper.state.data.faceValue.withoutIssuer(), + ourIdentity = services.myInfo.chooseIdentityAndCert(), + to = paper.state.data.owner + ) + tx.addInputState(paper) + tx.addCommand(Command(Commands.Redeem(), paper.state.data.owner.owningKey)) + } + // DOCEND 7 +} + +// DOCSTART 1 +data class State( + val issuance: PartyAndReference, + override val owner: AbstractParty, + val faceValue: Amount>, + val maturityDate: Instant +) : OwnableState { + override val participants = listOf(owner) + + fun withoutOwner() = copy(owner = AnonymousParty(NullKeys.NullPublicKey)) + override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(CommercialPaper.Commands.Move(), copy(owner = newOwner)) +} +// DOCEND 1 \ No newline at end of file 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/tutorial/flowstatemachines/TutorialFlowStateMachines.kt new file mode 100644 index 0000000000..0131077a8f --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/flowstatemachines/TutorialFlowStateMachines.kt @@ -0,0 +1,64 @@ +package net.corda.docs.tutorial.flowstatemachines + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Amount +import net.corda.core.contracts.OwnableState +import net.corda.core.contracts.StateAndRef +import net.corda.core.flows.FlowException +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.serialization.CordaSerializable +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.ProgressTracker +import net.corda.finance.flows.TwoPartyTradeFlow +import java.util.* + +// DOCSTART 1 +object TwoPartyTradeFlow { + class UnacceptablePriceException(givenPrice: Amount) : FlowException("Unacceptable price: $givenPrice") + class AssetMismatchException(val expectedTypeName: String, val typeName: String) : FlowException() { + override fun toString() = "The submitted asset didn't match the expected type: $expectedTypeName vs $typeName" + } + + /** + * This object is serialised to the network and is the first flow message the seller sends to the buyer. + * + * @param payToIdentity anonymous identity of the seller, for payment to be sent to. + */ + @CordaSerializable + data class SellerTradeInfo( + val price: Amount, + val payToIdentity: PartyAndCertificate + ) + + open class Seller(private val otherSideSession: FlowSession, + private val assetToSell: StateAndRef, + private val price: Amount, + private val myParty: PartyAndCertificate, + override val progressTracker: ProgressTracker = TwoPartyTradeFlow.Seller.tracker()) : FlowLogic() { + + companion object { + fun tracker() = ProgressTracker() + } + + @Suspendable + override fun call(): SignedTransaction { + TODO() + } + } + + open class Buyer(private val sellerSession: FlowSession, + private val notary: Party, + private val acceptablePrice: Amount, + private val typeToBuy: Class, + private val anonymous: Boolean) : FlowLogic() { + + @Suspendable + override fun call(): SignedTransaction { + TODO() + } + } +} +// DOCEND 1 \ No newline at end of file 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/tutorial/tearoffs/TutorialTearOffs.kt new file mode 100644 index 0000000000..c560c4f3f4 --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/tearoffs/TutorialTearOffs.kt @@ -0,0 +1,45 @@ +package net.corda.docs.tutorial.tearoffs + +import net.corda.core.contracts.Command +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TimeWindow +import net.corda.core.crypto.MerkleTreeException +import net.corda.core.transactions.FilteredTransaction +import net.corda.core.transactions.FilteredTransactionVerificationException +import net.corda.core.transactions.SignedTransaction +import net.corda.finance.contracts.Fix +import net.corda.testing.ALICE +import java.util.function.Predicate + +fun main(args: Array) { + // Typealias to make the example coherent. + val oracle = ALICE + val stx = Any() as SignedTransaction + + // DOCSTART 1 + val filtering = Predicate { + when (it) { + is Command<*> -> oracle.owningKey in it.signers && it.value is Fix + else -> false + } + } + // DOCEND 1 + + // DOCSTART 2 + val ftx: FilteredTransaction = stx.buildFilteredTransaction(filtering) + // DOCEND 2 + + // DOCSTART 3 + // Direct access to included commands, inputs, outputs, attachments etc. + val cmds: List> = ftx.commands + val ins: List = ftx.inputs + val timeWindow: TimeWindow? = ftx.timeWindow + // ... + // DOCEND 3 + + try { + ftx.verify() + } catch (e: FilteredTransactionVerificationException) { + throw MerkleTreeException("Rate Fix Oracle: Couldn't verify partial Merkle tree.") + } +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt new file mode 100644 index 0000000000..2b25fe726e --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt @@ -0,0 +1,247 @@ +package net.corda.docs.tutorial.testdsl + +import net.corda.core.utilities.days +import net.corda.finance.DOLLARS +import net.corda.finance.`issued by` +import net.corda.finance.contracts.CP_PROGRAM_ID +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.finance.contracts.asset.`issued by` +import net.corda.finance.contracts.asset.`owned by` +import net.corda.testing.* +import org.junit.Test + +class CommercialPaperTest { + // DOCSTART 1 + fun getPaper(): ICommercialPaperState = CommercialPaper.State( + issuance = MEGA_CORP.ref(123), + owner = MEGA_CORP, + faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123), + maturityDate = TEST_TX_TIME + 7.days + ) + // DOCEND 1 + + // DOCSTART 2 + @Test + fun simpleCP() { + val inState = getPaper() + ledger { + transaction { + attachments(CP_PROGRAM_ID) + input(CP_PROGRAM_ID) { inState } + this.verifies() + } + } + } + // DOCEND 2 + + // DOCSTART 3 + @Test + fun simpleCPMove() { + val inState = getPaper() + ledger { + transaction { + input(CP_PROGRAM_ID) { inState } + command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } + attachments(CP_PROGRAM_ID) + this.verifies() + } + } + } + // DOCEND 3 + + // DOCSTART 4 + @Test + fun simpleCPMoveFails() { + val inState = getPaper() + ledger { + transaction { + input(CP_PROGRAM_ID) { inState } + command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } + attachments(CP_PROGRAM_ID) + this `fails with` "the state is propagated" + } + } + } + // DOCEND 4 + + // DOCSTART 5 + @Test + fun simpleCPMoveSuccess() { + val inState = getPaper() + ledger { + transaction { + input(CP_PROGRAM_ID) { inState } + command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } + attachments(CP_PROGRAM_ID) + this `fails with` "the state is propagated" + output(CP_PROGRAM_ID, "alice's paper") { inState.withOwner(ALICE) } + this.verifies() + } + } + } + // DOCEND 5 + + // DOCSTART 6 + @Test + fun `simple issuance with tweak`() { + ledger { + transaction { + output(CP_PROGRAM_ID, "paper") { getPaper() } // Some CP is issued onto the ledger by MegaCorp. + attachments(CP_PROGRAM_ID) + tweak { + // The wrong pubkey. + command(BIG_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } + timeWindow(TEST_TX_TIME) + this `fails with` "output states are issued by a command signer" + } + command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } + timeWindow(TEST_TX_TIME) + this.verifies() + } + } + } + // DOCEND 6 + + // DOCSTART 7 + @Test + fun `simple issuance with tweak and top level transaction`() { + transaction { + output(CP_PROGRAM_ID, "paper") { getPaper() } // Some CP is issued onto the ledger by MegaCorp. + attachments(CP_PROGRAM_ID) + tweak { + // The wrong pubkey. + command(BIG_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } + timeWindow(TEST_TX_TIME) + this `fails with` "output states are issued by a command signer" + } + command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } + timeWindow(TEST_TX_TIME) + this.verifies() + } + } + // DOCEND 7 + + // DOCSTART 8 + @Test + fun `chain commercial paper`() { + val issuer = MEGA_CORP.ref(123) + + ledger { + unverifiedTransaction { + attachments(Cash.PROGRAM_ID) + output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE) + } + + // Some CP is issued onto the ledger by MegaCorp. + transaction("Issuance") { + output(CP_PROGRAM_ID, "paper") { getPaper() } + command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } + attachments(CP_PROGRAM_ID) + timeWindow(TEST_TX_TIME) + this.verifies() + } + + + transaction("Trade") { + input("paper") + input("alice's $900") + output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP } + output(CP_PROGRAM_ID, "alice's paper") { "paper".output().withOwner(ALICE) } + command(ALICE_PUBKEY) { Cash.Commands.Move() } + command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } + this.verifies() + } + } + } + // DOCEND 8 + + // DOCSTART 9 + @Test + fun `chain commercial paper double spend`() { + val issuer = MEGA_CORP.ref(123) + ledger { + unverifiedTransaction { + attachments(Cash.PROGRAM_ID) + output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE) + } + + // Some CP is issued onto the ledger by MegaCorp. + transaction("Issuance") { + output(CP_PROGRAM_ID, "paper") { getPaper() } + command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } + attachments(CP_PROGRAM_ID) + timeWindow(TEST_TX_TIME) + this.verifies() + } + + transaction("Trade") { + input("paper") + input("alice's $900") + output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP } + output(CP_PROGRAM_ID, "alice's paper") { "paper".output().withOwner(ALICE) } + command(ALICE_PUBKEY) { Cash.Commands.Move() } + command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } + this.verifies() + } + + transaction { + input("paper") + // We moved a paper to another pubkey. + output(CP_PROGRAM_ID, "bob's paper") { "paper".output().withOwner(BOB) } + command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } + this.verifies() + } + + this.fails() + } + } + // DOCEND 9 + + // DOCSTART 10 + @Test + fun `chain commercial tweak`() { + val issuer = MEGA_CORP.ref(123) + ledger { + unverifiedTransaction { + attachments(Cash.PROGRAM_ID) + output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE) + } + + // Some CP is issued onto the ledger by MegaCorp. + transaction("Issuance") { + output(CP_PROGRAM_ID, "paper") { getPaper() } + command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } + attachments(CP_PROGRAM_ID) + timeWindow(TEST_TX_TIME) + this.verifies() + } + + transaction("Trade") { + input("paper") + input("alice's $900") + output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP } + output(CP_PROGRAM_ID, "alice's paper") { "paper".output().withOwner(ALICE) } + command(ALICE_PUBKEY) { Cash.Commands.Move() } + command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } + this.verifies() + } + + tweak { + transaction { + input("paper") + // We moved a paper to another pubkey. + output(CP_PROGRAM_ID, "bob's paper") { "paper".output().withOwner(BOB) } + command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } + this.verifies() + } + this.fails() + } + + this.verifies() + } + } + // DOCEND 10 +} \ No newline at end of file diff --git a/docs/source/flow-state-machines.rst b/docs/source/flow-state-machines.rst index a125c6c221..bc7e484ae1 100644 --- a/docs/source/flow-state-machines.rst +++ b/docs/source/flow-state-machines.rst @@ -86,13 +86,14 @@ Our flow has two parties (B and S for buyer and seller) and will proceed as foll 1. S sends a ``StateAndRef`` pointing to the state they want to sell to B, along with info about the price they require B to pay. -2. B sends to S a ``SignedTransaction`` that includes the state as input, B's cash as input, the state with the new - owner key as output, and any change cash as output. It contains a single signature from B but isn't valid because - it lacks a signature from S authorising movement of the asset. +2. B sends to S a ``SignedTransaction`` that includes two inputs (the state owned by S, and cash owned by B) and three + outputs (the state now owned by B, the cash now owned by S, and any change cash still owned by B). The + ``SignedTransaction`` has a single signature from B but isn't valid because it lacks a signature from S authorising + movement of the asset. 3. S signs the transaction and sends it back to B. -4. B *finalises* the transaction by sending it to the notary who checks the transaction for validity, - recording the transaction in B's local vault, and then sending it on to S who also checks it and commits - the transaction to S's local vault. +4. B *finalises* the transaction by sending it to the notary who checks the transaction for validity, recording the + transaction in B's local vault, and then sending it on to S who also checks it and commits the transaction to S's + local vault. You can find the implementation of this flow in the file ``finance/src/main/kotlin/net/corda/finance/TwoPartyTradeFlow.kt``. @@ -109,64 +110,31 @@ each side. .. container:: codeset - .. sourcecode:: kotlin - - object TwoPartyTradeFlow { - class UnacceptablePriceException(val givenPrice: Amount) : FlowException("Unacceptable price: $givenPrice") - class AssetMismatchException(val expectedTypeName: String, val typeName: String) : FlowException() { - override fun toString() = "The submitted asset didn't match the expected type: $expectedTypeName vs $typeName" - } - - // This object is serialised to the network and is the first flow message the seller sends to the buyer. - @CordaSerializable - data class SellerTradeInfo( - val assetForSale: StateAndRef, - val price: Amount, - val sellerOwnerKey: PublicKey - ) - - open class Seller(val otherParty: Party, - val notaryNode: NodeInfo, - val assetToSell: StateAndRef, - val price: Amount, - val myKey: PublicKey, - override val progressTracker: ProgressTracker = Seller.tracker()) : FlowLogic() { - @Suspendable - override fun call(): SignedTransaction { - TODO() - } - } - - open class Buyer(val otherParty: Party, - val notary: Party, - val acceptablePrice: Amount, - val typeToBuy: Class) : FlowLogic() { - @Suspendable - override fun call(): SignedTransaction { - TODO() - } - } - } + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/flowstatemachines/TutorialFlowStateMachines.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 This code defines several classes nested inside the main ``TwoPartyTradeFlow`` singleton. Some of the classes are simply flow messages or exceptions. The other two represent the buyer and seller side of the flow. Going through the data needed to become a seller, we have: -- ``otherParty: Party`` - the party with which you are trading. -- ``notaryNode: NodeInfo`` - the entry in the network map for the chosen notary. See ":doc:`key-concepts-notaries`" for more - information on notaries. -- ``assetToSell: StateAndRef`` - a pointer to the ledger entry that represents the thing being sold. -- ``price: Amount`` - the agreed on price that the asset is being sold for (without an issuer constraint). -- ``myKey: PublicKey`` - the PublicKey part of the node's internal KeyPair that controls the asset being sold. -The matching PrivateKey stored in the KeyManagementService will be used to sign the transaction. +- ``otherSideSession: FlowSession`` - a flow session for communication with the buyer +- ``assetToSell: StateAndRef`` - a pointer to the ledger entry that represents the thing being sold +- ``price: Amount`` - the agreed on price that the asset is being sold for (without an issuer constraint) +- ``myParty: PartyAndCertificate`` - the certificate representing the party that controls the asset being sold And for the buyer: +- ``sellerSession: FlowSession`` - a flow session for communication with the seller +- ``notary: Party`` - the entry in the network map for the chosen notary. See “Notaries” for more information on + notaries - ``acceptablePrice: Amount`` - the price that was agreed upon out of band. If the seller specifies - a price less than or equal to this, then the trade will go ahead. + a price less than or equal to this, then the trade will go ahead - ``typeToBuy: Class`` - the type of state that is being purchased. This is used to check that the - sell side of the flow isn't trying to sell us the wrong thing, whether by accident or on purpose. + sell side of the flow isn't trying to sell us the wrong thing, whether by accident or on purpose +- ``anonymous: Boolean`` - whether to generate a fresh, anonymous public key for the transaction Alright, so using this flow shouldn't be too hard: in the simplest case we can just create a Buyer or Seller with the details of the trade, depending on who we are. We then have to start the flow in some way. Just @@ -191,9 +159,9 @@ and try again. Whitelisted classes with the Corda node --------------------------------------- -For security reasons, we do not want Corda nodes to be able to receive instances of any class on the classpath +For security reasons, we do not want Corda nodes to be able to just receive instances of any class on the classpath via messaging, since this has been exploited in other Java application containers in the past. Instead, we require -that every class contained in messages is whitelisted. Some classes are whitelisted by default (see ``DefaultWhitelist``), +every class contained in messages to be whitelisted. Some classes are whitelisted by default (see ``DefaultWhitelist``), but others outside of that set need to be whitelisted either by using the annotation ``@CordaSerializable`` or via the plugin framework. See :doc:`serialization`. You can see above that the ``SellerTradeInfo`` has been annotated. @@ -231,26 +199,28 @@ These will return a ``FlowProgressHandle``, which is just like a ``FlowHandle`` Implementing the seller ----------------------- -Let's implement the ``Seller.call`` method. This will be run when the flow is invoked. +Let's implement the ``Seller.call`` method that will be run when the flow is invoked. .. container:: codeset .. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt - :language: kotlin - :start-after: DOCSTART 4 - :end-before: DOCEND 4 - :dedent: 4 + :language: kotlin + :start-after: DOCSTART 4 + :end-before: DOCEND 4 + :dedent: 8 We start by sending information about the asset we wish to sell to the buyer. We fill out the initial flow message with -the trade info, and then call ``send``. which takes two arguments: +the trade info, and then call ``otherSideSession.send``. which takes two arguments: -- The party we wish to send the message to. -- The payload being sent. +- The party we wish to send the message to +- The payload being sent -``send`` will serialise the payload and send it to the other party automatically. +``otherSideSession.send`` will serialise the payload and send it to the other party automatically. -Next, we call a *subflow* called ``SignTransactionFlow`` (see :ref:`subflows`). ``SignTransactionFlow`` automates the -process of: +Next, we call a *subflow* called ``IdentitySyncFlow.Receive`` (see :ref:`subflows`). ``IdentitySyncFlow.Receive`` +ensures that our node can de-anonymise any confidential identities in the transaction it's about to be asked to sign. + +Next, we call another subflow called ``SignTransactionFlow``. ``SignTransactionFlow`` automates the process of: * Receiving a proposed trade transaction from the buyer, with the buyer's signature attached. * Checking that the proposed transaction is valid. @@ -273,7 +243,7 @@ OK, let's do the same for the buyer side: :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 - :dedent: 4 + :dedent: 8 This code is longer but no more complicated. Here are some things to pay attention to: @@ -472,7 +442,7 @@ Exception handling Flows can throw exceptions to prematurely terminate their execution. The flow framework gives special treatment to ``FlowException`` and its subtypes. These exceptions are treated as error responses of the flow and are propagated to all counterparties it is communicating with. The receiving flows will throw the same exception the next time they do -a ``receive`` or ``sendAndReceive`` and thus end the flow session. If the receiver was invoked via ``subFlow`` (details below) +a ``receive`` or ``sendAndReceive`` and thus end the flow session. If the receiver was invoked via ``subFlow`` then the exception can be caught there enabling re-invocation of the sub-flow. If the exception thrown by the erroring flow is not a ``FlowException`` it will still terminate but will not propagate to @@ -505,59 +475,40 @@ A flow might declare some steps with code inside the flow class like this: .. container:: codeset .. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt - :language: kotlin - :start-after: DOCSTART 2 - :end-before: DOCEND 2 - :dedent: 4 + :language: kotlin + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + :dedent: 8 - .. sourcecode:: java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/TutorialFlowStateMachines.java + :language: java + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + :dedent: 4 - private final ProgressTracker progressTracker = new ProgressTracker( - RECEIVING, - VERIFYING, - SIGNING, - COLLECTING_SIGNATURES, - RECORDING - ); +Each step exposes a label. By defining your own step types, you can export progress in a way that's both human readable +and machine readable. - private static final ProgressTracker.Step RECEIVING = new ProgressTracker.Step( - "Waiting for seller trading info"); - private static final ProgressTracker.Step VERIFYING = new ProgressTracker.Step( - "Verifying seller assets"); - private static final ProgressTracker.Step SIGNING = new ProgressTracker.Step( - "Generating and signing transaction proposal"); - private static final ProgressTracker.Step COLLECTING_SIGNATURES = new ProgressTracker.Step( - "Collecting signatures from other parties"); - private static final ProgressTracker.Step RECORDING = new ProgressTracker.Step( - "Recording completed transaction"); - -Each step exposes a label. By default labels are fixed, but by subclassing ``RelabelableStep`` you can make a step -that can update its label on the fly. That's useful for steps that want to expose non-structured progress information -like the current file being downloaded. By defining your own step types, you can export progress in a way that's both -human readable and machine readable. - -Progress trackers are hierarchical. Each step can be the parent for another tracker. By altering the -``ProgressTracker.childrenFor`` map, a tree of steps can be created. It's allowed to alter the hierarchy -at runtime, on the fly, and the progress renderers will adapt to that properly. This can be helpful when you don't -fully know ahead of time what steps will be required. If you *do* know what is required, configuring as much of the -hierarchy ahead of time is a good idea, as that will help the users see what is coming up. You can pre-configure -steps by overriding the ``Step`` class like this: +Progress trackers are hierarchical. Each step can be the parent for another tracker. By setting +``Step.childProgressTracker``, a tree of steps can be created. It's allowed to alter the hierarchy at runtime, on the +fly, and the progress renderers will adapt to that properly. This can be helpful when you don't fully know ahead of +time what steps will be required. If you *do* know what is required, configuring as much of the hierarchy ahead of time +is a good idea, as that will help the users see what is coming up. You can pre-configure steps by overriding the +``Step`` class like this: .. container:: codeset .. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt - :language: kotlin - :start-after: DOCSTART 3 - :end-before: DOCEND 3 - :dedent: 4 + :language: kotlin + :start-after: DOCSTART 3 + :end-before: DOCEND 3 + :dedent: 12 - .. sourcecode:: java - - private static final ProgressTracker.Step VERIFYING_AND_SIGNING = new ProgressTracker.Step("Verifying and signing transaction proposal") { - @Nullable @Override public ProgressTracker childProgressTracker() { - return SignTransactionFlow.Companion.tracker(); - } - }; + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/TutorialFlowStateMachines.java + :language: java + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + :dedent: 4 Every tracker has not only the steps given to it at construction time, but also the singleton ``ProgressTracker.UNSTARTED`` step and the ``ProgressTracker.DONE`` step. Once a tracker has become ``DONE`` its @@ -586,22 +537,6 @@ and linked ahead of time. In future, the progress tracking framework will become a vital part of how exceptions, errors, and other faults are surfaced to human operators for investigation and resolution. -Versioning ----------- - -Fibers involve persisting object-serialised stack frames to disk. Although we may do some R&D into in-place upgrades -in future, for now the upgrade process for flows is simple: you duplicate the code and rename it so it has a -new set of class names. Old versions of the flow can then drain out of the system whilst new versions are -initiated. When enough time has passed that no old versions are still waiting for anything to happen, the previous -copy of the code can be deleted. - -Whilst kind of ugly, this is a very simple approach that should suffice for now. - -.. warning:: Flows are not meant to live for months or years, and by implication they are not meant to implement entire deal - lifecycles. For instance, implementing the entire life cycle of an interest rate swap as a single flow - whilst - technically possible - would not be a good idea. The platform provides a job scheduler tool that can invoke - flows for this reason (see ":doc:`event-scheduling`") - Future features --------------- @@ -611,6 +546,6 @@ the features we have planned: * Exception management, with a "flow hospital" tool to manually provide solutions to unavoidable problems (e.g. the other side doesn't know the trade) * Being able to interact with people, either via some sort of external ticketing system, or email, or a custom UI. - For example to implement human transaction authorisations. + For example to implement human transaction authorisations * A standard library of flows that can be easily sub-classed by local developers in order to integrate internal - reporting logic, or anything else that might be required as part of a communications lifecycle. \ No newline at end of file + reporting logic, or anything else that might be required as part of a communications lifecycle \ No newline at end of file diff --git a/docs/source/flow-testing.rst b/docs/source/flow-testing.rst index 1d53696b61..939da0769e 100644 --- a/docs/source/flow-testing.rst +++ b/docs/source/flow-testing.rst @@ -15,74 +15,56 @@ A good example to examine for learning how to unit test flows is the ``ResolveTr flow takes care of downloading and verifying transaction graphs, with all the needed dependencies. We start with this basic skeleton: -.. container:: codeset - - .. sourcecode:: kotlin - - class ResolveTransactionsFlowTest { - lateinit var mockNet: MockNetwork - lateinit var a: MockNetwork.MockNode - lateinit var b: MockNetwork.MockNode - lateinit var notary: Party - - @Before - fun setup() { - mockNet = MockNetwork() - val nodes = mockNet.createSomeNodes() - a = nodes.partyNodes[0] - b = nodes.partyNodes[1] - mockNet.runNetwork() - notary = a.services.getDefaultNotary() - } - - @After - fun tearDown() { - mockNet.stopNodes() - } - } +.. literalinclude:: ../../core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt + :language: kotlin + :start-after: DOCSTART 3 + :end-before: DOCEND 3 We create a mock network in our ``@Before`` setup method and create a couple of nodes. We also record the identity of the notary in our test network, which will come in handy later. We also tidy up when we're done. Next, we write a test case: -.. literalinclude:: ../../core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt +.. literalinclude:: ../../core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 + :dedent: 4 We'll take a look at the ``makeTransactions`` function in a moment. For now, it's enough to know that it returns two -``SignedTransaction`` objects, the second of which spends the first. Both transactions are known by node A -but not node B. +``SignedTransaction`` objects, the second of which spends the first. Both transactions are known by MegaCorpNode but +not MiniCorpNode. -The test logic is simple enough: we create the flow, giving it node A's identity as the target to talk to. -Then we start it on node B and use the ``mockNet.runNetwork()`` method to bounce messages around until things have +The test logic is simple enough: we create the flow, giving it MegaCorpNode's identity as the target to talk to. +Then we start it on MiniCorpNode and use the ``mockNet.runNetwork()`` method to bounce messages around until things have settled (i.e. there are no more messages waiting to be delivered). All this is done using an in memory message routing implementation that is fast to initialise and use. Finally, we obtain the result of the flow and do -some tests on it. We also check the contents of node B's database to see that the flow had the intended effect +some tests on it. We also check the contents of MiniCorpNode's database to see that the flow had the intended effect on the node's persistent state. Here's what ``makeTransactions`` looks like: -.. literalinclude:: ../../core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt +.. literalinclude:: ../../core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt :language: kotlin :start-after: DOCSTART 2 :end-before: DOCEND 2 + :dedent: 4 We're using the ``DummyContract``, a simple test smart contract which stores a single number in its states, along with ownership and issuer information. You can issue such states, exit them and re-assign ownership (move them). It doesn't do anything else. This code simply creates a transaction that issues a dummy state (the issuer is ``MEGA_CORP``, a pre-defined unit test identity), signs it with the test notary and MegaCorp keys and then converts the builder to the final ``SignedTransaction``. It then does so again, but this time instead of issuing -it re-assigns ownership instead. The chain of two transactions is finally committed to node A by sending them -directly to the ``a.services.recordTransaction`` method (note that this method doesn't check the transactions are -valid) inside a ``database.transaction``. All node flows run within a database transaction in the nodes themselves, -but any time we need to use the database directly from a unit test, you need to provide a database transaction as shown -here. +it re-assigns ownership instead. The chain of two transactions is finally committed to MegaCorpNode by sending them +directly to the ``megaCorpNode.services.recordTransaction`` method (note that this method doesn't check the +transactions are valid) inside a ``database.transaction``. All node flows run within a database transaction in the +nodes themselves, but any time we need to use the database directly from a unit test, you need to provide a database +transaction as shown here. With regards to initiated flows (see :doc:`flow-state-machines` for information on initiated and initiating flows), the full node automatically registers them by scanning the CorDapp jars. In a unit test environment this is not possible so ``MockNode`` has the ``registerInitiatedFlow`` method to manually register an initiated flow. -And that's it: you can explore the documentation for the `MockNetwork API `_ +And that's it: you can explore the documentation for the +`MockNetwork API `_ here. diff --git a/docs/source/getting-set-up.rst b/docs/source/getting-set-up.rst index 04901d29fa..7a9b09b140 100644 --- a/docs/source/getting-set-up.rst +++ b/docs/source/getting-set-up.rst @@ -74,14 +74,14 @@ IntelliJ Download a sample project ^^^^^^^^^^^^^^^^^^^^^^^^^ 1. Open a command prompt -2. Clone the CorDapp tutorial repo by running ``git clone https://github.com/corda/cordapp-tutorial`` -3. Move into the cordapp-tutorial folder by running ``cd cordapp-tutorial`` +2. Clone the CorDapp example repo by running ``git clone https://github.com/corda/cordapp-example`` +3. Move into the cordapp-example folder by running ``cd cordapp-example`` 4. Retrieve a list of all the releases by running ``git branch -a --list`` 5. Check out the latest milestone release by running ``git checkout release-MX`` (where "X" is the latest milestone) Run from the command prompt ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -1. From the cordapp-tutorial folder, deploy the nodes by running ``gradlew deployNodes`` +1. From the cordapp-example folder, deploy the nodes by running ``gradlew deployNodes`` 2. Start the nodes by running ``call kotlin-source/build/nodes/runnodes.bat`` 3. Wait until all the terminal windows display either "Webserver started up in XX.X sec" or "Node for "NodeC" started up and registered in XX.XX sec" 4. Test the CorDapp is running correctly by visiting the front end at http://localhost:10007/web/example/ @@ -89,7 +89,7 @@ Run from the command prompt Run from IntelliJ ^^^^^^^^^^^^^^^^^ 1. Open IntelliJ Community Edition -2. On the splash screen, click "Open" (do NOT click "Import Project") and select the cordapp-tutorial folder +2. On the splash screen, click "Open" (do NOT click "Import Project") and select the cordapp-example folder .. warning:: If you click "Import Project" instead of "Open", the project's run configurations will be erased! @@ -123,14 +123,14 @@ IntelliJ Download a sample project ^^^^^^^^^^^^^^^^^^^^^^^^^ 1. Open a terminal -2. Clone the CorDapp tutorial repo by running ``git clone https://github.com/corda/cordapp-tutorial`` -3. Move into the cordapp-tutorial folder by running ``cd cordapp-tutorial`` +2. Clone the CorDapp example repo by running ``git clone https://github.com/corda/cordapp-example`` +3. Move into the cordapp-example folder by running ``cd cordapp-example`` 4. Retrieve a list of all the releases by running ``git branch -a --list`` 5. Check out the latest milestone release by running ``git checkout release-MX`` (where "X" is the latest milestone) Run from the terminal ^^^^^^^^^^^^^^^^^^^^^ -1. From the cordapp-tutorial folder, deploy the nodes by running ``./gradlew deployNodes`` +1. From the cordapp-example folder, deploy the nodes by running ``./gradlew deployNodes`` 2. Start the nodes by running ``kotlin-source/build/nodes/runnodes``. Do not click while 8 additional terminal windows start up. 3. Wait until all the terminal windows display either "Webserver started up in XX.X sec" or "Node for "NodeC" started up and registered in XX.XX sec" 4. Test the CorDapp is running correctly by visiting the front end at http://localhost:10007/web/example/ @@ -138,7 +138,7 @@ Run from the terminal Run from IntelliJ ^^^^^^^^^^^^^^^^^ 1. Open IntelliJ Community Edition -2. On the splash screen, click "Open" (do NOT click "Import Project") and select the cordapp-tutorial folder +2. On the splash screen, click "Open" (do NOT click "Import Project") and select the cordapp-example folder 3. Once the project is open, click "File > Project Structure". Under "Project SDK:", set the project SDK by clicking "New...", clicking "JDK", and navigating to /Library/Java/JavaVirtualMachines/jdk1.8.0_XXX (where "XXX" is the latest minor version number). Click "OK". 4. Click "View > Tool Windows > Event Log", and click "Import Gradle project", then "OK". Wait, and click "OK" again when the "Gradle Project Data To Import" window appears 5. Wait for indexing to finish (a progress bar will display at the bottom-right of the IntelliJ window until indexing is complete) @@ -161,7 +161,7 @@ A CorDapp template that you can use as the basis for your own CorDapps is availa And a simple example CorDapp for you to explore basic concepts is available here: - https://github.com/corda/cordapp-tutorial.git + https://github.com/corda/cordapp-example.git You can clone these repos to your local machine by running the command ``git clone [repo URL]``. @@ -170,8 +170,8 @@ instead by running ``git checkout release-MX`` (where “X” is the latest mile Next steps ---------- -The best way to check that everything is working fine is by running the :doc:`tutorial CorDapp ` and -the :doc:`samples `. +The best way to check that everything is working fine is by taking a deeper look at the +:doc:`example CorDapp `. Next, you should read through :doc:`Corda Key Concepts ` to understand how Corda works. diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index 2e15aaf74e..6bc74a3117 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -185,7 +185,7 @@ There are a number of improvements we could make to this CorDapp: * We could add an API, to make it easier to interact with the CorDapp We will explore some of these improvements in future tutorials. But you should now be ready to develop your own -CorDapps. There's `a more fleshed-out version of the IOU CorDapp `_ with an +CorDapps. There's `a more fleshed-out version of the IOU CorDapp `_ with an API and web front-end, and a set of example CorDapps in `the main Corda repo `_, under ``samples``. An explanation of how to run these samples :doc:`here `. diff --git a/docs/source/key-concepts-node.rst b/docs/source/key-concepts-node.rst index aa6517249a..537d0aebc4 100644 --- a/docs/source/key-concepts-node.rst +++ b/docs/source/key-concepts-node.rst @@ -55,7 +55,7 @@ node's owner does not interact with other network nodes directly. RPC interface ------------- The node's owner interacts with the node via remote procedure calls (RPC). The key RPC operations the node exposes -are documented in :doc:``api-rpc``. +are documented in :doc:`api-rpc`. The service hub --------------- diff --git a/docs/source/oracles.rst b/docs/source/oracles.rst index 177da9a320..802e9f33c0 100644 --- a/docs/source/oracles.rst +++ b/docs/source/oracles.rst @@ -17,25 +17,25 @@ Introduction to oracles ----------------------- Oracles are a key concept in the block chain/decentralised ledger space. They can be essential for many kinds of -application, because we often wish to condition a transaction on some fact being true or false, but the ledger itself +application, because we often wish to condition the validity of a transaction on some fact being true or false, but the ledger itself has a design that is essentially functional: all transactions are *pure* and *immutable*. Phrased another way, a -smart contract cannot perform any input/output or depend on any state outside of the transaction itself. There is no -way to download a web page or interact with the user, in a smart contract. It must be this way because everyone must -be able to independently check a transaction and arrive at an identical conclusion for the ledger to maintain its +contract cannot perform any input/output or depend on any state outside of the transaction itself. For example, there is no +way to download a web page or interact with the user from within a contract. It must be this way because everyone must +be able to independently check a transaction and arrive at an identical conclusion regarding its validity for the ledger to maintain its integrity: if a transaction could evaluate to "valid" on one computer and then "invalid" a few minutes later on a different computer, the entire shared ledger concept wouldn't work. -But it is often essential that transactions do depend on data from the outside world, for example, verifying that an +But transaction validity does often depend on data from the outside world - verifying that an interest rate swap is paying out correctly may require data on interest rates, verifying that a loan has reached maturity requires knowledge about the current time, knowing which side of a bet receives the payment may require -arbitrary facts about the real world (e.g. the bankruptcy or solvency of a company or country) ... and so on. +arbitrary facts about the real world (e.g. the bankruptcy or solvency of a company or country), and so on. We can solve this problem by introducing services that create digitally signed data structures which assert facts. These structures can then be used as an input to a transaction and distributed with the transaction data itself. Because the statements are themselves immutable and signed, it is impossible for an oracle to change its mind later and invalidate transactions that were previously found to be valid. In contrast, consider what would happen if a contract could do an HTTP request: it's possible that an answer would change after being downloaded, resulting in loss of -consensus (breaks). +consensus. The two basic approaches ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -78,7 +78,7 @@ Asserting continuously varying data Let's look at the interest rates oracle that can be found in the ``NodeInterestRates`` file. This is an example of an oracle that uses a command because the current interest rate fix is a constantly changing fact. -The obvious way to implement such a service is like this: +The obvious way to implement such a service is this: 1. The creator of the transaction that depends on the interest rate sends it to the oracle. 2. The oracle inserts a command with the rate and signs the transaction. @@ -101,40 +101,38 @@ class that binds it to the network layer. Here is an extract from the ``NodeInterestRates.Oracle`` class and supporting types: +.. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + +.. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt + :language: kotlin + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + .. sourcecode:: kotlin - /** A [FixOf] identifies the question side of a fix: what day, tenor and type of fix ("LIBOR", "EURIBOR" etc) */ - data class FixOf(val name: String, val forDay: LocalDate, val ofTenor: Tenor) - - /** A [Fix] represents a named interest rate, on a given day, for a given duration. It can be embedded in a tx. */ - data class Fix(val of: FixOf, val value: BigDecimal) : CommandData - class Oracle { - fun query(queries: List, deadline: Instant): List + fun query(queries: List): List - fun sign(ftx: FilteredTransaction, txId: SecureHash): TransactionSignature + fun sign(ftx: FilteredTransaction): TransactionSignature } -Because the fix contains a timestamp (the ``forDay`` field), that identifies the version of the data being requested, -there can be an arbitrary delay between a fix being requested via ``query`` and the signature being requested via ``sign`` -as the Oracle can know which, potentially historical, value it is being asked to sign for. This is an important -technique for continuously varying data. - -The ``query`` method takes a deadline, which is a point in time the requester is willing to wait until for the necessary -data to be available. Not every oracle will need this. This can be useful where data is expected to be available on a -particular schedule and we use scheduling functionality to automatically launch the processing associated with it. -We can schedule for the expected announcement (or publish) time and give a suitable deadline at which the lack of the -information being available and the delay to processing becomes significant and may need to be escalated. +The fix contains a timestamp (the ``forDay`` field) that identifies the version of the data being requested. Since +there can be an arbitrary delay between a fix being requested via ``query`` and the signature being requested via +``sign``, this timestamp allows the Oracle to know which, potentially historical, value it is being asked to sign for. This is an +important technique for continuously varying data. Hiding transaction data from the oracle ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Because the transaction is sent to the oracle for signing, ordinarily the oracle would be able to see the entire contents of that transaction including the inputs, output contract states and all the commands, not just the one (in this case) -relevant command. This is an obvious privacy leak for the other participants. We currently solve this with -``FilteredTransaction``-s and the use of Merkle Trees. These reveal only the necessary parts of the transaction to the -oracle but still allow it to sign it by providing the Merkle hashes for the remaining parts. See :doc:`merkle-trees` for -more details. +relevant command. This is an obvious privacy leak for the other participants. We currently solve this using a +``FilteredTransaction``, which implements a Merkle Tree. These reveal only the necessary parts of the transaction to the +oracle but still allow it to sign it by providing the Merkle hashes for the remaining parts. See :doc:`key-concepts-oracles` +for more details. Pay-per-play oracles ~~~~~~~~~~~~~~~~~~~~ @@ -143,7 +141,7 @@ Because the signature covers the transaction, and transactions may end up being is independently checkable. However, this approach can still be useful when the data itself costs money, because the act of issuing the signature in the first place can be charged for (e.g. by requiring the submission of a fresh ``Cash.State`` that has been re-assigned to a key owned by the oracle service). Because the signature covers the -*transaction* and not only the *fact*, this allows for a kind of weak pseudo-DRM over data feeds. Whilst a smart +*transaction* and not only the *fact*, this allows for a kind of weak pseudo-DRM over data feeds. Whilst a contract could in theory include a transaction parsing and signature checking library, writing a contract in this way would be conclusive evidence of intent to disobey the rules of the service (*res ipsa loquitur*). In an environment where parties are legally identifiable, usage of such a contract would by itself be sufficient to trigger some sort of @@ -156,24 +154,12 @@ Implement the core classes ~~~~~~~~~~~~~~~~~~~~~~~~~~ The key is to implement your oracle in a similar way to the ``NodeInterestRates.Oracle`` outline we gave above with -both ``query`` and ``sign`` methods. Typically you would want one class that encapsulates the parameters to the ``query`` -method (``FixOf`` above), and a ``CommandData`` implementation (``Fix`` above) that encapsulates both an instance of +both a ``query`` and a ``sign`` method. Typically you would want one class that encapsulates the parameters to the ``query`` +method (``FixOf``, above), and a ``CommandData`` implementation (``Fix``, above) that encapsulates both an instance of that parameter class and an instance of whatever the result of the ``query`` is (``BigDecimal`` above). -The ``NodeInterestRates.Oracle`` allows querying for multiple ``Fix``-es but that is not necessary and is -provided for the convenience of callers who might need multiple and can do it all in one query request. Likewise -the *deadline* functionality is optional and can be avoided initially. - -Let's see what parameters we pass to the constructor of this oracle. - -.. sourcecode:: kotlin - - class Oracle(val identity: Party, private val signingKey: PublicKey, val clock: Clock) = TODO() - -Here we see the oracle needs to have its own identity, so it can check which transaction commands it is expected to -sign for, and also needs the PublicKey portion of its signing key. Later this PublicKey will be passed to the KeyManagementService -to identify the internal PrivateKey used for transaction signing. -The clock is used for the deadline functionality which we will not discuss further here. +The ``NodeInterestRates.Oracle`` allows querying for multiple ``Fix`` objects but that is not necessary and is +provided for the convenience of callers who need multiple fixes and want to be able to do it all in one query request. Assuming you have a data source and can query it, it should be very easy to implement your ``query`` method and the parameter and ``CommandData`` classes. @@ -184,16 +170,17 @@ Let's see how the ``sign`` method for ``NodeInterestRates.Oracle`` is written: :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 + :dedent: 8 Here we can see that there are several steps: 1. Ensure that the transaction we have been sent is indeed valid and passes verification, even though we cannot see all - of it. + of it 2. Check that we only received commands as expected, and each of those commands expects us to sign for them and is of - the expected type (``Fix`` here). + the expected type (``Fix`` here) 3. Iterate over each of the commands we identified in the last step and check that the data they represent matches exactly our data source. The final step, assuming we have got this far, is to generate a signature for the - transaction and return it. + transaction and return it Binding to the network ~~~~~~~~~~~~~~~~~~~~~~ @@ -209,6 +196,7 @@ done: :language: kotlin :start-after: DOCSTART 3 :end-before: DOCEND 3 + :dedent: 4 The Corda node scans for any class with this annotation and initialises them. The only requirement is that the class provide a constructor with a single parameter of type ``ServiceHub``. @@ -217,9 +205,10 @@ a constructor with a single parameter of type ``ServiceHub``. :language: kotlin :start-after: DOCSTART 2 :end-before: DOCEND 2 + :dedent: 4 These two flows leverage the oracle to provide the querying and signing operations. They get reference to the oracle, -which will have already been initialised by the node, using ``ServiceHub.cordappProvider``. Both flows are annotated with +which will have already been initialised by the node, using ``ServiceHub.cordaService``. Both flows are annotated with ``@InitiatedBy``. This tells the node which initiating flow (which are discussed in the next section) they are meant to be executed with. @@ -227,17 +216,18 @@ Providing sub-flows for querying and signing ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We mentioned the client sub-flow briefly above. They are the mechanism that clients, in the form of other flows, will -interact with your oracle. Typically there will be one for querying and one for signing. Let's take a look at +use to interact with your oracle. Typically there will be one for querying and one for signing. Let's take a look at those for ``NodeInterestRates.Oracle``. .. literalinclude:: ../../samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 + :dedent: 4 You'll note that the ``FixSignFlow`` requires a ``FilterTransaction`` instance which includes only ``Fix`` commands. -You can find a further explanation of this in :doc:`merkle-trees`. Below you will see how to build such transaction with -hidden fields. +You can find a further explanation of this in :doc:`key-concepts-oracles`. Below you will see how to build such a +transaction with hidden fields. .. _filtering_ref: @@ -252,16 +242,16 @@ called ``RatesFixFlow``. Here's the ``call`` method of that flow. :language: kotlin :start-after: DOCSTART 2 :end-before: DOCEND 2 + :dedent: 4 As you can see, this: -1. Queries the oracle for the fact using the client sub-flow for querying from above. -2. Does some quick validation. -3. Adds the command to the transaction containing the fact to be signed for by the oracle. -4. Calls an extension point that allows clients to generate output states based on the fact from the oracle. -5. Builds filtered transaction based on filtering function extended from ``RatesFixFlow``. -6. Requests the signature from the oracle using the client sub-flow for signing from above. -7. Adds the signature returned from the oracle. +1. Queries the oracle for the fact using the client sub-flow for querying defined above +2. Does some quick validation +3. Adds the command to the transaction containing the fact to be signed for by the oracle +4. Calls an extension point that allows clients to generate output states based on the fact from the oracle +5. Builds filtered transaction based on filtering function extended from ``RatesFixFlow`` +6. Requests the signature from the oracle using the client sub-flow for signing from above Here's an example of it in action from ``FixingFlow.Fixer``. @@ -269,6 +259,7 @@ Here's an example of it in action from ``FixingFlow.Fixer``. :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 + :dedent: 4 .. note:: When overriding be careful when making the sub-class an anonymous or inner class (object declarations in Kotlin), @@ -279,6 +270,6 @@ Testing ------- When unit testing, we make use of the ``MockNetwork`` which allows us to create ``MockNode`` instances. A ``MockNode`` -is a simplified node suitable for tests. One feature that isn't available (and which is not suitable in unit testing +is a simplified node suitable for tests. One feature that isn't available (and which is not suitable for unit testing anyway) is the node's ability to scan and automatically install oracles it finds in the CorDapp jars. Instead, when working with ``MockNode``, use the ``installCordaService`` method to manually install the oracle on the relevant node. \ No newline at end of file diff --git a/docs/source/running-a-notary.rst b/docs/source/running-a-notary.rst index 81ade65043..32b62359a8 100644 --- a/docs/source/running-a-notary.rst +++ b/docs/source/running-a-notary.rst @@ -6,9 +6,10 @@ At present we have several prototype notary implementations: 1. ``SimpleNotaryService`` (single node) -- commits the provided transaction input states without any validation. 2. ``ValidatingNotaryService`` (single node) -- retrieves and validates the whole transaction history (including the given transaction) before committing. -3. ``RaftValidatingNotaryService`` (distributed) -- functionally equivalent to ``ValidatingNotaryService``, but stores +3. ``RaftNonValidatingNotaryService`` (distributed) -- functionally equivalent to ``SimpleNotaryService``, but stores the committed states in a distributed collection replicated and persisted in a Raft cluster. For the consensus layer - we are using the `Copycat `_ framework. + we are using the `Copycat `_ framework +4. ``RaftValidatingNotaryService`` (distributed) -- as above, but performs validation on the transactions received To have a node run a notary service, you need to set appropriate configuration values before starting it (see :doc:`corda-configuration-file` for reference). @@ -25,5 +26,5 @@ For ``ValidatingNotaryService``, it is: extraAdvertisedServiceIds : [ "net.corda.notary.validating" ] -Setting up a ``RaftValidatingNotaryService`` is currently slightly more involved and is not recommended for prototyping -purposes. There is work in progress to simplify it. To see it in action, however, you can try out the :ref:`notary-demo`. +Setting up a Raft notary is currently slightly more involved and is not recommended for prototyping purposes. There is +work in progress to simplify it. To see it in action, however, you can try out the :ref:`notary-demo`. diff --git a/docs/source/tutorial-attachments.rst b/docs/source/tutorial-attachments.rst index 09a2736b38..7047cfb959 100644 --- a/docs/source/tutorial-attachments.rst +++ b/docs/source/tutorial-attachments.rst @@ -68,57 +68,22 @@ RPC, which returns both a snapshot and an observable of changes. The observable transaction the node verifies is retrieved. That transaction is checked to see if it has the expected attachment and if so, printed out. -.. sourcecode:: kotlin +.. container:: codeset - fun recipient(rpc: CordaRPCOps) { - println("Waiting to receive transaction ...") - val stx = rpc.verifiedTransactions().second.toBlocking().first() - val wtx = stx.tx - if (wtx.attachments.isNotEmpty()) { - assertEquals(PROSPECTUS_HASH, wtx.attachments.first()) - require(rpc.attachmentExists(PROSPECTUS_HASH)) - println("File received - we're happy!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(wtx)}") - } else { - println("Error: no attachments found in ${wtx.id}") - } - } + .. literalinclude:: ../../samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 The sender correspondingly builds a transaction with the attachment, then calls ``FinalityFlow`` to complete the transaction and send it to the recipient node: -.. sourcecode:: kotlin - - fun sender(rpc: CordaRPCOps) { - // Get the identity key of the other side (the recipient). - val otherSide: Party = rpc.wellKnownPartyFromName("Bank B")!! - - // Make sure we have the file in storage - // TODO: We should have our own demo file, not share the trader demo file - if (!rpc.attachmentExists(PROSPECTUS_HASH)) { - Thread.currentThread().contextClassLoader.getResourceAsStream("bank-of-london-cp.jar").use { - val id = rpc.uploadAttachment(it) - assertEquals(PROSPECTUS_HASH, id) - } - } - - // Create a trivial transaction that just passes across the attachment - in normal cases there would be - // inputs, outputs and commands that refer to this attachment. - val ptx = TransactionBuilder(notary = null) - require(rpc.attachmentExists(PROSPECTUS_HASH)) - ptx.addAttachment(PROSPECTUS_HASH) - // TODO: Add a dummy state and specify a notary, so that the tx hash is randomised each time and the demo can be repeated. - - // Despite not having any states, we have to have at least one signature on the transaction - ptx.signWith(ALICE_KEY) - - // Send the transaction to the other recipient - val stx = ptx.toSignedTransaction() - println("Sending ${stx.id}") - val protocolHandle = rpc.startFlow(::FinalityFlow, stx, setOf(otherSide)) - protocolHandle.progress.subscribe(::println) - protocolHandle.returnValue.toBlocking().first() - } +.. container:: codeset + .. literalinclude:: ../../samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt + :language: kotlin + :start-after: DOCSTART 2 + :end-before: DOCEND 2 This side is a bit more complex. Firstly it looks up its counterparty by name in the network map. Then, if the node doesn't already have the attachment in its storage, we upload it from a JAR resource and check the hash was what diff --git a/docs/source/tutorial-building-transactions.rst b/docs/source/tutorial-building-transactions.rst index 3a7586dbb9..d7011e57f4 100644 --- a/docs/source/tutorial-building-transactions.rst +++ b/docs/source/tutorial-building-transactions.rst @@ -18,67 +18,54 @@ of a flow. The Basic Lifecycle Of Transactions ----------------------------------- -Transactions in Corda are constructed in stages and contain a number of -elements. In particular a transaction’s core data structure is the -``net.corda.core.transactions.WireTransaction``, which is usually -manipulated via a -``net.corda.core.transactions.TransactionBuilder`` and contains: +Transactions in Corda contain a number of elements: 1. A set of Input state references that will be consumed by the final -accepted transaction. + accepted transaction 2. A set of Output states to create/replace the consumed states and thus -become the new latest versions of data on the ledger. + become the new latest versions of data on the ledger 3. A set of ``Attachment`` items which can contain legal documents, contract -code, or private encrypted sections as an extension beyond the native -contract states. + code, or private encrypted sections as an extension beyond the native + contract states -4. A set of ``Command`` items which give a context to the type of ledger -transition that is encoded in the transaction. Also each command has an -associated set of signer keys, which will be required to sign the -transaction. +4. A set of ``Command`` items which indicate the type of ledger + transition that is encoded in the transaction. Each command also has an + associated set of signer keys, which will be required to sign the + transaction -5. A signers list, which is populated by the ``TransactionBuilder`` to -be the union of the signers on the individual Command objects. +5. A signers list, which is the union of the signers on the individual + Command objects -6. A notary identity to specify the Notary node which is tracking the -state consumption. (If the input states are registered with different -notary nodes the flow will have to insert additional ``NotaryChange`` -transactions to migrate the states across to a consistent notary node, -before being allowed to mutate any states.) +6. A notary identity to specify which notary node is tracking the + state consumption (if the transaction's input states are registered with different + notary nodes the flow will have to insert additional ``NotaryChange`` + transactions to migrate the states across to a consistent notary node + before being allowed to mutate any states) -7. Optionally a timestamp that can used in the Notary to time bound the -period in which the proposed transaction stays valid. +7. Optionally a timestamp that can used by the notary to bound the + period during which the proposed transaction can be committed to the + ledger -Typically, the ``WireTransaction`` should be regarded as a proposal and -may need to be exchanged back and forth between parties before it can be -fully populated. This is an immediate consequence of the Corda privacy -model, which means that the input states are likely to be unknown to the -other node. +A transaction is built by populating a ``TransactionBuilder``. Typically, +the ``TransactionBuilder`` will need to be exchanged back and forth between +parties before it is fully populated. This is an immediate consequence of +the Corda privacy model, in which the input states are likely to be unknown +to the other node. -Once the proposed data is fully populated the flow code should freeze -the ``WireTransaction`` and form a ``SignedTransaction``. This is key to -the ledger agreement process, as once a flow has attached a node’s -signature it has stated that all details of the transaction are -acceptable to it. A flow should take care not to attach signatures to -intermediate data, which might be maliciously used to construct a -different ``SignedTransaction``. For instance in a foreign exchange -scenario we shouldn't send a ``SignedTransaction`` with only our sell -side populated as that could be used to take the money without the -expected return of the other currency. Also, it is best practice for -flows to receive back the ``TransactionSignature`` of other parties -rather than a full ``SignedTransaction`` objects, because otherwise we -have to separately check that this is still the same +Once the builder is fully populated, the flow should freeze the ``TransactionBuilder`` by signing it to create a +``SignedTransaction``. This is key to the ledger agreement process - once a flow has attached a node’s signature to a +transaction, it has effectively stated that it accepts all the details of the transaction. + +It is best practice for flows to receive back the ``TransactionSignature`` of other parties rather than a full +``SignedTransaction`` objects, because otherwise we have to separately check that this is still the same ``SignedTransaction`` and not a malicious substitute. -The final stage of committing the transaction to the ledger is to -notarise the ``SignedTransaction``, distribute this to all appropriate -parties and record the data into the ledger. These actions are best -delegated to the ``FinalityFlow``, rather than calling the individual -steps manually. However, do note that the final broadcast to the other -nodes is asynchronous, so care must be used in unit testing to -correctly await the Vault updates. +The final stage of committing the transaction to the ledger is to notarise the ``SignedTransaction``, distribute it to +all appropriate parties and record the data into the ledger. These actions are best delegated to the ``FinalityFlow``, +rather than calling the individual steps manually. However, do note that the final broadcast to the other nodes is +asynchronous, so care must be used in unit testing to correctly await the vault updates. Gathering Inputs ---------------- @@ -87,39 +74,36 @@ One of the first steps to forming a transaction is gathering the set of input references. This process will clearly vary according to the nature of the business process being captured by the smart contract and the parameterised details of the request. However, it will generally involve -searching the Vault via the ``VaultService`` interface on the +searching the vault via the ``VaultService`` interface on the ``ServiceHub`` to locate the input states. To give a few more specific details consider two simplified real world -scenarios. First, a basic foreign exchange Cash transaction. This +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``. -Second, a simple business model in which parties manually accept, or -reject each other's trade proposals which is implemented in +Second, a simple business model in which parties manually accept or +reject each other's trade proposals, which is implemented in ``WorkflowTransactionBuildTutorial.kt``. To run and explore these -examples using the IntelliJ IDE one can run/step the respective unit +examples using the IntelliJ IDE one can run/step through the respective unit tests in ``FxTransactionBuildTutorialTest.kt`` and ``WorkflowTransactionBuildTutorialTest.kt``, which drive the flows as part of a simulated in-memory network of nodes. -.. |nbsp| unicode:: 0xA0 - :trim: - .. note:: Before creating the IntelliJ run configurations for these unit tests go to Run -> Edit |nbsp| Configurations -> Defaults -> JUnit, add - ``-javaagent:lib/quasar.jar -Dco.paralleluniverse.fibers.verifyInstrumentation`` + ``-javaagent:lib/quasar.jar`` to the VM options, and set Working directory to ``$PROJECT_DIR$`` so that the ``Quasar`` instrumentation is correctly configured. -For the Cash transaction let’s assume the cash resources are using the -standard ``CashState`` in the ``:financial`` Gradle module. The Cash +For the cash transaction, let’s assume we are using the +standard ``CashState`` in the ``:financial`` Gradle module. The ``Cash`` contract uses ``FungibleAsset`` states to model holdings of -interchangeable assets and allow the split/merge and summing of +interchangeable assets and allow the splitting, merging and summing of states to meet a contractual obligation. We would normally use the ``Cash.generateSpend`` method to gather the required -amount of cash into a ``TransactionBuilder``, set the outputs and move -command. However, to elucidate more clearly example flow code is shown -here that will manually carry out the inputs queries by specifying relevant +amount of cash into a ``TransactionBuilder``, set the outputs and generate the ``Move`` +command. However, to make things clearer, the example flow code shown +here will manually carry out the input queries by specifying relevant query criteria filters to the ``tryLockFungibleStatesForSpending`` method of the ``VaultService``. @@ -128,14 +112,11 @@ of the ``VaultService``. :start-after: DOCSTART 1 :end-before: DOCEND 1 -As a foreign exchange transaction we expect an exchange of two -currencies, so we will also require a set of input states from the other -counterparty. However, the Corda privacy model means we do not know the -other node’s states. Our flow must therefore negotiate with the other -node for them to carry out a similar query and populate the inputs (See -the ``ForeignExchangeFlow`` for more details of the exchange). Having -identified a set of Input ``StateRef`` items we can then create the -output as discussed below. +This is a foreign exchange transaction, so we expect another set of input states of another currency from a +counterparty. However, the Corda privacy model means we are not aware of the other node’s states. Our flow must +therefore ask the other node to carry out a similar query and return the additional inputs to the transaction (see the +``ForeignExchangeFlow`` for more details of the exchange). We now have all the required input ``StateRef`` items, and +can turn to gathering the outputs. For the trade approval flow we need to implement a simple workflow pattern. We start by recording the unconfirmed trade details in a state @@ -167,6 +148,7 @@ the ``VaultService`` as follows: :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 + :dedent: 8 Generating Commands ------------------- @@ -187,7 +169,7 @@ environment the ``Contract.verify``, transaction is the only allowed to use the content of the transaction to decide validity. Another essential requirement for commands is that the correct set of -``CompositeKeys`` are added to the Command on the builder, which will be +``PublicKey`` objects are added to the ``Command`` on the builder, which will be used to form the set of required signers on the final validated transaction. These must correctly align with the expectations of the ``Contract.verify`` method, which should be written to defensively check @@ -200,7 +182,7 @@ exchange of assets. Generating Outputs ------------------ -Having located a set of ``StateAndRefs`` as the transaction inputs, the +Having located a ``StateAndRefs`` set as the transaction inputs, the flow has to generate the output states. Typically, this is a simple call to the Kotlin ``copy`` method to modify the few fields that will transitioned in the transaction. The contract code may provide a @@ -210,7 +192,7 @@ usually sufficient, especially as it is expected that we wish to preserve the ``linearId`` between state revisions, so that Vault queries can find the latest revision. -For fungible contract states such as ``Cash`` it is common to distribute +For fungible contract states such as ``cash`` it is common to distribute and split the total amount e.g. to produce a remaining balance output state for the original owner when breaking up a large amount input state. Remember that the result of a successful transaction is always to @@ -221,36 +203,39 @@ the total cash. For example from the demo code: :language: kotlin :start-after: DOCSTART 2 :end-before: DOCEND 2 + :dedent: 4 -Building the WireTransaction ----------------------------- +Building the SignedTransaction +------------------------------ -Having gathered all the ingredients for the transaction we now need to -use a ``TransactionBuilder`` to construct the full ``WireTransaction``. -The initial ``TransactionBuilder`` should be created by calling the -``TransactionBuilder`` method. At this point the -Notary to associate with the states should be recorded. Then we keep -adding inputs, outputs, commands and attachments to fill the -transaction. Examples of this process are: +Having gathered all the components for the transaction we now need to use a ``TransactionBuilder`` to construct the +full ``SignedTransaction``. We instantiate a ``TransactionBuilder`` and provide a notary that will be associated with +the output states. Then we keep adding inputs, outputs, commands and attachments to complete the transaction. + +Once the transaction is fully formed, we call ``ServiceHub.signInitialTransaction`` to sign the ``TransactionBuilder`` +and convert it into a ``SignedTransaction``. + +Examples of this process are: .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/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 :language: kotlin :start-after: DOCSTART 3 :end-before: DOCEND 3 + :dedent: 4 Completing the SignedTransaction -------------------------------- -Having created an initial ``WireTransaction`` and converted this to an -initial ``SignedTransaction`` the process of verifying and forming a -full ``SignedTransaction`` begins and then completes with the +Having created an initial ``TransactionBuilder`` and converted this to a ``SignedTransaction``, the process of +verifying and forming a full ``SignedTransaction`` begins and then completes with the notarisation. In practice this is a relatively stereotypical process, -because assuming the ``WireTransaction`` is correctly constructed the +because assuming the ``SignedTransaction`` is correctly constructed the verification should be immediate. However, it is also important to recheck the business details of any data received back from an external node, because a malicious party could always modify the contents before @@ -263,12 +248,11 @@ of the transaction has not been altered by the remote parties. The typical code therefore checks the received ``SignedTransaction`` using the ``verifySignaturesExcept`` method, excluding itself, the -notary and any other parties yet to apply their signature. The contents of the -``WireTransaction`` inside the ``SignedTransaction`` should be fully +notary and any other parties yet to apply their signature. The contents of the ``SignedTransaction`` should be fully verified further by expanding with ``toLedgerTransaction`` and calling ``verify``. Further context specific and business checks should then be made, because the ``Contract.verify`` is not allowed to access external -context. For example the flow may need to check that the parties are the +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: @@ -276,6 +260,7 @@ expected for this specific flow. An example of this from the demo code is: :language: kotlin :start-after: DOCSTART 3 :end-before: DOCEND 3 + :dedent: 8 After verification the remote flow will return its signature to the originator. The originator should apply that signature to the starting @@ -284,18 +269,15 @@ originator. The originator should apply that signature to the starting Committing the Transaction -------------------------- -Once all the party signatures are applied to the SignedTransaction the -final step is notarisation. This involves calling ``NotaryFlow.Client`` -to confirm the transaction, consume the inputs and return its confirming -signature. Then the flow should ensure that all nodes end with all -signatures and that they call ``ServiceHub.recordTransactions``. The -code for this is standardised in the ``FinalityFlow``, or more explicitly -an example is: +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 :language: kotlin :start-after: DOCSTART 4 :end-before: DOCEND 4 + :dedent: 8 Partially Visible Transactions ------------------------------ @@ -307,12 +289,12 @@ a regulator, but does not wish to share that with the other trading partner. The tear-off/Merkle tree support in Corda allows flows to send portions of the full transaction to restrict visibility to remote parties. To do this one can use the -``WireTransaction.buildFilteredTransaction`` extension method to produce +``SignedTransaction.buildFilteredTransaction`` extension method to produce a ``FilteredTransaction``. The elements of the ``SignedTransaction`` which we wish to be hide will be replaced with their secure hash. The -overall transaction txid is still provable from the +overall transaction id is still provable from the ``FilteredTransaction`` preventing change of the private data, but we do not expose that data to the other node directly. A full example of this can be found in the ``NodeInterestRates`` Oracle code from the ``irs-demo`` project which interacts with the ``RatesFixFlow`` flow. -Also, refer to the :doc:`merkle-trees` documentation. +Also, refer to the :doc:`merkle-trees` documentation. \ No newline at end of file diff --git a/docs/source/tutorial-clientrpc-api.rst b/docs/source/tutorial-clientrpc-api.rst index 0181e0778b..aa49889b4f 100644 --- a/docs/source/tutorial-clientrpc-api.rst +++ b/docs/source/tutorial-clientrpc-api.rst @@ -3,58 +3,58 @@ Using the client RPC API ======================== -In this tutorial we will build a simple command line utility that connects to a node, creates some Cash transactions and -meanwhile dumps the transaction graph to the standard output. We will then put some simple visualisation on top. For an -explanation on how the RPC works see :doc:`clientrpc`. +In this tutorial we will build a simple command line utility that connects to a node, creates some cash transactions +and dumps the transaction graph to the standard output. We will then put some simple visualisation on top. For an +explanation on how RPC works in Corda see :doc:`clientrpc`. We start off by connecting to the node itself. For the purposes of the tutorial we will use the Driver to start up a notary -and a node that issues/exits and moves Cash around for herself. To authenticate we will use the certificates of the nodes -directly. +and a Alice node that can issue, move and exit cash. -Note how we configure the node to create a user that has permission to start the CashFlow. +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 :language: kotlin :start-after: START 1 :end-before: END 1 -Now we can connect to the node itself using a valid RPC login. We login using the configured user. +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 :language: kotlin :start-after: START 2 :end-before: END 2 + :dedent: 8 -We start generating transactions in a different thread (``generateTransactions`` to be defined later) using ``proxy``, -which exposes the full RPC interface of the node: +``proxy`` exposes the full RPC interface of the node: .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt :language: kotlin :start-after: interface CordaRPCOps :end-before: } -.. warning:: This API is evolving and will continue to grow as new functionality and features added to Corda are made - available to RPC clients. - -The one we need in order to dump the transaction graph is ``verifiedTransactions``. The type signature tells us that the -RPC will return a list of transactions and an Observable stream. This is a 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` +The RPC operation we need in order to dump the transaction graph is ``internalVerifiedTransactionsFeed``. The type +signature tells us that the RPC operation will return a list of transactions and an ``Observable`` stream. This is a +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 :language: kotlin :start-after: START 3 :end-before: END 3 + :dedent: 8 -The graph will be defined by nodes and edges between them. Each node represents a transaction and edges represent -output-input relations. For now let's just print ``NODE `` for the former and ``EDGE `` for the -latter. +The graph will be defined as follows: + +* Each transaction is a vertex, represented by printing ``NODE `` +* Each input-output relationship is an edge, represented by prining ``EDGE `` .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt :language: kotlin :start-after: START 4 :end-before: END 4 - + :dedent: 8 Now we just need to create the transactions themselves! @@ -65,17 +65,16 @@ Now we just need to create the transactions themselves! We utilise several RPC functions here to query things like the notaries in the node cluster or our own vault. These RPC functions also return ``Observable`` objects so that the node can send us updated values. However, we don't need updates -here and so we mark these observables as ``notUsed``. (As a rule, you should always either subscribe to an ``Observable`` -or mark it as not used. Failing to do this will leak resources in the node.) +here and so we mark these observables as ``notUsed`` (as a rule, you should always either subscribe to an ``Observable`` +or mark it as not used. Failing to do so will leak resources in the node). Then in a loop we generate randomly either an Issue, a Pay or an Exit transaction. -The RPC we need to initiate a Cash transaction is ``startFlowDynamic`` which may start an arbitrary flow, given sufficient -permissions to do so. We won't use this function directly, but rather a type-safe wrapper around it ``startFlow`` that -type-checks the arguments for us. +The RPC we need to initiate a cash transaction is ``startFlow`` which starts an arbitrary flow given sufficient +permissions to do so. Finally we have everything in place: we start a couple of nodes, connect to them, and start creating transactions while -listening on successfully created ones, which are dumped to the console. We just need to run it!: +listening on successfully created ones, which are dumped to the console. We just need to run it! .. code-block:: text @@ -84,12 +83,13 @@ listening on successfully created ones, which are dumped to the console. We just # Start it ./docs/source/example-code/build/install/docs/source/example-code/bin/client-rpc-tutorial Print -Now let's try to visualise the transaction graph. We will use a graph drawing library called graphstream_ +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 :language: kotlin :start-after: START 5 :end-before: END 5 + :dedent: 8 If we run the client with ``Visualise`` we should see a simple random graph being drawn as new transactions are being created. @@ -106,11 +106,9 @@ requests or responses with the Corda node. Here's an example of both ways you c See more on plugins in :doc:`running-a-node`. -.. warning:: We will be replacing the use of Kryo in the serialization framework and so additional changes here are likely. - Security -------- -RPC credentials associated with a Client must match the permission set configured on the server Node. +RPC credentials associated with a Client must match the permission set configured on the server node. This refers to both authentication (username and password) and role-based authorisation (a permissioned set of RPC operations an authenticated user is entitled to run). @@ -161,5 +159,5 @@ You can then deploy and launch the nodes (Notary and Alice) as follows: With regards to the start flow RPCs, there is an extra layer of security whereby the flow to be executed has to be annotated with ``@StartableByRPC``. Flows without this annotation cannot execute using RPC. -See more on security in :doc:`secure-coding-guidelines`, node configuration in :doc:`corda-configuration-file` and -Cordformation in :doc:`running-a-node`. +See more on security in :doc:`secure-coding-guidelines`, node configuration in :doc:`corda-configuration-file` and +Cordformation in :doc:`running-a-node`. \ No newline at end of file diff --git a/docs/source/tutorial-contract.rst b/docs/source/tutorial-contract.rst index 2aca2177f7..11e6ef0755 100644 --- a/docs/source/tutorial-contract.rst +++ b/docs/source/tutorial-contract.rst @@ -32,26 +32,16 @@ This lifecycle for commercial paper is illustrated in the diagram below: .. image:: resources/contract-cp.png -Where to put your code ----------------------- - -A CorDapp is a collection of contracts, state definitions, flows and other ways to extend the Corda platform. -To create one you would typically clone the CorDapp template project ("cordapp-template"), which provides an example -structure for the code. Alternatively you can just create a Java-style project as normal, with your choice of build -system (Maven, Gradle, etc), then add a dependency on ``net.corda.core:0.X`` where X is the milestone number you are -depending on. The core module defines the base classes used in this tutorial. - Starting the commercial paper class ----------------------------------- A smart contract is a class that implements the ``Contract`` interface. This can be either implemented directly, as done here, or by subclassing an abstract contract such as ``OnLedgerAsset``. The heart of any contract in Corda is the -``verify()`` function, which determined whether any given transaction is valid. This example shows how to write a -``verify()`` function from scratch. +``verify`` function, which determines whether a given transaction is valid. This example shows how to write a +``verify`` function from scratch. -You can see the full Kotlin version of this contract in the code as ``CommercialPaperLegacy``. The code in this -tutorial is available in both Kotlin and Java. You can quickly switch between them to get a feeling for how -Kotlin syntax works. +The code in this tutorial is available in both Kotlin and Java. You can quickly switch between them to get a feeling +for Kotlin's syntax. .. container:: codeset @@ -72,13 +62,8 @@ Kotlin syntax works. } } -Every contract must have at least a ``verify()`` method. - -.. note:: In the future there will be a way to bind legal contract prose to a smart contract implementation, - that may take precedence over the code in case of a dispute. - -The verify method returns nothing. This is intentional: the function either completes correctly, or throws an exception, -in which case the transaction is rejected. +Every contract must have at least a ``verify`` method. The verify method returns nothing. This is intentional: the +function either completes correctly, or throws an exception, in which case the transaction is rejected. So far, so simple. Now we need to define the commercial paper *state*, which represents the fact of ownership of a piece of issued paper. @@ -90,111 +75,21 @@ A state is a class that stores data that is checked by the contract. A commercia .. image:: resources/contract-cp-state.png - .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 - data class State( - val issuance: PartyAndReference, - override val owner: AbstractParty, - val faceValue: Amount>, - val maturityDate: Instant - ) : OwnableState { - override val contract = "net.corda.finance.contracts.CommercialPaper" - override val participants = listOf(owner) - - fun withoutOwner() = copy(owner = AnonymousParty(NullPublicKey)) - override fun withNewOwner(newOwner: AbstractParty) = Pair(Commands.Move(), copy(owner = newOwner)) - } - - .. sourcecode:: java - - public static class State implements OwnableState { - private PartyAndReference issuance; - private AbstractParty owner; - private Amount> faceValue; - private Instant maturityDate; - - public State() { - } // For serialization - - public State(PartyAndReference issuance, PublicKey owner, Amount> faceValue, - Instant maturityDate) { - this.issuance = issuance; - this.owner = owner; - this.faceValue = faceValue; - this.maturityDate = maturityDate; - } - - public State copy() { - return new State(this.issuance, this.owner, this.faceValue, this.maturityDate); - } - - @NotNull - @Override - public Pair withNewOwner(@NotNull AbstractParty newOwner) { - return new Pair<>(new Commands.Move(), new State(this.issuance, newOwner, this.faceValue, this.maturityDate)); - } - - public PartyAndReference getIssuance() { - return issuance; - } - - public AbstractParty getOwner() { - return owner; - } - - public Amount> getFaceValue() { - return faceValue; - } - - public Instant getMaturityDate() { - return maturityDate; - } - - @NotNull - @Override - public Contract getContract() { - return new JavaCommercialPaper(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - State state = (State) o; - - if (issuance != null ? !issuance.equals(state.issuance) : state.issuance != null) return false; - if (owner != null ? !owner.equals(state.owner) : state.owner != null) return false; - if (faceValue != null ? !faceValue.equals(state.faceValue) : state.faceValue != null) return false; - return !(maturityDate != null ? !maturityDate.equals(state.maturityDate) : state.maturityDate != null); - } - - @Override - public int hashCode() { - int result = issuance != null ? issuance.hashCode() : 0; - result = 31 * result + (owner != null ? owner.hashCode() : 0); - result = 31 * result + (faceValue != null ? faceValue.hashCode() : 0); - result = 31 * result + (maturityDate != null ? maturityDate.hashCode() : 0); - return result; - } - - @NotNull - @Override - public List getParticipants() { - return ImmutableList.of(this.owner); - } - } + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/State.java + :language: java + :start-after: DOCSTART 1 + :end-before: DOCEND 1 We define a class that implements the ``ContractState`` interface. -The ``ContractState`` interface requires us to provide a ``getContract`` method that returns an instance of the -contract class itself. In future, this may change to support dynamic loading of contracts with versioning -and signing constraints, but for now this is how it's written. - We have four fields in our state: * ``issuance``, a reference to a specific piece of commercial paper issued by some party. @@ -234,39 +129,17 @@ Let's define a few commands now: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + :language: kotlin + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + :dedent: 4 - interface Commands : CommandData { - class Move : TypeOnlyCommandData(), Commands - class Redeem : TypeOnlyCommandData(), Commands - class Issue : TypeOnlyCommandData(), Commands - } - - - .. sourcecode:: java - - public static class Commands implements core.contract.Command { - public static class Move extends Commands { - @Override - public boolean equals(Object obj) { - return obj instanceof Move; - } - } - - public static class Redeem extends Commands { - @Override - public boolean equals(Object obj) { - return obj instanceof Redeem; - } - } - - public static class Issue extends Commands { - @Override - public boolean equals(Object obj) { - return obj instanceof Issue; - } - } - } + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/CommercialPaper.java + :language: java + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + :dedent: 4 We define a simple grouping interface or static class, this gives us a type that all our commands have in common, then we go ahead and create three commands: ``Move``, ``Redeem``, ``Issue``. ``TypeOnlyCommandData`` is a helpful utility @@ -287,22 +160,17 @@ run two contracts one time each: Cash and CommercialPaper. .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + :language: kotlin + :start-after: DOCSTART 3 + :end-before: DOCEND 3 + :dedent: 4 - override fun verify(tx: LedgerTransaction) { - // Group by everything except owner: any modification to the CP at all is considered changing it fundamentally. - val groups = tx.groupStates(State::withoutOwner) - - // There are two possible things that can be done with this CP. The first is trading it. The second is redeeming - // it for cash on or after the maturity date. - val command = tx.commands.requireSingleCommand() - - .. sourcecode:: java - - @Override - public void verify(LedgerTransaction tx) { - List> groups = tx.groupStates(State.class, State::withoutOwner); - CommandWithParties cmd = requireSingleCommand(tx.getCommands(), Commands.class); + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/CommercialPaper.java + :language: java + :start-after: DOCSTART 3 + :end-before: DOCEND 3 + :dedent: 4 We start by using the ``groupStates`` method, which takes a type and a function. State grouping is a way of ensuring your contract can handle multiple unrelated states of the same type in the same transaction, which is needed for @@ -312,8 +180,6 @@ The second line does what the code suggests: it searches for a command object th ``CommercialPaper.Commands`` supertype, and either returns it, or throws an exception if there's zero or more than one such command. -.. _state_ref: - Using state groups ------------------ @@ -362,12 +228,12 @@ Here are some code examples: .. sourcecode:: kotlin // Type of groups is List>> - val groups = tx.groupStates() { it: Cash.State -> Pair(it.deposit, it.amount.currency) } - for ((inputs, outputs, key) in groups) { - // Either inputs or outputs could be empty. - val (deposit, currency) = key + val groups = tx.groupStates { it: Cash.State -> it.amount.token } + for ((inputs, outputs, key) in groups) { + // Either inputs or outputs could be empty. + val (deposit, currency) = key - ... + ... } .. sourcecode:: java @@ -414,101 +280,37 @@ in equals and hashCode. Checking the requirements ------------------------- - After extracting the command and the groups, we then iterate over each group and verify it meets the required business logic. .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + :language: kotlin + :start-after: DOCSTART 4 + :end-before: DOCEND 4 + :dedent: 8 - val timeWindow: TimeWindow? = tx.timeWindow - - for ((inputs, outputs, key) in groups) { - when (command.value) { - is Commands.Move -> { - val input = inputs.single() - requireThat { - "the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers) - "the state is propagated" using (outputs.size == 1) - // Don't need to check anything else, as if outputs.size == 1 then the output is equal to - // the input ignoring the owner field due to the grouping. - } - } - - is Commands.Redeem -> { - // Redemption of the paper requires movement of on-ledger cash. - val input = inputs.single() - val received = tx.outputs.map{ it.data }.sumCashBy(input.owner) - val time = timeWindow?.fromTime ?: throw IllegalArgumentException("Redemptions must be timestamped") - requireThat { - "the paper must have matured" using (time >= input.maturityDate) - "the received amount equals the face value" using (received == input.faceValue) - "the paper must be destroyed" using outputs.isEmpty() - "the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers) - } - } - - is Commands.Issue -> { - val output = outputs.single() - val time = timeWindow?.untilTime ?: throw IllegalArgumentException("Issuances must be timestamped") - requireThat { - // Don't allow people to issue commercial paper under other entities identities. - "output states are issued by a command signer" using (output.issuance.party.owningKey in command.signers) - "output values sum to more than the inputs" using (output.faceValue.quantity > 0) - "the maturity date is not in the past" using (time < output.maturityDate) - // Don't allow an existing CP state to be replaced by this issuance. - "can't reissue an existing state" by inputs.isEmpty() - } - } - - else -> throw IllegalArgumentException("Unrecognised command") - } - } - - .. sourcecode:: java - - Timestamp time = tx.getTimestamp(); // Can be null/missing. - for (InOutGroup group : groups) { - List inputs = group.getInputs(); - List outputs = group.getOutputs(); - - // For now do not allow multiple pieces of CP to trade in a single transaction. Study this more! - State input = single(filterIsInstance(inputs, State.class)); - - checkState(cmd.getSigners().contains(input.getOwner()), "the transaction is signed by the owner of the CP"); - - if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Move) { - checkState(outputs.size() == 1, "the state is propagated"); - // Don't need to check anything else, as if outputs.size == 1 then the output is equal to - // the input ignoring the owner field due to the grouping. - } else if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Redeem) { - TimeWindow timeWindow = tx.getTimeWindow(); - Instant time = null == timeWindow - ? null - : timeWindow.getUntilTime(); - Amount> received = CashKt.sumCashBy(tx.getOutputs(), input.getOwner()); - - checkState(received.equals(input.getFaceValue()), "received amount equals the face value"); - checkState(time != null && !time.isBefore(input.getMaturityDate(), "the paper must have matured"); - checkState(outputs.isEmpty(), "the paper must be destroyed"); - } else if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Issue) { - // .. etc .. (see Kotlin for full definition) - } - } + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/CommercialPaper.java + :language: java + :start-after: DOCSTART 4 + :end-before: DOCEND 4 + :dedent: 8 This loop is the core logic of the contract. The first line simply gets the timestamp out of the transaction. Timestamping of transactions is optional, so a time may be missing here. We check for it being null later. -.. note:: In future timestamping may be mandatory for all transactions. - .. warning:: In the Kotlin version as long as we write a comparison with the transaction time first the compiler will verify we didn't forget to check if it's missing. Unfortunately due to the need for smooth Java interop, this check won't happen if we write e.g. ``someDate > time``, it has to be ``time < someDate``. So it's good practice to always write the transaction timestamp first. +Next, we take one of three paths, depending on what the type of the command object is. + +**If the command is a ``Move`` command:** + The first line (first three lines in Java) impose a requirement that there be a single piece of commercial paper in this group. We do not allow multiple units of CP to be split or merged even if they are owned by the same owner. The ``single()`` method is a static *extension method* defined by the Kotlin standard library: given a list, it throws an @@ -520,23 +322,23 @@ behind the scenes, the code compiles to the same bytecodes. Next, we check that the transaction was signed by the public key that's marked as the current owner of the commercial paper. Because the platform has already verified all the digital signatures before the contract begins execution, all we have to do is verify that the owner's public key was one of the keys that signed the transaction. The Java code -is straightforward: we are simply using the ``Preconditions.checkState`` method from Guava. The Kotlin version looks a little odd: we have a *requireThat* construct that looks like it's -built into the language. In fact *requireThat* is an ordinary function provided by the platform's contract API. Kotlin -supports the creation of *domain specific languages* through the intersection of several features of the language, and -we use it here to support the natural listing of requirements. To see what it compiles down to, look at the Java version. -Each ``"string" using (expression)`` statement inside a ``requireThat`` turns into an assertion that the given expression is -true, with an ``IllegalStateException`` being thrown that contains the string if not. It's just another way to write out a regular -assertion, but with the English-language requirement being put front and center. +is straightforward: we are simply using the ``Preconditions.checkState`` method from Guava. The Kotlin version looks a +little odd: we have a *requireThat* construct that looks like it's built into the language. In fact *requireThat* is an +ordinary function provided by the platform's contract API. Kotlin supports the creation of *domain specific languages* +through the intersection of several features of the language, and we use it here to support the natural listing of +requirements. To see what it compiles down to, look at the Java version. Each ``"string" using (expression)`` statement +inside a ``requireThat`` turns into an assertion that the given expression is true, with an ``IllegalStateException`` +being thrown that contains the string if not. It's just another way to write out a regular assertion, but with the +English-language requirement being put front and center. -Next, we take one of two paths, depending on what the type of the command object is. +Next, we simply verify that the output state is actually present: a move is not allowed to delete the CP from the ledger. +The grouping logic already ensured that the details are identical and haven't been changed, save for the public key of +the owner. -If the command is a ``Move`` command, then we simply verify that the output state is actually present: a move is not -allowed to delete the CP from the ledger. The grouping logic already ensured that the details are identical and haven't -been changed, save for the public key of the owner. +**If the command is a ``Redeem`` command, then the requirements are more complex:** -If the command is a ``Redeem`` command, then the requirements are more complex: - -1. We want to see that the face value of the CP is being moved as a cash claim against some party, that is, the +1. We still check there is a CP input state. +2. We want to see that the face value of the CP is being moved as a cash claim against some party, that is, the issuer of the CP is really paying back the face value. 2. The transaction must be happening after the maturity date. 3. The commercial paper must *not* be propagated by this transaction: it must be deleted, by the group having no @@ -551,8 +353,9 @@ represented in the outputs! So we can see that this contract imposes a limitatio transaction: you are not allowed to move currencies in the same transaction that the CP does not involve. This limitation could be addressed with better APIs, if it were to be a real limitation. -Finally, we support an ``Issue`` command, to create new instances of commercial paper on the ledger. It likewise -enforces various invariants upon the issuance. +**Finally, we support an ``Issue`` command, to create new instances of commercial paper on the ledger.** + +It likewise enforces various invariants upon the issuance, such as, there must be one output CP state, for instance. This contract is simple and does not implement all the business logic a real commercial paper lifecycle management program would. For instance, there is no logic requiring a signature from the issuer for redemption: @@ -577,7 +380,6 @@ error message. Testing contracts with this domain specific language is covered in the separate tutorial, :doc:`tutorial-test-dsl`. - Adding a generation API to your contract ---------------------------------------- @@ -602,13 +404,11 @@ a method to wrap up the issuance process: .. container:: codeset - .. sourcecode:: kotlin - - fun generateIssue(issuance: PartyAndReference, faceValue: Amount>, maturityDate: Instant, - notary: Party): TransactionBuilder { - val state = State(issuance, issuance.party, faceValue, maturityDate) - return TransactionBuilder(notary = notary).withItems(state, Command(Commands.Issue(), issuance.party.owningKey)) - } + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + :language: kotlin + :start-after: DOCSTART 5 + :end-before: DOCEND 5 + :dedent: 4 We take a reference that points to the issuing party (i.e. the caller) and which can contain any internal bookkeeping/reference numbers that we may require. The reference field is an ideal place to put (for example) a @@ -625,12 +425,33 @@ outputs and commands to it and is designed to be passed around, potentially betw The function we define creates a ``CommercialPaper.State`` object that mostly just uses the arguments we were given, but it fills out the owner field of the state to be the same public key as the issuing party. +We then combine the ``CommercialPaper.State`` object with a reference to the ``CommercialPaper`` contract, which is +defined inside the contract itself + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + :language: kotlin + :start-after: DOCSTART 8 + :end-before: DOCEND 8 + :dedent: 4 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/CommercialPaper.java + :language: java + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + :dedent: 4 + +This value, which is the fully qualified class name of the contract, tells the Corda platform where to find the contract +code that should be used to validate a transaction containing an output state of this contract type. Typically the contract +code will be included in the transaction as an attachment (see :doc:`tutorial-attachments`). + The returned partial transaction has a ``Command`` object as a parameter. This is a container for any object that implements the ``CommandData`` interface, along with a list of keys that are expected to sign this transaction. In this case, issuance requires that the issuing party sign, so we put the key of the party there. The ``TransactionBuilder`` has a convenience ``withItems`` method that takes a variable argument list. You can pass in -any ``StateAndRef`` (input), ``ContractState`` (output) or ``Command`` objects and it'll build up the transaction +any ``StateAndRef`` (input), ``StateAndContract`` (output) or ``Command`` objects and it'll build up the transaction for you. There's one final thing to be aware of: we ask the caller to select a *notary* that controls this state and @@ -643,13 +464,11 @@ What about moving the paper, i.e. reassigning ownership to someone else? .. container:: codeset - .. sourcecode:: kotlin - - fun generateMove(tx: TransactionBuilder, paper: StateAndRef, newOwner: AbstractParty) { - tx.addInputState(paper) - tx.addOutputState(paper.state.data.withOwner(newOwner)) - tx.addCommand(Command(Commands.Move(), paper.state.data.owner.owningKey)) - } + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + :language: kotlin + :start-after: DOCSTART 6 + :end-before: DOCEND 6 + :dedent: 4 Here, the method takes a pre-existing ``TransactionBuilder`` and adds to it. This is correct because typically you will want to combine a sale of CP atomically with the movement of some other asset, such as cash. So both @@ -668,15 +487,11 @@ Finally, we can do redemption. .. container:: codeset - .. sourcecode:: kotlin - - @Throws(InsufficientBalanceException::class) - fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, services: ServiceHub) { - // Add the cash movement using the states in our vault. - Cash.generateSpend(services, tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner) - tx.addInputState(paper) - tx.addCommand(Command(Commands.Redeem(), paper.state.data.owner.owningKey)) - } + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt + :language: kotlin + :start-after: DOCSTART 7 + :end-before: DOCEND 7 + :dedent: 4 Here we can see an example of composing contracts together. When an owner wishes to redeem the commercial paper, the issuer (i.e. the caller) must gather cash from its vault and send the face value to the owner of the paper. @@ -747,15 +562,14 @@ Encumbrances All contract states may be *encumbered* by up to one other state, which we call an **encumbrance**. -The encumbrance state, if present, forces additional controls over the encumbered state, since the encumbrance state contract -will also be verified during the execution of the transaction. For example, a contract state could be encumbered -with a time-lock contract state; the state is then only processable in a transaction that verifies that the time -specified in the encumbrance time-lock has passed. +The encumbrance state, if present, forces additional controls over the encumbered state, since the encumbrance state +contract will also be verified during the execution of the transaction. For example, a contract state could be +encumbered with a time-lock contract state; the state is then only processable in a transaction that verifies that the +time specified in the encumbrance time-lock has passed. -The encumbered state refers to its encumbrance by index, and the referred encumbrance state -is an output state in a particular position on the same transaction that created the encumbered state. Note that an -encumbered state that is being consumed must have its encumbrance consumed in the same transaction, otherwise the -transaction is not valid. +The encumbered state refers to its encumbrance by index, and the referred encumbrance state is an output state in a +particular position on the same transaction that created the encumbered state. Note that an encumbered state that is +being consumed must have its encumbrance consumed in the same transaction, otherwise the transaction is not valid. The encumbrance reference is optional in the ``ContractState`` interface: diff --git a/docs/source/tutorial-integration-testing.rst b/docs/source/tutorial-integration-testing.rst index 0cbed18a15..f2e410d347 100644 --- a/docs/source/tutorial-integration-testing.rst +++ b/docs/source/tutorial-integration-testing.rst @@ -4,14 +4,14 @@ Integration testing Integration testing involves bringing up nodes locally and testing invariants about them by starting flows and inspecting their state. -In this tutorial we will bring up three nodes Alice, Bob and a -Notary. Alice will issue Cash to Bob, then Bob will send this Cash +In this tutorial we will bring up three nodes - Alice, Bob and a +notary. Alice will issue cash to Bob, then Bob will send this cash back to Alice. We will see how to test some simple deterministic and nondeterministic invariants in the meantime. -(Note that this example where Alice is self-issuing Cash is purely for -demonstration purposes, in reality Cash would be issued by a bank and -subsequently passed around.) +.. note:: This example where Alice is self-issuing cash is purely for + demonstration purposes, in reality, cash would be issued by a bank + and subsequently passed around. In order to spawn nodes we will use the Driver DSL. This DSL allows one to start up node processes from code. It manages a network map @@ -21,53 +21,55 @@ service and safe shutting down of nodes in the background. :language: kotlin :start-after: START 1 :end-before: END 1 + :dedent: 8 -The above code creates a ``User`` permissioned to start the -``CashFlow`` protocol. It then starts up Alice and Bob with this user, -allowing us to later connect to the nodes. +The above code starts three nodes: -Then the notary is started up. Note that we need to add -``ValidatingNotaryService`` as an advertised service in order for this -node to serve notary functionality. This is also where flows added in -plugins should be specified. Note also that we won't connect to the -notary directly, so there's no need to pass in the test ``User``. +* Alice, who has user permissions to start the ``CashIssueFlow`` and + ``CashPaymentFlow`` flows +* Bob, who only has user permissions to start the ``CashPaymentFlow`` +* A notary that offers a ``ValidatingNotaryService``. We won't connect + to the notary directly, so there's no need to provide a ``User`` The ``startNode`` function returns a future that completes once the node is fully started. This allows starting of the nodes to be parallel. We wait on these futures as we need the information -returned; their respective ``NodeHandles`` s. After getting the handles we -wait for both parties to register with the network map to ensure we don't -have race conditions with network map registration. +returned; their respective ``NodeHandles`` s. .. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt :language: kotlin :start-after: START 2 :end-before: END 2 + :dedent: 12 -Next we connect to Alice and Bob respectively from the test process -using the test user we created. Then we establish RPC links that allow -us to start flows and query state. +After getting the handles we wait for both parties to register with +the network map to ensure we don't have race conditions with network +map registration. Next we connect to Alice and Bob respectively from +the test process using the test user we created. Then we establish RPC +links that allow us to start flows and query state. .. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt :language: kotlin :start-after: START 3 :end-before: END 3 + :dedent: 12 We will be interested in changes to Alice's and Bob's vault, so we query a stream of vault updates from each. -Now that we're all set up we can finally get some Cash action going! +Now that we're all set up we can finally get some cash action going! .. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt :language: kotlin :start-after: START 4 :end-before: END 4 + :dedent: 12 The first loop creates 10 threads, each starting a ``CashFlow`` flow on the Alice node. We specify that we want to issue ``i`` dollars to -Bob, using the Notary as the notary responsible for notarising the +Bob, setting our notary as the notary responsible for notarising the created states. Note that no notarisation will occur yet as we're not -spending any states, only entering new ones to the ledger. +spending any states, only creating new ones on the ledger. We started the flows from different threads for the sake of the tutorial, to demonstrate how to test non-determinism, which is what @@ -76,20 +78,22 @@ the ``expectEvents`` block does. The Expect DSL allows ordering constraints to be checked on a stream of events. The above code specifies that we are expecting 10 updates to be emitted on the ``bobVaultUpdates`` stream in unspecified order -(this is what the ``parallel`` construct does). We specify a +(this is what the ``parallel`` construct does). We specify an (otherwise optional) ``match`` predicate to identify specific updates we are interested in, which we then print. If we run the code written so far we should see 4 nodes starting up -(Alice,Bob,Notary + implicit Network Map service), then 10 logs of Bob -receiving 1,2,...10 dollars from Alice in some unspecified order. +(Alice, Bob, the notary and an implicit Network Map service), then +10 logs of Bob receiving 1,2,...10 dollars from Alice in some unspecified +order. -Next we want Bob to send this Cash back to Alice. +Next we want Bob to send this cash back to Alice. .. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt :language: kotlin :start-after: START 5 :end-before: END 5 + :dedent: 12 This time we'll do it sequentially. We make Bob pay 1,2,..10 dollars to Alice in order. We make sure that a the ``CashFlow`` has finished @@ -109,8 +113,7 @@ To run the complete test you can open ``example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt`` from IntelliJ and run the test, or alternatively use gradle: - .. sourcecode:: bash # Run example-code integration tests - ./gradlew docs/source/example-code:integrationTest -i + ./gradlew docs/source/example-code:integrationTest -i \ No newline at end of file diff --git a/docs/source/tutorial-tear-offs.rst b/docs/source/tutorial-tear-offs.rst index 12a1ec8512..80d8395112 100644 --- a/docs/source/tutorial-tear-offs.rst +++ b/docs/source/tutorial-tear-offs.rst @@ -1,39 +1,32 @@ Transaction tear-offs ===================== -Example of usage ----------------- -Let’s focus on a code example. We want to construct a transaction with commands containing interest rate fix data as in: -:doc:`oracles`. After construction of a partial transaction, with included ``Fix`` commands in it, we want to send it -to the Oracle for checking and signing. To do so we need to specify which parts of the transaction are going to be -revealed. That can be done by constructing filtering function over fields of ``WireTransaction`` of type ``(Any) -> -Boolean``. +Suppose we want to construct a transaction that includes commands containing interest rate fix data as in +:doc:`oracles`. Before sending the transaction to the oracle to obtain its signature, we need to filter out every part +of the transaction except for the ``Fix`` commands. + +To do so, we need to create a filtering function that specifies which fields of the transaction should be included. +Each field will only be included if the filtering function returns `true` when the field is passed in as input. .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/tearoffs/TutorialTearOffs.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + :dedent: 4 - val partialTx = ... - val oracle: Party = ... - fun filtering(elem: Any): Boolean { - return when (elem) { - is Command -> oracleParty.owningKey in elem.signers && elem.value is Fix - else -> false - } - } - -Assuming that we already assembled partialTx with some commands and know the identity of Oracle service, we construct -filtering function over commands - ``filtering``. It performs type checking and filters only ``Fix`` commands as in -IRSDemo example. Then we can construct ``FilteredTransaction``: +We can now use our filtering function to construct a ``FilteredTransaction``: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/tearoffs/TutorialTearOffs.kt + :language: kotlin + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + :dedent: 4 - val wtx: WireTransaction = partialTx.toWireTransaction() - val ftx: FilteredTransaction = wtx.buildFilteredTransaction(filtering) - -In the Oracle example this step takes place in ``RatesFixFlow`` by overriding ``filtering`` function, see: +In the Oracle example this step takes place in ``RatesFixFlow`` by overriding the ``filtering`` function. See :ref:`filtering_ref`. Both ``WireTransaction`` and ``FilteredTransaction`` inherit from ``TraversableTransaction``, so access to the @@ -42,45 +35,25 @@ transaction components is exactly the same. Note that unlike ``WireTransaction`` .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/tearoffs/TutorialTearOffs.kt + :language: kotlin + :start-after: DOCSTART 3 + :end-before: DOCEND 3 + :dedent: 4 - // Direct access to included commands, inputs, outputs, attachments etc. - val cmds: List = ftx.commands - val ins: List = ftx.inputs - val timeWindow: TimeWindow? = ftx.timeWindow - ... +The following code snippet is taken from ``NodeInterestRates.kt`` and implements a signing part of an Oracle. .. literalinclude:: ../../samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 - -Above code snippet is taken from ``NodeInterestRates.kt`` file and implements a signing part of an Oracle. You can -check only leaves using ``ftx.checkWithFun { check(it) }`` and then verify obtained ``FilteredTransaction`` to see -if its data belongs to ``WireTransaction`` with provided ``id``. All you need is the root hash -of the full transaction: - -.. container:: codeset - - .. sourcecode:: kotlin - - if (!ftx.verify(merkleRoot)){ - throw MerkleTreeException("Rate Fix Oracle: Couldn't verify partial Merkle tree.") - } - -Or combine the two steps together: - -.. container:: codeset - - .. sourcecode:: kotlin - - ftx.verifyWithFunction(merkleRoot, ::check) + :dedent: 8 .. note:: The way the ``FilteredTransaction`` is constructed ensures that after signing of the root hash it's impossible -to add or remove components (leaves). However, it can happen that having transaction with multiple commands one party -reveals only subset of them to the Oracle. As signing is done now over the Merkle root hash, the service signs all -commands of given type, even though it didn't see all of them. In the case however where all of the commands should be -visible to an Oracle, one can type ``ftx.checkAllComponentsVisible(COMMANDS_GROUP)`` before invoking ``ftx.verify``. -``checkAllComponentsVisible`` is using a sophisticated underlying partial Merkle tree check to guarantee that all of -the components of a particular group that existed in the original ``WireTransaction`` are included in the received -``FilteredTransaction``. + to add or remove components (leaves). However, it can happen that having transaction with multiple commands one party + reveals only subset of them to the Oracle. As signing is done now over the Merkle root hash, the service signs all + commands of given type, even though it didn't see all of them. In the case however where all of the commands should be + visible to an Oracle, one can type ``ftx.checkAllComponentsVisible(COMMANDS_GROUP)`` before invoking ``ftx.verify``. + ``checkAllComponentsVisible`` is using a sophisticated underlying partial Merkle tree check to guarantee that all of + the components of a particular group that existed in the original ``WireTransaction`` are included in the received + ``FilteredTransaction``. \ No newline at end of file diff --git a/docs/source/tutorial-test-dsl.rst b/docs/source/tutorial-test-dsl.rst index 2eb870c781..8bfbab79b0 100644 --- a/docs/source/tutorial-test-dsl.rst +++ b/docs/source/tutorial-test-dsl.rst @@ -52,27 +52,17 @@ We will start with defining helper function that returns a ``CommercialPaper`` s .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + :dedent: 4 - fun getPaper(): ICommercialPaperState = CommercialPaper.State( - issuance = MEGA_CORP.ref(123), - owner = MEGA_CORP, - faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123), - maturityDate = TEST_TX_TIME + 7.days - ) - - .. sourcecode:: java - - private final OpaqueBytes defaultRef = new OpaqueBytes(new byte[]{123}); - - private ICommercialPaperState getPaper() { - return new JavaCommercialPaper.State( - getMEGA_CORP().ref(defaultRef), - getMEGA_CORP(), - issuedBy(DOLLARS(1000), getMEGA_CORP().ref(defaultRef)), - getTEST_TX_TIME().plus(7, ChronoUnit.DAYS) - ); - } + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + :dedent: 4 It's a ``CommercialPaper`` issued by ``MEGA_CORP`` with face value of $1000 and maturity date in 7 days. @@ -87,7 +77,7 @@ Let's add a ``CommercialPaper`` transaction: val inState = getPaper() ledger { transaction { - input(inState) + input(CommercialPaper.CP_PROGRAM_ID) { inState } } } } @@ -129,65 +119,33 @@ last line of ``transaction``: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + :dedent: 4 - @Test - fun simpleCP() { - val inState = getPaper() - ledger { - transaction { - input(inState) - this.verifies() - } - } - } - - .. sourcecode:: java - - @Test - public void simpleCP() { - ICommercialPaperState inState = getPaper(); - ledger(l -> { - l.transaction(tx -> { - tx.input(inState); - return tx.verifies(); - }); - return Unit.INSTANCE; - }); - } + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + :dedent: 4 Let's take a look at a transaction that fails. .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 3 + :end-before: DOCEND 3 + :dedent: 4 - @Test - fun simpleCPMove() { - val inState = getPaper() - ledger { - transaction { - input(inState) - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } - this.verifies() - } - } - } - - .. sourcecode:: java - - @Test - public void simpleCPMove() { - ICommercialPaperState inState = getPaper(); - ledger(l -> { - l.transaction(tx -> { - tx.input(inState); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); - return tx.verifies(); - }); - return Unit.INSTANCE; - }); - } + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java +:language: java + :start-after: DOCSTART 3 + :end-before: DOCEND 3 + :dedent: 4 When run, that code produces the following error: @@ -206,71 +164,33 @@ However we can specify that this is an intended behaviour by changing ``this.ver .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 4 + :end-before: DOCEND 4 + :dedent: 4 - @Test - fun simpleCPMoveFails() { - val inState = getPaper() - ledger { - transaction { - input(inState) - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } - this `fails with` "the state is propagated" - } - } - } - - .. sourcecode:: java - - @Test - public void simpleCPMoveFails() { - ICommercialPaperState inState = getPaper(); - ledger(l -> { - l.transaction(tx -> { - tx.input(inState); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); - return tx.failsWith("the state is propagated"); - }); - return Unit.INSTANCE; - }); - } + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 4 + :end-before: DOCEND 4 + :dedent: 4 We can continue to build the transaction until it ``verifies``: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 5 + :end-before: DOCEND 5 + :dedent: 4 - @Test - fun simpleCPMoveSuccess() { - val inState = getPaper() - ledger { - transaction { - input(inState) - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } - this `fails with` "the state is propagated" - output(CommercialPaper.CP_PROGRAM_ID, "alice's paper") { inState `owned by` ALICE_PUBKEY } - this.verifies() - } - } - } - - .. sourcecode:: java - - @Test - public void simpleCPMoveSuccess() { - ICommercialPaperState inState = getPaper(); - ledger(l -> { - l.transaction(tx -> { - tx.input(inState); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); - tx.failsWith("the state is propagated"); - tx.output(CommercialPaper.CP_PROGRAM_ID, "alice's paper", inState.withOwner(getALICE_PUBKEY())); - return tx.verifies(); - }); - return Unit.INSTANCE; - }); - } + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 5 + :end-before: DOCEND 5 + :dedent: 4 ``output`` specifies that we want the input state to be transferred to ``ALICE`` and ``command`` adds the ``Move`` command itself, signed by the current owner of the input state, ``MEGA_CORP_PUBKEY``. @@ -283,45 +203,17 @@ What should we do if we wanted to test what happens when the wrong party signs t .. container:: codeset - .. sourcecode:: kotlin - - @Test - fun `simple issuance with tweak`() { - ledger { - transaction { - output(CommercialPaper.CP_PROGRAM_ID, "paper") { getPaper() } // Some CP is issued onto the ledger by MegaCorp. - tweak { - command(DUMMY_PUBKEY_1) { CommercialPaper.Commands.Issue() } - timestamp(TEST_TX_TIME) - this `fails with` "output states are issued by a command signer" - } - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } - timestamp(TEST_TX_TIME) - this.verifies() - } - } - } - - .. sourcecode:: java - - @Test - public void simpleIssuanceWithTweak() { - ledger(l -> { - l.transaction(tx -> { - tx.output(CommercialPaper.CP_PROGRAM_ID, "paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp. - tx.tweak(tw -> { - tw.command(getDUMMY_PUBKEY_1(), new JavaCommercialPaper.Commands.Issue()); - tw.timestamp(getTEST_TX_TIME()); - return tw.failsWith("output states are issued by a command signer"); - }); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); - tx.timestamp(getTEST_TX_TIME()); - return tx.verifies(); - }); - return Unit.INSTANCE; - }); - } + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 6 + :end-before: DOCEND 6 + :dedent: 4 + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 6 + :end-before: DOCEND 6 + :dedent: 4 ``tweak`` creates a local copy of the transaction. This makes possible to locally "ruin" the transaction while not modifying the original one, allowing testing of different error conditions. @@ -332,39 +224,17 @@ ledger with a single transaction: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 7 + :end-before: DOCEND 7 + :dedent: 4 - @Test - fun `simple issuance with tweak and top level transaction`() { - transaction { - output(CommercialPaper.CP_PROGRAM_ID, "paper") { getPaper() } // Some CP is issued onto the ledger by MegaCorp. - tweak { - command(DUMMY_PUBKEY_1) { CommercialPaper.Commands.Issue() } - timestamp(TEST_TX_TIME) - this `fails with` "output states are issued by a command signer" - } - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } - timestamp(TEST_TX_TIME) - this.verifies() - } - } - - .. sourcecode:: java - - @Test - public void simpleIssuanceWithTweakTopLevelTx() { - transaction(tx -> { - tx.output(CommercialPaper.CP_PROGRAM_ID, "paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp. - tx.tweak(tw -> { - tw.command(getDUMMY_PUBKEY_1(), new JavaCommercialPaper.Commands.Issue()); - tw.timestamp(getTEST_TX_TIME()); - return tw.failsWith("output states are issued by a command signer"); - }); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); - tx.timestamp(getTEST_TX_TIME()); - return tx.verifies(); - }); - } + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 7 + :end-before: DOCEND 7 + :dedent: 4 Chaining transactions --------------------- @@ -373,72 +243,17 @@ Now that we know how to define a single transaction, let's look at how to define .. container:: codeset - .. sourcecode:: kotlin - - @Test - fun `chain commercial paper`() { - val issuer = MEGA_CORP.ref(123) - - ledger { - unverifiedTransaction { - output(Cash.CP_PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY) - } - - // Some CP is issued onto the ledger by MegaCorp. - transaction("Issuance") { - output(CommercialPaper.CP_PROGRAM_ID, "paper") { getPaper() } - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } - timestamp(TEST_TX_TIME) - this.verifies() - } - - - transaction("Trade") { - input("paper") - input("alice's $900") - output(Cash.CP_PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } - output(CommercialPaper.CP_PROGRAM_ID, "alice's paper") { "paper".output() `owned by` ALICE_PUBKEY } - command(ALICE_PUBKEY) { Cash.Commands.Move() } - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } - this.verifies() - } - } - } - - .. sourcecode:: java - - @Test - public void chainCommercialPaper() { - PartyAndReference issuer = getMEGA_CORP().ref(defaultRef); - ledger(l -> { - l.unverifiedTransaction(tx -> { - tx.output(Cash.CP_PROGRAM_ID, "alice's $900", - new Cash.State(issuedBy(DOLLARS(900), issuer), getALICE_PUBKEY(), null)); - return Unit.INSTANCE; - }); - - // Some CP is issued onto the ledger by MegaCorp. - l.transaction("Issuance", tx -> { - tx.output(CommercialPaper.CP_PROGRAM_ID, "paper", getPaper()); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); - tx.timestamp(getTEST_TX_TIME()); - return tx.verifies(); - }); - - l.transaction("Trade", tx -> { - tx.input("paper"); - tx.input("alice's $900"); - tx.output(Cash.CP_PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getMEGA_CORP_PUBKEY(), null)); - JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); - tx.output(CommercialPaper.CP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(getALICE_PUBKEY())); - tx.command(getALICE_PUBKEY(), new Cash.Commands.Move()); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); - return tx.verifies(); - }); - return Unit.INSTANCE; - }); - } + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 8 + :end-before: DOCEND 8 + :dedent: 4 + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 8 + :end-before: DOCEND 8 + :dedent: 4 In this example we declare that ``ALICE`` has $900 but we don't care where from. For this we can use ``unverifiedTransaction``. Note how we don't need to specify ``this.verifies()``. @@ -455,182 +270,31 @@ To do so let's create a simple example that uses the same input twice: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 9 + :end-before: DOCEND 9 + :dedent: 4 - @Test - fun `chain commercial paper double spend`() { - val issuer = MEGA_CORP.ref(123) - ledger { - unverifiedTransaction { - output(Cash.CP_PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY) - } - - // Some CP is issued onto the ledger by MegaCorp. - transaction("Issuance") { - output(CommercialPaper.CP_PROGRAM_ID, "paper") { getPaper() } - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } - timestamp(TEST_TX_TIME) - this.verifies() - } - - transaction("Trade") { - input("paper") - input("alice's $900") - output(Cash.CP_PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } - output(CommercialPaper.CP_PROGRAM_ID, "alice's paper") { "paper".output() `owned by` ALICE_PUBKEY } - command(ALICE_PUBKEY) { Cash.Commands.Move() } - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } - this.verifies() - } - - transaction { - input("paper") - // We moved a paper to another pubkey. - output(CommercialPaper.CP_PROGRAM_ID, "bob's paper") { "paper".output() `owned by` BOB_PUBKEY } - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } - this.verifies() - } - - this.fails() - } - } - - .. sourcecode:: java - - @Test - public void chainCommercialPaperDoubleSpend() { - PartyAndReference issuer = getMEGA_CORP().ref(defaultRef); - ledger(l -> { - l.unverifiedTransaction(tx -> { - tx.output(Cash.CP_PROGRAM_ID, "alice's $900", - new Cash.State(issuedBy(DOLLARS(900), issuer), getALICE_PUBKEY(), null)); - return Unit.INSTANCE; - }); - - // Some CP is issued onto the ledger by MegaCorp. - l.transaction("Issuance", tx -> { - tx.output(CommercialPaper.CP_PROGRAM_ID, "paper", getPaper()); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); - tx.timestamp(getTEST_TX_TIME()); - return tx.verifies(); - }); - - l.transaction("Trade", tx -> { - tx.input("paper"); - tx.input("alice's $900"); - tx.output(Cash.CP_PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getMEGA_CORP_PUBKEY(), null)); - JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); - tx.output(CommercialPaper.CP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(getALICE_PUBKEY())); - tx.command(getALICE_PUBKEY(), new Cash.Commands.Move()); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); - return tx.verifies(); - }); - - l.transaction(tx -> { - tx.input("paper"); - JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); - // We moved a paper to other pubkey. - tx.output(CommercialPaper.CP_PROGRAM_ID, "bob's paper", inputPaper.withOwner(getBOB_PUBKEY())); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); - return tx.verifies(); - }); - l.fails(); - return Unit.INSTANCE; - }); - } + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 9 + :end-before: DOCEND 9 + :dedent: 4 The transactions ``verifies()`` individually, however the state was spent twice! That's why we need the global ledger verification (``this.fails()`` at the end). As in previous examples we can use ``tweak`` to create a local copy of the whole ledger: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 10 + :end-before: DOCEND 10 + :dedent: 4 - @Test - fun `chain commercial tweak`() { - val issuer = MEGA_CORP.ref(123) - ledger { - unverifiedTransaction { - output(Cash.CP_PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY) - } - - // Some CP is issued onto the ledger by MegaCorp. - transaction("Issuance") { - output(CommercialPaper.CP_PROGRAM_ID, "paper") { getPaper() } - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } - timestamp(TEST_TX_TIME) - this.verifies() - } - - transaction("Trade") { - input("paper") - input("alice's $900") - output(Cash.CP_PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } - output(CommercialPaper.CP_PROGRAM_ID, "alice's paper") { "paper".output() `owned by` ALICE_PUBKEY } - command(ALICE_PUBKEY) { Cash.Commands.Move() } - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } - this.verifies() - } - - tweak { - transaction { - input("paper") - // We moved a paper to another pubkey. - output(CommercialPaper.CP_PROGRAM_ID, "bob's paper") { "paper".output() `owned by` BOB_PUBKEY } - command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } - this.verifies() - } - this.fails() - } - - this.verifies() - } - } - - .. sourcecode:: java - - @Test - public void chainCommercialPaperTweak() { - PartyAndReference issuer = getMEGA_CORP().ref(defaultRef); - ledger(l -> { - l.unverifiedTransaction(tx -> { - tx.output(Cash.CP_PROGRAM_ID, "alice's $900", - new Cash.State(issuedBy(DOLLARS(900), issuer), getALICE_PUBKEY(), null)); - return Unit.INSTANCE; - }); - - // Some CP is issued onto the ledger by MegaCorp. - l.transaction("Issuance", tx -> { - tx.output(CommercialPaper.CP_PROGRAM_ID, "paper", getPaper()); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue()); - tx.timestamp(getTEST_TX_TIME()); - return tx.verifies(); - }); - - l.transaction("Trade", tx -> { - tx.input("paper"); - tx.input("alice's $900"); - tx.output(Cash.CP_PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getMEGA_CORP_PUBKEY(), null)); - JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); - tx.output(CommercialPaper.CP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(getALICE_PUBKEY())); - tx.command(getALICE_PUBKEY(), new Cash.Commands.Move()); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); - return tx.verifies(); - }); - - l.tweak(lw -> { - lw.transaction(tx -> { - tx.input("paper"); - JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); - // We moved a paper to another pubkey. - tx.output(CommercialPaper.CP_PROGRAM_ID, "bob's paper", inputPaper.withOwner(getBOB_PUBKEY())); - tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move()); - return tx.verifies(); - }); - lw.fails(); - return Unit.INSTANCE; - }); - l.verifies(); - return Unit.INSTANCE; - }); - } + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 10 + :end-before: DOCEND 10 + :dedent: 4 \ No newline at end of file diff --git a/docs/source/tutorials-index.rst b/docs/source/tutorials-index.rst index f9ffad2dd3..c0b1a5af14 100644 --- a/docs/source/tutorials-index.rst +++ b/docs/source/tutorials-index.rst @@ -15,7 +15,6 @@ Tutorials flow-state-machines flow-testing running-a-notary - using-a-notary oracles tutorial-tear-offs tutorial-attachments diff --git a/docs/source/using-a-notary.rst b/docs/source/using-a-notary.rst deleted file mode 100644 index d37d05809d..0000000000 --- a/docs/source/using-a-notary.rst +++ /dev/null @@ -1,138 +0,0 @@ -Using a notary service ----------------------- - -This tutorial describes how to assign a notary to a newly issued state, and how to get a transaction notarised by -obtaining a signature of the required notary. It assumes some familiarity with *flows* and how to write them, as described -in :doc:`flow-state-machines`. - -Assigning a notary -================== - -The first step is to choose a notary and obtain its identity. Identities of all notaries on the network are kept by -the :ref:`network-map-service`. The network map cache exposes two methods for obtaining a notary: - -.. sourcecode:: kotlin - - /** - * Gets a notary identity by the given name. - */ - fun getNotary(name: String): Party? - - /** - * Returns a notary identity advertised by any of the nodes on the network (chosen at random) - * - * @param type Limits the result to notaries of the specified type (optional) - */ - fun getAnyNotary(type: ServiceType? = null): Party? - -Currently notaries can only be differentiated by name and type, but in the future the network map service will be -able to provide more metadata, such as location or legal identities of the nodes operating it. - -Now, let's say we want to issue an asset and assign it to a notary named "Notary A". -The first step is to obtain the notary identity -- ``Party``: - -.. sourcecode:: kotlin - - val ourNotary: Party = serviceHub.networkMapCache.getNotary("Notary A") - -Then we initialise the transaction builder: - -.. sourcecode:: kotlin - - val builder: TransactionBuilder = TransactionBuilder(notary = ourNotary) - -For any output state we add to this transaction builder, ``ourNotary`` will be assigned as its notary. -Next we create a state object and assign ourselves as the owner. For this example we'll use a -``DummyContract.State``, which is a simple state that just maintains an integer and can change ownership. - -.. sourcecode:: kotlin - - val myIdentity = serviceHub.chooseIdentity() - val state = DummyContract.SingleOwnerState(magicNumber = 42, owner = myIdentity.owningKey) - -Then we add the state as the transaction output along with the relevant command. The state will automatically be assigned -to our previously specified "Notary A". - -.. sourcecode:: kotlin - - builder.addOutputState(state) - val createCommand = DummyContract.Commands.Create() - builder.addCommand(Command(createCommand, myIdentity)) - -We then sign the transaction, build and record it to our transaction storage: - -.. sourcecode:: kotlin - - val mySigningKey: PublicKey = serviceHub.chooseIdentity().owningKey - val issueTransaction = serviceHub.toSignedTransaction(issueTransaction, mySigningKey) - serviceHub.recordTransactions(issueTransaction) - -The transaction is recorded and we now have a state (asset) in possession that we can transfer to someone else. Note -that the issuing transaction does not need to be notarised, as it doesn't consume any input states. - -Notarising a transaction -======================== - -Following our example for the previous section, let's say we now want to transfer our issued state to Alice. - -First we obtain a reference to the state, which will be the input to our "move" transaction: - -.. sourcecode:: kotlin - - val stateRef = StateRef(txhash = issueTransaction.id, index = 0) - -Then we create a new state -- a copy of our state but with the owner set to Alice. This is a bit more involved so -we just use a helper that handles it for us. We also assume that we already have the ``Party`` for Alice, ``aliceParty``. - -.. sourcecode:: kotlin - - val inputState = StateAndRef(state, stateRef) - val moveTransactionBuilder = DummyContract.withNewOwnerAndAmount(inputState, newOwner = aliceParty.owningKey) - -The ``DummyContract.withNewOwnerAndAmount()`` method will a new transaction builder with our old state as the input, a new state -with Alice as the owner, and a relevant contract command for "move". - -Again we sign the transaction, and build it: - -.. sourcecode:: kotlin - - // We build it and add our default identity signature without checking if all signatures are present, - // Note we know that the notary signature is missing, so thie SignedTransaction is still partial. - val moveTransaction = serviceHub.toSignedTransaction(moveTransactionBuilder) - -Next we need to obtain a signature from the notary for the transaction to be valid. Prior to signing, the notary will -commit our old (input) state so it cannot be used again. - -To manually obtain a signature from a notary we can run the ``NotaryFlow.Client`` flow. The flow will work out -which notary needs to be called based on the input states (and the timestamp command, if it's present). - -.. sourcecode:: kotlin - - // The subFlow() helper is available within the context of a Flow - val notarySignature: DigitalSignature = subFlow(NotaryFlow.Client(moveTransaction)) - -.. note:: If our input state has already been consumed in another transaction, then ``NotaryFlow`` with throw a ``NotaryException`` - containing the conflict details: - - .. sourcecode:: kotlin - - /** Specifies the consuming transaction for the conflicting input state */ - data class Conflict(val stateHistory: Map) - - /** - * Specifies the transaction id, the position of the consumed state in the inputs, and - * the caller identity requesting the commit - */ - data class ConsumingTx(val id: SecureHash, val inputIndex: Int, val requestingParty: Party) - - Conflict handling and resolution is currently the responsibility of the flow author. - -Note that instead of calling the notary directly, we would normally call ``FinalityFlow`` passing in the ``SignedTransaction`` -(including signatures from the participants) and a list of participants to notify. The flow will request a notary signature -if needed, record the notarised transaction, and then send a copy of the transaction to all participants for them to store. -``FinalityFlow`` delegates to ``NotaryFlow.Client`` followed by ``BroadcastTransactionFlow`` to do the -actual work of notarising and broadcasting the transaction. For example: - -.. sourcecode:: kotlin - - subFlow(FinalityFlow(moveTransaction, setOf(aliceParty)) diff --git a/finance/src/main/java/net/corda/finance/contracts/JavaCommercialPaper.java b/finance/src/main/java/net/corda/finance/contracts/JavaCommercialPaper.java index 468d1919c8..02bf2bbd45 100644 --- a/finance/src/main/java/net/corda/finance/contracts/JavaCommercialPaper.java +++ b/finance/src/main/java/net/corda/finance/contracts/JavaCommercialPaper.java @@ -32,7 +32,7 @@ import static net.corda.core.contracts.ContractsDSL.requireThat; */ @SuppressWarnings("unused") public class JavaCommercialPaper implements Contract { - static final String JCP_PROGRAM_ID = "net.corda.finance.contracts.JavaCommercialPaper"; + public static final String JCP_PROGRAM_ID = "net.corda.finance.contracts.JavaCommercialPaper"; @SuppressWarnings("unused") public static class State implements OwnableState, ICommercialPaperState { diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt b/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt index 13248bd58e..1079beeccc 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt @@ -29,12 +29,16 @@ import java.util.* // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// DOCSTART 1 /** A [FixOf] identifies the question side of a fix: what day, tenor and type of fix ("LIBOR", "EURIBOR" etc) */ @CordaSerializable data class FixOf(val name: String, val forDay: LocalDate, val ofTenor: Tenor) +// DOCEND 1 +// DOCSTART 2 /** A [Fix] represents a named interest rate, on a given day, for a given duration. It can be embedded in a tx. */ data class Fix(val of: FixOf, val value: BigDecimal) : CommandData +// DOCEND 2 /** Represents a textual expression of e.g. a formula */ @CordaSerializable diff --git a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt index 833d1bec1c..17313ec688 100644 --- a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt +++ b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt @@ -74,6 +74,7 @@ fun main(args: Array) { } /** An in memory test zip attachment of at least numOfClearBytes size, will be used. */ +// DOCSTART 2 fun sender(rpc: CordaRPCOps, numOfClearBytes: Int = 1024) { // default size 1K. val (inputStream, hash) = InputStreamAndHash.createInMemoryTestZip(numOfClearBytes, 0) val executor = Executors.newScheduledThreadPool(2) @@ -105,6 +106,7 @@ private fun sender(rpc: CordaRPCOps, inputStream: InputStream, hash: SecureHash. val stx = flowHandle.returnValue.getOrThrow() println("Sent ${stx.id}") } +// DOCEND 2 @StartableByRPC class AttachmentDemoFlow(private val otherSide: Party, @@ -132,6 +134,7 @@ class AttachmentDemoFlow(private val otherSide: Party, } } +// DOCSTART 1 fun recipient(rpc: CordaRPCOps, webPort: Int) { println("Waiting to receive transaction ...") val stx = rpc.internalVerifiedTransactionsFeed().updates.toBlocking().first() @@ -170,6 +173,7 @@ fun recipient(rpc: CordaRPCOps, webPort: Int) { println("Error: no attachments found in ${wtx.id}") } } +// DOCEND 1 private fun printHelp(parser: OptionParser) { println(""" diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt index 8070d86f29..097a33c5e8 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt @@ -609,6 +609,7 @@ class InterestRateSwap : Contract { override val participants: List get() = listOf(fixedLeg.fixedRatePayer, floatingLeg.floatingRatePayer) + // DOCSTART 1 override fun nextScheduledActivity(thisStateRef: StateRef, flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity? { val nextFixingOf = nextFixingOf() ?: return null @@ -616,6 +617,7 @@ class InterestRateSwap : Contract { val instant = suggestInterestRateAnnouncementTimeWindow(index = nextFixingOf.name, source = floatingLeg.indexSource, date = nextFixingOf.forDay).fromTime!! return ScheduledActivity(flowLogicRefFactory.create(FixingFlow.FixingRoleDecider::class.java, thisStateRef), instant) } + // DOCEND 1 override fun generateAgreement(notary: Party): TransactionBuilder { return InterestRateSwap().generateAgreement(floatingLeg, fixedLeg, calculation, common, oracle, notary) From 2a1a9c9d2009b87898e8f0bda41772e16b80143b Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Mon, 2 Oct 2017 09:08:59 +0100 Subject: [PATCH 054/185] Updates hello world tutorials for v1.0. --- docs/source/hello-world-contract.rst | 6 +- docs/source/hello-world-flow.rst | 34 ++++++---- docs/source/hello-world-running.rst | 77 ++++++++++++--------- docs/source/hello-world-state.rst | 24 +------ docs/source/resources/tutorial-state.png | Bin 196921 -> 159954 bytes docs/source/tut-two-party-flow.rst | 82 ++++++++++++++--------- 6 files changed, 124 insertions(+), 99 deletions(-) diff --git a/docs/source/hello-world-contract.rst b/docs/source/hello-world-contract.rst index d5e4de152f..7e367ff7eb 100644 --- a/docs/source/hello-world-contract.rst +++ b/docs/source/hello-world-contract.rst @@ -41,7 +41,8 @@ Just as every Corda state must implement the ``ContractState`` interface, every You can read about function declarations in Kotlin `here `_. -We can see that ``Contract`` expresses its constraints through a ``verify`` function that takes a transaction as input, and: +We can see that ``Contract`` expresses its constraints through a ``verify`` function that takes a transaction as input, +and: * Throws an ``IllegalArgumentException`` if it rejects the transaction proposal * Returns silently if it accepts the transaction proposal @@ -118,7 +119,6 @@ Let's write a contract that enforces these constraints. We'll do this by modifyi import net.corda.core.contracts.CommandData; import net.corda.core.contracts.Contract; import net.corda.core.transactions.LedgerTransaction; - import net.corda.core.crypto.SecureHash; import net.corda.core.identity.Party; import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; @@ -258,7 +258,5 @@ We've now written an ``IOUContract`` constraining the evolution of each ``IOUSta * The ``IOUState`` created by the issuance transaction must have a non-negative value, and the lender and borrower must be different entities -Before we move on, make sure you go back and modify ``IOUState`` to point to the new ``IOUContract`` class. - The final step in the creation of our CorDapp will be to write the ``IOUFlow`` that will allow a node to orchestrate the creation of a new ``IOUState`` on the ledger, while only sharing information on a need-to-know basis. diff --git a/docs/source/hello-world-flow.rst b/docs/source/hello-world-flow.rst index e3bd8db52b..351888fd0a 100644 --- a/docs/source/hello-world-flow.rst +++ b/docs/source/hello-world-flow.rst @@ -59,15 +59,20 @@ with the following: /** The flow logic is encapsulated within the call() method. */ @Suspendable override fun call() { - val notary = serviceHub.networkMapCache.getAnyNotary() + // We retrieve the notary identity from the network map. + val notary = serviceHub.networkMapCache.notaryIdentities[0] // We create a transaction builder val txBuilder = TransactionBuilder(notary = notary) + // We create the transaction components. + val outputState = IOUState(iouValue, ourIdentity, otherParty) + val outputContract = IOUContract::class.jvmName + val outputContractAndState = StateAndContract(outputState, outputContract) + val cmd = Command(IOUContract.Create(), ourIdentity.owningKey) + // We add the items to the builder. - val state = IOUState(iouValue, me, otherParty) - val cmd = Command(IOUContract.Create(), me.owningKey) - txBuilder.withItems(state, cmd) + txBuilder.withItems(outputContractAndState, cmd) // Verifying the transaction. txBuilder.verify(serviceHub) @@ -88,6 +93,7 @@ with the following: import com.template.contract.IOUContract; import com.template.state.IOUState; import net.corda.core.contracts.Command; + import net.corda.core.contracts.StateAndContract; import net.corda.core.flows.*; import net.corda.core.identity.Party; import net.corda.core.transactions.SignedTransaction; @@ -121,17 +127,21 @@ with the following: @Suspendable @Override public Void call() throws FlowException { - // We retrieve the required identities from the network map. - final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); + // We retrieve the notary identity from the network map. + final Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0); // We create a transaction builder. final TransactionBuilder txBuilder = new TransactionBuilder(); txBuilder.setNotary(notary); + // We create the transaction components. + IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty); + String outputContract = IOUContract.class.getName(); + StateAndContract outputContractAndState = new StateAndContract(outputState, outputContract); + Command cmd = new Command<>(new IOUContract.Create(), getOurIdentity().getOwningKey()); + // We add the items to the builder. - IOUState state = new IOUState(iouValue, me, otherParty); - Command cmd = new Command(new IOUContract.Create(), me.getOwningKey()); - txBuilder.withItems(state, cmd); + txBuilder.withItems(outputContractAndState, cmd); // Verifying the transaction. txBuilder.verify(getServiceHub()); @@ -200,7 +210,7 @@ the following transaction: So we'll need the following: -* The output ``IOUState`` +* The output ``IOUState`` and its associated contract * A ``Create`` command listing the IOU's lender as a signer The command we use pairs the ``IOUContract.Create`` command defined earlier with our public key. Including this command @@ -208,8 +218,8 @@ in the transaction makes us one of the transaction's required signers. We add these items to the transaction using the ``TransactionBuilder.withItems`` method, which takes a ``vararg`` of: -* ``ContractState`` or ``TransactionState`` objects, which are added to the builder as output states -* ``StateRef`` objects (references to the outputs of previous transactions), which are added to the builder as input +* ``StateAndContract`` or ``TransactionState`` objects, which are added to the builder as output states +* ``StateAndRef`` objects (references to the outputs of previous transactions), which are added to the builder as input state references * ``Command`` objects, which are added to the builder as commands * ``SecureHash`` objects, which are added to the builder as attachments diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index 6bc74a3117..7bf00e4771 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -25,30 +25,30 @@ Let's take a look at the nodes we're going to deploy. Open the project's ``build task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "O=Controller,OU=corda,L=London,C=UK" + networkMap "O=Controller,L=London,C=GB" node { - name "O=Controller,OU=corda,L=London,C=UK" + name "O=Controller,L=London,C=GB" advertisedServices = ["corda.notary.validating"] p2pPort 10002 rpcPort 10003 - cordapps = [] + cordapps = ["net.corda:corda-finance:$corda_release_version"] } node { - name "CN=NodeA,O=NodeA,L=London,C=UK" + name "O=PartyA,L=London,C=GB" advertisedServices = [] p2pPort 10005 rpcPort 10006 webPort 10007 - cordapps = [] + cordapps = ["net.corda:corda-finance:$corda_release_version"] rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] } node { - name "CN=NodeB,O=NodeB,L=New York,C=US" + name "O=PartyB,L=New York,C=US" advertisedServices = [] p2pPort 10008 rpcPort 10009 webPort 10010 - cordapps = [] + cordapps = ["net.corda:corda-finance:$corda_release_version"] rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] } } @@ -111,57 +111,72 @@ Now that our nodes are running, let's order one of them to create an IOU by kick app, we'd generally provide a web API sitting on top of our node. Here, for simplicity, we'll be interacting with the node via its built-in CRaSH shell. -Go to the terminal window displaying the CRaSH shell of Node A. Typing ``help`` will display a list of the available +Go to the terminal window displaying the CRaSH shell of PartyA. Typing ``help`` will display a list of the available commands. -We want to create an IOU of 100 with Node B. We start the ``IOUFlow`` by typing: +We want to create an IOU of 100 with PartyB. We start the ``IOUFlow`` by typing: .. container:: codeset .. code-block:: java - start IOUFlow arg0: 99, arg1: "NodeB" + start IOUFlow arg0: 99, arg1: "O=PartyB,L=New York,C=US" .. code-block:: kotlin - start IOUFlow iouValue: 99, otherParty: "NodeB" + start IOUFlow iouValue: 99, otherParty: "O=PartyB,L=New York,C=US" -Node A and Node B will automatically agree an IOU. If the flow worked, it should have led to the recording of a new IOU -in the vaults of both Node A and Node B. +PartyA and PartyB will automatically agree an IOU. If the flow worked, it should have led to the recording of a new IOU +in the vaults of both PartyA and PartyB. We can check the flow has worked by using an RPC operation to check the contents of each node's vault. Typing ``run`` will display a list of the available commands. We can examine the contents of a node's vault by running: -.. code:: python +.. container:: codeset - run vaultAndUpdates + .. code-block:: java -And we can also examine a node's transaction storage, by running: + run vaultQuery contractStateType: com.template.state.IOUState + + .. code-block:: kotlin + + run vaultQuery contractStateType: com.template.IOUState + +The vaults of PartyA and PartyB should both display the following output: .. code:: python - run verifiedTransactions - -The vaults of Node A and Node B should both display the following output: - -.. code:: python - - first: + states: - state: data: value: 99 - lender: "CN=NodeA,O=NodeA,L=London,C=GB" - borrower: "CN=NodeB,O=NodeB,L=New York,C=US" - contract: {} + lender: "C=GB,L=London,O=PartyA" + borrower: "C=US,L=New York,O=PartyB" participants: - - "CN=NodeA,O=NodeA,L=London,C=GB" - - "CN=NodeB,O=NodeB,L=New York,C=US" - notary: "O=Controller,OU=corda,L=London,C=GB,OU=corda.notary.validating" + - "C=GB,L=London,O=PartyA" + - "C=US,L=New York,O=PartyB" + contract: "com.template.contract.IOUContract" + notary: "C=GB,L=London,O=Controller,CN=corda.notary.validating" encumbrance: null + constraint: + attachmentId: "F578320232CAB87BB1E919F3E5DB9D81B7346F9D7EA6D9155DC0F7BA8E472552" ref: - txhash: "656A1BF64D5AEEC6F6C944E287F34EF133336F5FC2C5BFB9A0BFAE25E826125F" + txhash: "5CED068E790A347B0DD1C6BB5B2B463406807F95E080037208627565E6A2103B" index: 0 - second: "(observable)" + statesMetadata: + - ref: + txhash: "5CED068E790A347B0DD1C6BB5B2B463406807F95E080037208627565E6A2103B" + index: 0 + contractStateClassName: "com.template.state.IOUState" + recordedTime: 1506415268.875000000 + consumedTime: null + status: "UNCONSUMED" + notary: "C=GB,L=London,O=Controller,CN=corda.notary.validating" + lockId: null + lockUpdateTime: 1506415269.548000000 + totalStatesAvailable: -1 + stateTypes: "UNCONSUMED" + otherResults: [] Conclusion ---------- diff --git a/docs/source/hello-world-state.rst b/docs/source/hello-world-state.rst index 1677e3276a..dbcbe7c4b7 100644 --- a/docs/source/hello-world-state.rst +++ b/docs/source/hello-world-state.rst @@ -20,9 +20,6 @@ defined as follows: .. code-block:: kotlin interface ContractState { - // The contract that imposes constraints on how this state can evolve over time. - val contract: Contract - // The list of entities considered to have a stake in this state. val participants: List } @@ -38,13 +35,10 @@ If you do want to dive into Kotlin, there's an official `getting started guide `_, and a series of `Kotlin Koans `_. -We can see that the ``ContractState`` interface declares two properties: +We can see that the ``ContractState`` interface has a single field, ``participants``. ``participants`` is a list of +the entities for which this state is relevant. -* ``contract``: the contract controlling transactions involving this state -* ``participants``: the list of entities that have to approve state changes such as changing the state's notary or - upgrading the state's contract - -Beyond this, our state is free to define any properties, methods, helpers or inner classes it requires to accurately +Beyond this, our state is free to define any fields, methods, helpers or inner classes it requires to accurately represent a given class of shared facts on the ledger. ``ContractState`` also has several child interfaces that you may wish to implement depending on your state, such as @@ -74,7 +68,6 @@ define an ``IOUState``: class IOUState(val value: Int, val lender: Party, val borrower: Party) : ContractState { - override val contract = "net.corda.contract.TemplateContract" override val participants get() = listOf(lender, borrower) } @@ -83,7 +76,6 @@ define an ``IOUState``: package com.template.state; import com.google.common.collect.ImmutableList; - import com.template.contract.TemplateContract; import net.corda.core.contracts.ContractState; import net.corda.core.identity.AbstractParty; import net.corda.core.identity.Party; @@ -94,7 +86,6 @@ define an ``IOUState``: private final int value; private final Party lender; private final Party borrower; - private final TemplateContract contract = new TemplateContract(); public IOUState(int value, Party lender, Party borrower) { this.value = value; @@ -114,12 +105,6 @@ define an ``IOUState``: return borrower; } - @Override - // TODO: Once we've defined IOUContract, come back and update this. - public TemplateContract getContract() { - return contract; - } - @Override public List getParticipants() { return ImmutableList.of(lender, borrower); @@ -141,9 +126,6 @@ We've made the following changes: * Actions such as changing a state's contract or notary will require approval from all the ``participants`` -We've left ``IOUState``'s contract as ``TemplateContract`` for now. We'll update this once we've defined the -``IOUContract``. - Progress so far --------------- We've defined an ``IOUState`` that can be used to represent IOUs as shared facts on the ledger. As we've seen, states in diff --git a/docs/source/resources/tutorial-state.png b/docs/source/resources/tutorial-state.png index 11b0a0d385805999bf2330e8185e319e4a5c88d9..d621277ba9441bd8c6379d7ab2992f11bf8fa9e3 100644 GIT binary patch literal 159954 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 literal 196921 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 requiredSigners = ImmutableList.of(getOurIdentity().getOwningKey(), otherParty.getOwningKey()); + Command cmd = new Command<>(new IOUContract.Create(), requiredSigners); + // We add the items to the builder. - IOUState state = new IOUState(iouValue, me, otherParty); - List requiredSigners = ImmutableList.of(me.getOwningKey(), otherParty.getOwningKey()); - Command cmd = new Command(new IOUContract.Create(), requiredSigners); - txBuilder.withItems(state, cmd); + txBuilder.withItems(outputContractAndState, cmd); // Verifying the transaction. txBuilder.verify(getServiceHub()); @@ -70,20 +80,34 @@ In ``IOUFlow.java``/``IOUFlow.kt``, update ``IOUFlow.call`` as follows: // Signing the transaction. final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); - // Obtaining the counterparty's signature - final FlowSession otherSession = initiateFlow(otherParty) - final SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(signedTx, Collections.singleton(otherSession), CollectSignaturesFlow.Companion.tracker())); + // Creating a session with the other party. + FlowSession otherpartySession = initiateFlow(otherParty); + + // Obtaining the counterparty's signature. + SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow( + signedTx, ImmutableList.of(otherpartySession), CollectSignaturesFlow.tracker())); // Finalising the transaction. - subFlow(new FinalityFlow(fullySignedTx)); + subFlow(new FinalityFlow(signedTx)); return null; To make the borrower a required signer, we simply add the borrower's public key to the list of signers on the command. -``CollectSignaturesFlow``, meanwhile, takes a transaction signed by the flow initiator, and returns a transaction -signed by all the transaction's other required signers. We then pass this fully-signed transaction into -``FinalityFlow``. +We now need to communicate with the borrower to request their signature. Whenever you want to communicate with another +party in the context of a flow, you first need to establish a flow session with them. If the counterparty has a +``FlowLogic`` registered to respond to the ``FlowLogic`` initiating the session, a session will be established. All +communication between the two ``FlowLogic`` instances will then place as part of this session. + +Once we have a session with the borrower, we gather the borrower's signature using ``CollectSignaturesFlow``, which +takes: + +* A transaction signed by the flow initiator +* A list of flow-sessions between the flow initiator and the required signers + +And returns a transaction signed by all the required signers. + +We then pass this fully-signed transaction into ``FinalityFlow``. Creating the borrower's flow ---------------------------- @@ -101,10 +125,10 @@ In a new ``IOUFlowResponder.java`` file in Java, or within the ``App.kt`` file i ... @InitiatedBy(IOUFlow::class) - class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic() { + class IOUFlowResponder(val otherPartyFlow: FlowSession) : FlowLogic() { @Suspendable override fun call() { - val signTransactionFlow = object : SignTransactionFlow(otherPartySession, SignTransactionFlow.tracker()) { + val signTransactionFlow = object : SignTransactionFlow(otherPartyFlow, SignTransactionFlow.tracker()) { override fun checkTransaction(stx: SignedTransaction) = requireThat { val output = stx.tx.outputs.single().data "This must be an IOU transaction." using (output is IOUState) @@ -124,11 +148,7 @@ In a new ``IOUFlowResponder.java`` file in Java, or within the ``App.kt`` file i import co.paralleluniverse.fibers.Suspendable; import com.template.state.IOUState; import net.corda.core.contracts.ContractState; - import net.corda.core.flows.FlowException; - 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.core.flows.*; import net.corda.core.transactions.SignedTransaction; import net.corda.core.utilities.ProgressTracker; @@ -136,18 +156,18 @@ In a new ``IOUFlowResponder.java`` file in Java, or within the ``App.kt`` file i @InitiatedBy(IOUFlow.class) public class IOUFlowResponder extends FlowLogic { - private final FlowSession otherPartySession; + private final FlowSession otherPartyFlow; - public IOUFlowResponder(FlowSession otherPartySession) { - this.otherPartySession = otherPartySession; + public IOUFlowResponder(FlowSession otherPartyFlow) { + this.otherPartyFlow = otherPartyFlow; } @Suspendable @Override public Void call() throws FlowException { - class SignTxFlow extends SignTransactionFlow { - private signTxFlow(FlowSession otherPartySession, ProgressTracker progressTracker) { - super(otherPartySession, progressTracker); + class signTxFlow extends SignTransactionFlow { + private signTxFlow(FlowSession otherPartyFlow, ProgressTracker progressTracker) { + super(otherPartyFlow, progressTracker); } @Override @@ -162,7 +182,7 @@ In a new ``IOUFlowResponder.java`` file in Java, or within the ``App.kt`` file i } } - subFlow(new SignTxFlow(otherPartySession, SignTransactionFlow.Companion.tracker())); + subFlow(new signTxFlow(otherPartyFlow, SignTransactionFlow.Companion.tracker())); return null; } From f9614123968035cb325c6de0b84cf2718643f399 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Mon, 2 Oct 2017 09:09:16 +0100 Subject: [PATCH 055/185] Fixes a formatting issue. --- docs/source/key-concepts-contract-constraints.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/key-concepts-contract-constraints.rst b/docs/source/key-concepts-contract-constraints.rst index 6c3e536264..5518e45ff6 100644 --- a/docs/source/key-concepts-contract-constraints.rst +++ b/docs/source/key-concepts-contract-constraints.rst @@ -15,7 +15,7 @@ A typical constraint is the hash of the CorDapp JAR that contains the contract a include constraints that require specific signers of the JAR, or both the signer and the hash. Constraints can be specified when constructing a transaction; if unspecified, an automatic constraint is used. -``TransactionState``s have a ``constraint`` field that represents that state's attachment constraint. When a party +A ``TransactionState`` has a ``constraint`` field that represents that state's attachment constraint. When a party constructs a ``TransactionState`` without specifying the constraint parameter a default value (``AutomaticHashConstraint``) is used. This default will be automatically resolved to a specific ``HashAttachmentConstraint`` that contains the hash of the attachment which contains the contract of that @@ -80,8 +80,8 @@ attachment JAR. This allows for trusting of attachments from trusted entities. Limitations ----------- -``AttachmentConstraint``s are verified by running the ``AttachmentConstraint.isSatisfiedBy`` method. When this is called -it is provided only the relevant attachment by the transaction that is verifying it. +An ``AttachmentConstraint`` is verified by running the ``AttachmentConstraint.isSatisfiedBy`` method. When this is +called it is provided only the relevant attachment by the transaction that is verifying it. Testing ------- From 33e8105a2987bfe2ceafcdeb93f454cd5def584d Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Mon, 2 Oct 2017 10:50:39 +0100 Subject: [PATCH 056/185] CORDA-649: Second attempt to make PersistentNetworkMapCacheTest more stable (#1738) --- build.gradle | 1 - node/build.gradle | 1 - .../network/PersistentNetworkMapCacheTest.kt | 11 ++++----- .../kotlin/net/corda/testing/Eventually.kt | 24 +++++++++++++++++++ 4 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 testing/test-utils/src/main/kotlin/net/corda/testing/Eventually.kt diff --git a/build.gradle b/build.gradle index 63cce612c6..96cd49c445 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,6 @@ buildscript { ext.jersey_version = '2.25' ext.jolokia_version = '2.0.0-M3' ext.assertj_version = '3.6.1' - ext.kotlintest_version = '2.0.5' ext.slf4j_version = '1.7.25' ext.log4j_version = '2.7' ext.bouncycastle_version = constants.getProperty("bouncycastleVersion") diff --git a/node/build.gradle b/node/build.gradle index bcdc43d571..276efe01df 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -127,7 +127,6 @@ dependencies { // Unit testing helpers. testCompile "junit:junit:$junit_version" testCompile "org.assertj:assertj-core:${assertj_version}" - testCompile "io.kotlintest:kotlintest:${kotlintest_version}" testCompile project(':test-utils') testCompile project(':client:jfx') testCompile project(':finance') 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 4da002b539..465fb72229 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,8 +1,6 @@ package net.corda.node.services.network import co.paralleluniverse.fibers.Suspendable -import io.kotlintest.eventually -import io.kotlintest.milliseconds import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession import net.corda.core.flows.InitiatedBy @@ -17,8 +15,8 @@ import net.corda.testing.* import net.corda.testing.node.NodeBasedTest import org.assertj.core.api.Assertions.assertThat import org.junit.Before -import org.junit.Ignore import org.junit.Test +import java.time.Duration import kotlin.test.assertEquals import kotlin.test.assertFails import kotlin.test.assertTrue @@ -114,7 +112,6 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { assertFails { startNode(CHARLIE.name, noNetworkMap = true).getOrThrow(2.seconds) } } - @Ignore("Unstable test that needs more work") @Test fun `new node joins network without network map started`() { @@ -146,9 +143,11 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { // This is prediction of the longest time it will take to get the cluster into a stable state such that further // testing can be performed upon it - val maxInstabilityInterval = BRIDGE_RETRY_MS * allTheStartedNodesPopulation.size * 2 + val maxInstabilityInterval = BRIDGE_RETRY_MS * allTheStartedNodesPopulation.size * 30 + logger.info("Instability interval is set to: $maxInstabilityInterval ms") - eventually(maxInstabilityInterval.milliseconds) { + // TODO: Re-visit this sort of re-try for stable cluster once network map redesign is finished. + eventually(Duration.ofMillis(maxInstabilityInterval)) { logger.info("Checking connectivity") checkConnectivity(listOf(otherNodes[0], nms)) // Checks connectivity from A to NMS. logger.info("Loading caches") diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/Eventually.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/Eventually.kt new file mode 100644 index 0000000000..b3a4588609 --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/Eventually.kt @@ -0,0 +1,24 @@ +package net.corda.testing + +import java.time.Duration + +/** + * Ideas borrowed from "io.kotlintest" with some improvements made + * This is meant for use from Kotlin code use only mainly due to it's inline/reified nature + */ +inline fun eventually(duration: Duration, f: () -> R): R { + val end = System.nanoTime() + duration.toNanos() + var times = 0 + while (System.nanoTime() < end) { + try { + return f() + } catch (e: Throwable) { + when(e) { + is E -> {}// ignore and continue + else -> throw e // unexpected exception type - rethrow + } + } + times++ + } + throw AssertionError("Test failed after $duration; attempted $times times") +} \ No newline at end of file From 0ec75f009779c1c0d1b2479079b05c8a81114523 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Mon, 2 Oct 2017 14:53:33 +0100 Subject: [PATCH 057/185] Fixes java-docs breaking typo. --- .../kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt index 9ef8101bdd..c38368aaf6 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt @@ -20,8 +20,8 @@ import java.util.* class ResolveTransactionsFlow(private val txHashes: Set, private val otherSide: FlowSession) : FlowLogic>() { /** - * Resolves and validates the dependencies of the specified [signedTransaction]. Fetches the attachments, but does - * *not* validate or store the [signedTransaction] itself. + * Resolves and validates the dependencies of the specified [SignedTransaction]. Fetches the attachments, but does + * *not* validate or store the [SignedTransaction] itself. * * @return a list of verified [SignedTransaction] objects, in a depth-first order. */ From ef9b54327ada1c4a3bd6cfaae09fc8088cbfb6e6 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Mon, 2 Oct 2017 15:01:19 +0100 Subject: [PATCH 058/185] Checking out the latest milestone will no longer be required. --- docs/source/getting-set-up.rst | 7 ------- docs/source/hello-world-template.rst | 8 +------- docs/source/troubleshooting.rst | 7 ------- docs/source/tutorial-cordapp.rst | 10 ---------- 4 files changed, 1 insertion(+), 31 deletions(-) diff --git a/docs/source/getting-set-up.rst b/docs/source/getting-set-up.rst index 7a9b09b140..95bb99bfc9 100644 --- a/docs/source/getting-set-up.rst +++ b/docs/source/getting-set-up.rst @@ -76,8 +76,6 @@ Download a sample project 1. Open a command prompt 2. Clone the CorDapp example repo by running ``git clone https://github.com/corda/cordapp-example`` 3. Move into the cordapp-example folder by running ``cd cordapp-example`` -4. Retrieve a list of all the releases by running ``git branch -a --list`` -5. Check out the latest milestone release by running ``git checkout release-MX`` (where "X" is the latest milestone) Run from the command prompt ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -125,8 +123,6 @@ Download a sample project 1. Open a terminal 2. Clone the CorDapp example repo by running ``git clone https://github.com/corda/cordapp-example`` 3. Move into the cordapp-example folder by running ``cd cordapp-example`` -4. Retrieve a list of all the releases by running ``git branch -a --list`` -5. Check out the latest milestone release by running ``git checkout release-MX`` (where "X" is the latest milestone) Run from the terminal ^^^^^^^^^^^^^^^^^^^^^ @@ -165,9 +161,6 @@ And a simple example CorDapp for you to explore basic concepts is available here You can clone these repos to your local machine by running the command ``git clone [repo URL]``. -By default, these repos will be on the unstable ``master`` branch. You should check out the latest milestone release -instead by running ``git checkout release-MX`` (where “X” is the latest milestone). - Next steps ---------- The best way to check that everything is working fine is by taking a deeper look at the diff --git a/docs/source/hello-world-template.rst b/docs/source/hello-world-template.rst index acb131dc6f..5a6e3920a1 100644 --- a/docs/source/hello-world-template.rst +++ b/docs/source/hello-world-template.rst @@ -22,7 +22,7 @@ Downloading the template ------------------------ Open a terminal window in the directory where you want to download the CorDapp template, and run the following commands: -.. code-block:: text +.. code-block:: bash # Clone the template from GitHub: git clone https://github.com/corda/cordapp-template-java.git ; cd cordapp-template-java @@ -31,12 +31,6 @@ Open a terminal window in the directory where you want to download the CorDapp t git clone https://github.com/corda/cordapp-template-kotlin.git ; cd cordapp-template-kotlin - # Retrieve a list of the stable Milestone branches using: - git branch -a --list *release-M* - - # Check out the Milestone branch with the latest version number: - git checkout release-M[*version number*] ; git pull - Template structure ------------------ We can write our CorDapp in either Java or Kotlin, and will be providing the code in both languages throughout. To diff --git a/docs/source/troubleshooting.rst b/docs/source/troubleshooting.rst index 434cdbe579..a07980119c 100644 --- a/docs/source/troubleshooting.rst +++ b/docs/source/troubleshooting.rst @@ -1,13 +1,6 @@ Troubleshooting =============== -Milestone releases ------------------- - -When you clone the corda or cordapp-template repos, they will default to the master branch. The master branch is being continuously developed upon, and its features may not align with the state of Corda as described in the docs. Additionally, the master branch of the CorDapp template may break in response to changes in the main corda repo. - -When developing on Corda, you should always check out the latest milestone (i.e. stable) branch instead. For example, to check out milestone 0, you'd run ``git checkout release-M0``. - Java issues ----------- diff --git a/docs/source/tutorial-cordapp.rst b/docs/source/tutorial-cordapp.rst index 3eec271545..ed203a66e5 100644 --- a/docs/source/tutorial-cordapp.rst +++ b/docs/source/tutorial-cordapp.rst @@ -36,16 +36,6 @@ We need to download the example CorDapp from GitHub. * Change directories to the freshly cloned repo: ``cd cordapp-example`` -* We want to work off the latest Milestone release - - * To enumerate all the Milestone releases, run: ``git tag`` - - * Check out the latest (highest-numbered) Milestone release using: ``git checkout [tag_name]`` - - Where ``tag_name`` is the name of the tag you wish to checkout - - * Gradle will grab all the required dependencies for you from `Maven `_ - .. note:: If you wish to build off the latest, unstable version of the codebase, follow the instructions in :doc:`building against Master ` instead. From 383eb2bef8e930baf780130fe45aa9d8d00bbdd7 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Mon, 2 Oct 2017 15:59:31 +0100 Subject: [PATCH 059/185] CORDA-540: Fix to make IRS demo code pass in AMQP mode (#1769) * CORDA-540: Do not use concrete instance of an ArrayList as wire representation of it may be different * CORDA-540: Make data structures suitable for AMQP serialization * CORDA-540: Use "name" instead of "toString()" Classes like "net.corda.finance.contracts.DayCountBasisDay" override "toString()" which leads to error behaviour --- .../nodeapi/internal/serialization/amqp/EnumSerializer.kt | 4 ++-- .../src/main/kotlin/net/corda/irs/contract/IRSUtils.kt | 5 +++-- .../src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt | 4 +--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt index 6ac728783c..a9e6d04cd4 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt @@ -30,9 +30,9 @@ class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: Seria override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any { val enumName = (obj as List<*>)[0] as String val enumOrd = obj[1] as Int - val fromOrd = type.asClass()!!.enumConstants[enumOrd] + val fromOrd = type.asClass()!!.enumConstants[enumOrd] as Enum<*>? - if (enumName != fromOrd?.toString()) { + if (enumName != fromOrd?.name) { throw NotSerializableException("Deserializing obj as enum $type with value $enumName.$enumOrd but " + "ordinality has changed") } diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt index b8950ee871..fb7e367a3c 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt @@ -22,9 +22,9 @@ open class RatioUnit(val value: BigDecimal) { // TODO: Discuss this type } /** - * A class to reprecent a percentage in an unambiguous way. + * A class to represent a percentage in an unambiguous way. */ -open class PercentageRatioUnit(percentageAsString: String) : RatioUnit(BigDecimal(percentageAsString).divide(BigDecimal("100"))) { +open class PercentageRatioUnit(val percentageAsString: String) : RatioUnit(BigDecimal(percentageAsString).divide(BigDecimal("100"))) { override fun toString() = value.times(BigDecimal(100)).toString() + "%" } @@ -39,6 +39,7 @@ val String.percent: PercentageRatioUnit get() = PercentageRatioUnit(this) * Parent of the Rate family. Used to denote fixed rates, floating rates, reference rates etc. */ @JsonIgnoreProperties(ignoreUnknown = true) +@CordaSerializable open class Rate(val ratioUnit: RatioUnit? = null) { override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt index c2370a8ddf..4f148f01f6 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt @@ -5,7 +5,6 @@ import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.isFulfilledBy import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowSession import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable @@ -17,7 +16,6 @@ import net.corda.finance.contracts.Fix import net.corda.finance.contracts.FixOf import net.corda.irs.flows.RatesFixFlow.FixOutOfRange import java.math.BigDecimal -import java.util.* import java.util.function.Predicate // This code is unit tested in NodeInterestRates.kt @@ -100,7 +98,7 @@ open class RatesFixFlow(protected val tx: TransactionBuilder, override fun call(): Fix { val oracleSession = initiateFlow(oracle) // TODO: add deadline to receive - val resp = oracleSession.sendAndReceive>(QueryRequest(listOf(fixOf))) + val resp = oracleSession.sendAndReceive>(QueryRequest(listOf(fixOf))) return resp.unwrap { val fix = it.first() From 12982b30346685b0d4289555e017217ca8c145ca Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Mon, 2 Oct 2017 15:59:55 +0100 Subject: [PATCH 060/185] Move dbCloser to MockNode, the only thing that uses it. (#1512) --- .../src/main/kotlin/net/corda/node/internal/AbstractNode.kt | 6 +----- .../src/main/kotlin/net/corda/testing/node/MockNode.kt | 6 ++++++ 2 files changed, 7 insertions(+), 5 deletions(-) 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 ea492d2a64..b0293ed838 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -146,7 +146,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, protected lateinit var network: MessagingService protected val runOnStop = ArrayList<() -> Any?>() protected lateinit var database: CordaPersistence - protected var dbCloser: (() -> Any?)? = null lateinit var cordappProvider: CordappProviderImpl protected val cordappLoader by lazy { makeCordappLoader() } @@ -476,10 +475,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, database.transaction { log.info("Connected to ${database.dataSource.connection.metaData.databaseProductName} database.") } - this.database::close.let { - dbCloser = it - runOnStop += it - } + runOnStop += database::close return database.transaction { insideTransaction() } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index 63dd79f8d8..9ef99e42e8 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -241,6 +241,12 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, // It is used from the network visualiser tool. @Suppress("unused") val place: WorldMapLocation get() = findMyLocation()!! + private var dbCloser: (() -> Any?)? = null + override fun initialiseDatabasePersistence(insideTransaction: () -> T) = super.initialiseDatabasePersistence { + dbCloser = database::close + insideTransaction() + } + fun disableDBCloseOnStop() { runOnStop.remove(dbCloser) } From 9a1601144834672bd7448489e0011f37cc7521e5 Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Fri, 29 Sep 2017 15:45:19 +0100 Subject: [PATCH 061/185] * Move CompositeSignaturesWithKeys into net.corda.core.crypto package. * Rename and move CordaPluginRegistry to reflect its real purpose now. * Docs: docsite improvements * Remove discussion of webserver from 'writing a cordapp' page. * Fixup some flow docs. * Add a couple more package descriptions. * Review comments - always apply default whitelist and no longer load it via ServiceLoader * Added wording about renaming services resource file --- .../kotlin/net/corda/core/cordapp/Cordapp.kt | 6 +-- .../corda/core/crypto/CompositeSignature.kt | 1 - .../CompositeSignaturesWithKeys.kt | 3 +- .../core/internal/cordapp/CordappImpl.kt | 6 +-- .../corda/core/node/CordaPluginRegistry.kt | 19 --------- .../SerializationCustomization.kt | 6 --- .../serialization/SerializationWhitelist.kt | 16 ++++++++ .../corda/core/crypto/CompositeKeyTests.kt | 3 +- docs/packages.md | 8 ++-- docs/source/changelog.rst | 8 ++-- .../net/corda/docs/ClientRpcTutorial.kt | 15 +++---- docs/source/flow-state-machines.rst | 37 ++--------------- docs/source/writing-cordapps.rst | 40 +------------------ .../contracts/isolated/IsolatedPlugin.kt | 5 --- .../net.corda.core.node.CordaPluginRegistry | 1 - .../serialization/AMQPSerializationScheme.kt | 17 +++----- .../serialization/CordaClassResolver.kt | 2 +- .../serialization/DefaultKryoCustomizer.kt | 19 ++++++--- .../serialization/DefaultWhitelist.kt | 16 +++----- .../KryoSerializationCustomization.kt | 17 -------- .../net.corda.core.node.CordaPluginRegistry | 2 - .../net/corda/node/internal/AbstractNode.kt | 7 ++-- .../net/corda/node/internal/NodeStartup.kt | 8 +--- .../node/internal/cordapp/CordappLoader.kt | 9 +++-- .../internal/cordapp/CordappLoaderTest.kt | 2 - ...webserver.services.WebServerPluginRegistry | 1 - .../net/corda/vega/plugin/SimmPlugin.kt | 4 +- .../corda/vega/plugin/SimmPluginRegistry.kt | 39 ++++++++---------- .../net.corda.core.node.CordaPluginRegistry | 2 - ....core.serialization.SerializationWhitelist | 1 + .../kotlin/net/corda/testing/node/MockNode.kt | 26 +++++++----- 31 files changed, 114 insertions(+), 232 deletions(-) rename core/src/main/kotlin/net/corda/core/crypto/{composite => }/CompositeSignaturesWithKeys.kt (81%) delete mode 100644 core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt delete mode 100644 core/src/main/kotlin/net/corda/core/serialization/SerializationCustomization.kt create mode 100644 core/src/main/kotlin/net/corda/core/serialization/SerializationWhitelist.kt delete mode 100644 finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/IsolatedPlugin.kt delete mode 100644 finance/isolated/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry delete mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoSerializationCustomization.kt delete mode 100644 node-api/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry delete mode 100644 samples/simm-valuation-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry create mode 100644 samples/simm-valuation-demo/src/main/resources/META-INF/services/net.corda.core.serialization.SerializationWhitelist 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 ea9557d7c9..32c011866e 100644 --- a/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt +++ b/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt @@ -1,8 +1,8 @@ package net.corda.core.cordapp import net.corda.core.flows.FlowLogic -import net.corda.core.node.CordaPluginRegistry import net.corda.core.schemas.MappedSchema +import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import java.net.URL @@ -19,7 +19,7 @@ import java.net.URL * @property rpcFlows List of RPC initiable flows classes * @property schedulableFlows List of flows startable by the scheduler * @property servies List of RPC services - * @property plugins List of Corda plugin registries + * @property serializationWhitelists List of Corda plugin registries * @property customSchemas List of custom schemas * @property jarPath The path to the JAR for this CorDapp */ @@ -30,7 +30,7 @@ interface Cordapp { val rpcFlows: List>> val schedulableFlows: List>> val services: List> - val plugins: List + val serializationWhitelists: List val customSchemas: Set val jarPath: URL val cordappClasses: List diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt index 8317a11a64..b359e31991 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt @@ -1,6 +1,5 @@ package net.corda.core.crypto -import net.corda.core.crypto.composite.CompositeSignaturesWithKeys import net.corda.core.serialization.deserialize import java.io.ByteArrayOutputStream import java.security.* diff --git a/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeSignaturesWithKeys.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt similarity index 81% rename from core/src/main/kotlin/net/corda/core/crypto/composite/CompositeSignaturesWithKeys.kt rename to core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt index f9c8da8ab3..0ea62b1f0e 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeSignaturesWithKeys.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt @@ -1,6 +1,5 @@ -package net.corda.core.crypto.composite +package net.corda.core.crypto -import net.corda.core.crypto.TransactionSignature import net.corda.core.serialization.CordaSerializable /** 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 8cfda77d42..e03e83f92c 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 @@ -2,8 +2,8 @@ package net.corda.core.internal.cordapp import net.corda.core.cordapp.Cordapp import net.corda.core.flows.FlowLogic -import net.corda.core.node.CordaPluginRegistry import net.corda.core.schemas.MappedSchema +import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import java.io.File import java.net.URL @@ -14,7 +14,7 @@ data class CordappImpl( override val rpcFlows: List>>, override val schedulableFlows: List>>, override val services: List>, - override val plugins: List, + override val serializationWhitelists: List, override val customSchemas: Set, override val jarPath: URL) : Cordapp { override val name: String = File(jarPath.toURI()).name.removeSuffix(".jar") @@ -24,5 +24,5 @@ data class CordappImpl( * * TODO: Also add [SchedulableFlow] as a Cordapp class */ - override val cordappClasses = ((rpcFlows + initiatedFlows + services + plugins.map { javaClass }).map { it.name } + contractClassNames) + override val cordappClasses = ((rpcFlows + initiatedFlows + services + serializationWhitelists.map { javaClass }).map { it.name } + contractClassNames) } diff --git a/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt b/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt deleted file mode 100644 index 600f548a80..0000000000 --- a/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt +++ /dev/null @@ -1,19 +0,0 @@ -package net.corda.core.node - -import net.corda.core.serialization.SerializationCustomization - -/** - * Implement this interface on a class advertised in a META-INF/services/net.corda.core.node.CordaPluginRegistry file - * to extend a Corda node with additional application services. - */ -abstract class CordaPluginRegistry { - /** - * Optionally whitelist types for use in object serialization, as we lock down the types that can be serialized. - * - * For example, if you add a new [net.corda.core.contracts.ContractState] it needs to be whitelisted. You can do that - * either by adding the [net.corda.core.serialization.CordaSerializable] annotation or via this method. - ** - * @return true if you register types, otherwise you will be filtered out of the list of plugins considered in future. - */ - open fun customizeSerialization(custom: SerializationCustomization): Boolean = false -} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomization.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomization.kt deleted file mode 100644 index 91c5dea265..0000000000 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomization.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.corda.core.serialization - -interface SerializationCustomization { - fun addToWhitelist(vararg types: Class<*>) -} - diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationWhitelist.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationWhitelist.kt new file mode 100644 index 0000000000..0c2be0c16b --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationWhitelist.kt @@ -0,0 +1,16 @@ +package net.corda.core.serialization + +/** + * Provide a subclass of this via the [java.util.ServiceLoader] mechanism to be able to whitelist types for + * serialisation that you cannot otherwise annotate. The name of the class must appear in a text file on the + * classpath under the path META-INF/services/net.corda.core.serialization.SerializationWhitelist + */ +interface SerializationWhitelist { + /** + * Optionally whitelist types for use in object serialization, as we lock down the types that can be serialized. + * + * For example, if you add a new [net.corda.core.contracts.ContractState] it needs to be whitelisted. You can do that + * either by adding the [net.corda.core.serialization.CordaSerializable] annotation or via this method. + */ + val whitelist: List> +} \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt index 0b7c6187e9..80124d722d 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt @@ -1,13 +1,12 @@ package net.corda.core.crypto import net.corda.core.crypto.CompositeKey.NodeAndWeight -import net.corda.core.crypto.composite.CompositeSignaturesWithKeys import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.cert import net.corda.core.internal.declaredField import net.corda.core.internal.div import net.corda.core.serialization.serialize import net.corda.core.utilities.OpaqueBytes -import net.corda.core.internal.cert import net.corda.core.utilities.toBase58String import net.corda.node.utilities.* import net.corda.testing.TestDependencyInjectionBase diff --git a/docs/packages.md b/docs/packages.md index 2c2090e790..eb039e5237 100644 --- a/docs/packages.md +++ b/docs/packages.md @@ -24,6 +24,10 @@ RPC client interface to Corda, for use both by user-facing client and integratio Internal, do not use. These APIs and implementations which are currently being revised and are subject to future change. +# Package net.corda.core + +Exception types and some utilities for working with observables and futures. + # Package net.corda.core.cordapp This package contains the interface to CorDapps from within a node. A CorDapp can access its own context by using @@ -60,10 +64,6 @@ processes such as handling fixing of interest rate swaps. Data classes which model different forms of identity (potentially with supporting evidence) for legal entities and services. -# Package net.corda.core.internal - -Internal, do not use. These APIs and implementations which are currently being revised and are subject to future change. - # Package net.corda.core.messaging Data types used by the Corda messaging layer to manage state of messaging and sessions between nodes. diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index c32cc9913a..696507c047 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -65,9 +65,11 @@ Release 1.0 * About half of the code in test-utils has been moved to a new module ``node-driver``, and the test scope modules are now located in a ``testing`` directory. -* Removed `requireSchemas` CordaPluginRegistry configuration item. - Custom schemas are now automatically located using classpath scanning for deployed CorDapps. - Improved support for testing custom schemas in MockNode and MockServices using explicit registration. +* CordaPluginRegistry has been renamed to SerializationWhitelist and moved to the net.corda.core.serialization + package. The API for whitelisting types that can't be annotated was slightly simplified. This class used to contain + many things, but as we switched to annotations and classpath scanning over time it hollowed out until this was + the only functionality left. You also need to rename your services resource file to the new class name. + An associated property on ``MockNode`` was renamed from ``testPluginRegistries`` to ``testSerializationWhitelists``. * Contract Upgrades: deprecated RPC authorisation / deauthorisation API calls in favour of equivalent flows in ContractUpgradeFlow. Implemented contract upgrade persistence using JDBC backed persistent map. 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/ClientRpcTutorial.kt index bc21a3b5f2..bf6b46be6e 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/ClientRpcTutorial.kt @@ -4,9 +4,8 @@ import net.corda.core.contracts.Amount import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow import net.corda.core.messaging.vaultQueryBy -import net.corda.core.node.CordaPluginRegistry import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.SerializationCustomization +import net.corda.core.serialization.SerializationWhitelist import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow @@ -16,9 +15,9 @@ import net.corda.finance.flows.CashExitFlow import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.nodeapi.internal.ServiceInfo import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.User +import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.ALICE import net.corda.testing.DUMMY_NOTARY import net.corda.testing.driver.driver @@ -142,12 +141,8 @@ data class ExampleRPCValue(val foo: String) @CordaSerializable data class ExampleRPCValue2(val bar: Int) -class ExampleRPCCordaPluginRegistry : CordaPluginRegistry() { - override fun customizeSerialization(custom: SerializationCustomization): Boolean { - // Add classes like this. - custom.addToWhitelist(ExampleRPCValue::class.java) - // You should return true, otherwise your plugin will be ignored for registering classes with Kryo. - return true - } +class ExampleRPCSerializationWhitelist : SerializationWhitelist { + // Add classes like this. + override val whitelist = listOf(ExampleRPCValue::class.java) } // END 7 diff --git a/docs/source/flow-state-machines.rst b/docs/source/flow-state-machines.rst index bc7e484ae1..c0946f07a6 100644 --- a/docs/source/flow-state-machines.rst +++ b/docs/source/flow-state-machines.rst @@ -277,40 +277,9 @@ will propagate to the other side. Any other exception will not propagate. Taking a step back, we mentioned that the other side has to accept the session request for there to be a communication channel. A node accepts a session request if it has registered the flow type (the fully-qualified class name) that is -making the request - each session initiation includes the initiating flow type. The registration is done by a CorDapp -which has made available the particular flow communication, using ``PluginServiceHub.registerServiceFlow``. This method -specifies a flow factory for generating the counter-flow to any given initiating flow. If this registration doesn't exist -then no further communication takes place and the initiating flow ends with an exception. - -Going back to our buyer and seller flows, we need a way to initiate communication between the two. This is typically done -with one side started manually using the ``startFlowDynamic`` RPC and this initiates the counter-flow on the other side. -In this case it doesn't matter which flow is the initiator and which is the initiated. If we choose the seller side as -the initiator then the buyer side would need to register their flow, perhaps with something like: - -.. container:: codeset - - .. sourcecode:: kotlin - - class TwoPartyTradeFlowPlugin : CordaPluginRegistry() { - override val servicePlugins = listOf(Function(TwoPartyTradeFlowService::Service)) - } - - object TwoPartyTradeFlowService { - class Service(services: PluginServiceHub) { - init { - services.registerServiceFlow(TwoPartyTradeFlow.Seller::class.java) { - TwoPartyTradeFlow.Buyer( - it, - notary = services.networkMapCache.notaryIdentities[0].party, - acceptablePrice = TODO(), - typeToBuy = TODO()) - } - } - } - } - -This is telling the buyer node to fire up an instance of ``TwoPartyTradeFlow.Buyer`` (the code in the lambda) when -they receive a message from the initiating seller side of the flow (``TwoPartyTradeFlow.Seller::class.java``). +making the request - each session initiation includes the initiating flow type. The *initiated* (server) flow must name the +*initiating* (client) flow using the ``@InitiatedBy`` annotation and passing the class name that will be starting the +flow session as the annotation parameter. .. _subflows: diff --git a/docs/source/writing-cordapps.rst b/docs/source/writing-cordapps.rst index 9200cb8fe5..55eda0e091 100644 --- a/docs/source/writing-cordapps.rst +++ b/docs/source/writing-cordapps.rst @@ -44,7 +44,7 @@ The ``src`` directory of the Template CorDapp, where we define our CorDapp's sou │ └── resources │ ├── META-INF │ │ └── services - │ │ ├── net.corda.core.node.CordaPluginRegistry + │ │ ├── net.corda.core.serialization.SerializationWhitelist │ │ └── net.corda.webserver.services.WebServerPluginRegistry │ ├── certificates │ │ ├── sslkeystore.jks @@ -58,40 +58,4 @@ The ``src`` directory of the Template CorDapp, where we define our CorDapp's sou └── com └── template └── contract - └── TemplateTests.java - -Defining plugins ----------------- -Your CorDapp may need to define two types of plugins: - -* ``CordaPluginRegistry`` subclasses, which define additional serializable classes -* ``WebServerPluginRegistry`` subclasses, which define the APIs and static web content served by your CorDapp - -The fully-qualified class path of each ``CordaPluginRegistry`` subclass must then be added to the -``net.corda.core.node.CordaPluginRegistry`` file in the CorDapp's ``resources/META-INF/services`` folder. Meanwhile, -the fully-qualified class path of each ``WebServerPluginRegistry`` subclass must be added to the -``net.corda.webserver.services.WebServerPluginRegistry`` file, again in the CorDapp's ``resources/META-INF/services`` -folder. - -The ``CordaPluginRegistry`` class defines the following: - -* ``customizeSerialization``, which can be overridden to provide a list of the classes to be whitelisted for object - serialisation, over and above those tagged with the ``@CordaSerializable`` annotation. See :doc:`serialization` - -The ``WebServerPluginRegistry`` class defines the following: - -* ``webApis``, which can be overridden to return a list of JAX-RS annotated REST access classes. These classes will be - constructed by the bundled web server and must have a single argument constructor taking a ``CordaRPCOps`` object. - This will allow the API to communicate with the node process via the RPC interface. These web APIs will not be - available if the bundled web server is not started - -* ``staticServeDirs``, which can be overridden to map static web content to virtual paths and allow simple web demos to - be distributed within the CorDapp jars. This static content will not be available if the bundled web server is not - started - -* ``customizeJSONSerialization``, which can be overridden to register custom JSON serializers if required by the REST api. - - * The static web content itself should be placed inside the ``src/main/resources`` directory - -To learn about how to use gradle to build your cordapp against Corda and generate an artifact please read -:doc:`cordapp-build-systems`. \ No newline at end of file + └── TemplateTests.java \ No newline at end of file diff --git a/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/IsolatedPlugin.kt b/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/IsolatedPlugin.kt deleted file mode 100644 index 61e0288d88..0000000000 --- a/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/IsolatedPlugin.kt +++ /dev/null @@ -1,5 +0,0 @@ -package net.corda.finance.contracts.isolated - -import net.corda.core.node.CordaPluginRegistry - -class IsolatedPlugin : CordaPluginRegistry() \ No newline at end of file diff --git a/finance/isolated/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry b/finance/isolated/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry deleted file mode 100644 index 759dc08195..0000000000 --- a/finance/isolated/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry +++ /dev/null @@ -1 +0,0 @@ -net.corda.finance.contracts.isolated.IsolatedPlugin \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt index 6368a416a0..bbc3be72a8 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt @@ -2,7 +2,6 @@ package net.corda.nodeapi.internal.serialization -import net.corda.core.node.CordaPluginRegistry import net.corda.core.serialization.* import net.corda.core.utilities.ByteSequence import net.corda.nodeapi.internal.serialization.amqp.AmqpHeaderV1_0 @@ -14,12 +13,6 @@ import java.util.concurrent.ConcurrentHashMap val AMQP_ENABLED get() = SerializationDefaults.P2P_CONTEXT.preferredSerializationVersion == AmqpHeaderV1_0 -class AMQPSerializationCustomization(val factory: SerializerFactory) : SerializationCustomization { - override fun addToWhitelist(vararg types: Class<*>) { - factory.addToWhitelist(*types) - } -} - fun SerializerFactory.addToWhitelist(vararg types: Class<*>) { require(types.toSet().size == types.size) { val duplicates = types.toMutableList() @@ -33,12 +26,12 @@ fun SerializerFactory.addToWhitelist(vararg types: Class<*>) { abstract class AbstractAMQPSerializationScheme : SerializationScheme { internal companion object { - private val pluginRegistries: List by lazy { - ServiceLoader.load(CordaPluginRegistry::class.java, this::class.java.classLoader).toList() + private val serializationWhitelists: List by lazy { + ServiceLoader.load(SerializationWhitelist::class.java, this::class.java.classLoader).toList() + DefaultWhitelist } fun registerCustomSerializers(factory: SerializerFactory) { - factory.apply { + with(factory) { register(net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer) register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(this)) register(net.corda.nodeapi.internal.serialization.amqp.custom.X500NameSerializer) @@ -67,8 +60,8 @@ abstract class AbstractAMQPSerializationScheme : SerializationScheme { register(net.corda.nodeapi.internal.serialization.amqp.custom.BitSetSerializer(this)) register(net.corda.nodeapi.internal.serialization.amqp.custom.EnumSetSerializer(this)) } - val customizer = AMQPSerializationCustomization(factory) - pluginRegistries.forEach { it.customizeSerialization(customizer) } + for (whitelistProvider in serializationWhitelists) + factory.addToWhitelist(*whitelistProvider.whitelist.toTypedArray()) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt index 45f98cbda9..91573ed24e 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt @@ -168,7 +168,7 @@ class GlobalTransientClassWhiteList(val delegate: ClassWhitelist) : MutableClass } /** - * A whitelist that can be customised via the [net.corda.core.node.CordaPluginRegistry], since implements [MutableClassWhitelist]. + * A whitelist that can be customised via the [net.corda.core.node.SerializationWhitelist], since implements [MutableClassWhitelist]. */ class TransientClassWhiteList(val delegate: ClassWhitelist) : MutableClassWhitelist, ClassWhitelist by delegate { val whitelist: MutableSet = Collections.synchronizedSet(mutableSetOf()) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt index 75274a810d..d4a6f11ee2 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt @@ -15,7 +15,7 @@ import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.PrivacySalt import net.corda.core.crypto.CompositeKey import net.corda.core.identity.PartyAndCertificate -import net.corda.core.node.CordaPluginRegistry +import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SerializedBytes import net.corda.core.transactions.NotaryChangeWireTransaction @@ -49,8 +49,8 @@ import java.util.* import kotlin.collections.ArrayList object DefaultKryoCustomizer { - private val pluginRegistries: List by lazy { - ServiceLoader.load(CordaPluginRegistry::class.java, this.javaClass.classLoader).toList() + private val serializationWhitelists: List by lazy { + ServiceLoader.load(SerializationWhitelist::class.java, this.javaClass.classLoader).toList() + DefaultWhitelist } fun customize(kryo: Kryo): Kryo { @@ -122,8 +122,17 @@ object DefaultKryoCustomizer { register(java.lang.invoke.SerializedLambda::class.java) register(ClosureSerializer.Closure::class.java, CordaClosureBlacklistSerializer) - val customization = KryoSerializationCustomization(this) - pluginRegistries.forEach { it.customizeSerialization(customization) } + for (whitelistProvider in serializationWhitelists) { + val types = whitelistProvider.whitelist + require(types.toSet().size == types.size) { + val duplicates = types.toMutableList() + types.toSet().forEach { duplicates -= it } + "Cannot add duplicate classes to the whitelist ($duplicates)." + } + for (type in types) { + ((kryo.classResolver as? CordaClassResolver)?.whitelist as? MutableClassWhitelist)?.add(type) + } + } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultWhitelist.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultWhitelist.kt index c602698ade..8a5cdbcaa3 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultWhitelist.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultWhitelist.kt @@ -1,8 +1,7 @@ package net.corda.nodeapi.internal.serialization import com.esotericsoftware.kryo.KryoException -import net.corda.core.node.CordaPluginRegistry -import net.corda.core.serialization.SerializationCustomization +import net.corda.core.serialization.SerializationWhitelist import net.corda.core.utilities.NetworkHostAndPort import org.apache.activemq.artemis.api.core.SimpleString import rx.Notification @@ -12,10 +11,9 @@ import java.util.* /** * NOTE: We do not whitelist [HashMap] or [HashSet] since they are unstable under serialization. */ -class DefaultWhitelist : CordaPluginRegistry() { - override fun customizeSerialization(custom: SerializationCustomization): Boolean { - custom.apply { - addToWhitelist(Array(0, {}).javaClass, +object DefaultWhitelist : SerializationWhitelist { + override val whitelist = + listOf(Array(0, {}).javaClass, Notification::class.java, Notification.Kind::class.java, ArrayList::class.java, @@ -60,8 +58,6 @@ class DefaultWhitelist : CordaPluginRegistry() { java.util.LinkedHashMap::class.java, BitSet::class.java, OnErrorNotImplementedException::class.java, - StackTraceElement::class.java) - } - return true - } + StackTraceElement::class.java + ) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoSerializationCustomization.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoSerializationCustomization.kt deleted file mode 100644 index 993a2dad02..0000000000 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoSerializationCustomization.kt +++ /dev/null @@ -1,17 +0,0 @@ -package net.corda.nodeapi.internal.serialization - -import com.esotericsoftware.kryo.Kryo -import net.corda.core.serialization.SerializationCustomization - -class KryoSerializationCustomization(val kryo: Kryo) : SerializationCustomization { - override fun addToWhitelist(vararg types: Class<*>) { - require(types.toSet().size == types.size) { - val duplicates = types.toMutableList() - types.toSet().forEach { duplicates -= it } - "Cannot add duplicate classes to the whitelist ($duplicates)." - } - for (type in types) { - ((kryo.classResolver as? CordaClassResolver)?.whitelist as? MutableClassWhitelist)?.add(type) - } - } -} \ No newline at end of file diff --git a/node-api/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry b/node-api/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry deleted file mode 100644 index 6a4ca5dcd5..0000000000 --- a/node-api/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry +++ /dev/null @@ -1,2 +0,0 @@ -# Register a ServiceLoader service extending from net.corda.core.node.CordaPluginRegistry -net.corda.nodeapi.internal.serialization.DefaultWhitelist \ No newline at end of file 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 b0293ed838..f0711e97a7 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -23,12 +23,12 @@ import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.RPCOps import net.corda.core.messaging.SingleMessageRecipient -import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub import net.corda.core.node.services.* import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.schemas.MappedSchema +import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction @@ -70,7 +70,6 @@ import net.corda.node.utilities.* import net.corda.node.utilities.AddOrRemove.ADD import net.corda.nodeapi.internal.ServiceInfo import net.corda.nodeapi.internal.ServiceType -import net.corda.nodeapi.internal.serialization.DefaultWhitelist import org.apache.activemq.artemis.utils.ReusableLatch import org.slf4j.Logger import rx.Observable @@ -160,8 +159,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, CordaX500Name.build(cert.subjectX500Principal).copy(commonName = null) } - open val pluginRegistries: List by lazy { - cordappProvider.cordapps.flatMap { it.plugins } + DefaultWhitelist() + open val serializationWhitelists: List by lazy { + cordappProvider.cordapps.flatMap { it.serializationWhitelists } } /** Set to non-null once [start] has been successfully called. */ 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 9fd82c83d0..d180abaa41 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -7,12 +7,12 @@ import net.corda.core.internal.* import net.corda.core.internal.concurrent.thenMatch import net.corda.core.utilities.loggerFor import net.corda.node.* -import net.corda.nodeapi.internal.ServiceInfo import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.transactions.bftSMaRtSerialFilter import net.corda.node.shell.InteractiveShell import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NetworkRegistrationHelper +import net.corda.nodeapi.internal.ServiceInfo import net.corda.nodeapi.internal.addShutdownHook import org.fusesource.jansi.Ansi import org.fusesource.jansi.AnsiConsole @@ -264,12 +264,6 @@ open class NodeStartup(val args: Array) { if (it.isNotEmpty()) Node.printBasicNodeInfo("Providing additional services", it.joinToString()) } Node.printBasicNodeInfo("Loaded CorDapps", node.cordappProvider.cordapps.map { it.name }.joinToString()) - val plugins = node.pluginRegistries - .map { it.javaClass.name } - .filterNot { it.startsWith("net.corda.node.") || it.startsWith("net.corda.core.") || it.startsWith("net.corda.nodeapi.") } - .map { it.substringBefore('$') } - if (plugins.isNotEmpty()) - Node.printBasicNodeInfo("Loaded plugins", plugins.joinToString()) } open fun drawBanner(versionInfo: VersionInfo) { diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt index bb98afbe43..1449a32fb8 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt @@ -8,12 +8,13 @@ import net.corda.core.cordapp.Cordapp import net.corda.core.flows.* import net.corda.core.internal.* import net.corda.core.internal.cordapp.CordappImpl -import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.services.CordaService import net.corda.core.schemas.MappedSchema +import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import net.corda.core.utilities.loggerFor import net.corda.node.internal.classloading.requireAnnotation +import net.corda.nodeapi.internal.serialization.DefaultWhitelist import java.io.File import java.io.FileOutputStream import java.lang.reflect.Modifier @@ -222,10 +223,10 @@ class CordappLoader private constructor(private val cordappJarPaths: List) return (scanResult.getNamesOfClassesImplementing(Contract::class.java) + scanResult.getNamesOfClassesImplementing(UpgradedContract::class.java)).distinct() } - private fun findPlugins(cordappJarPath: URL): List { - return ServiceLoader.load(CordaPluginRegistry::class.java, URLClassLoader(arrayOf(cordappJarPath), appClassLoader)).toList().filter { + private fun findPlugins(cordappJarPath: URL): List { + return ServiceLoader.load(SerializationWhitelist::class.java, URLClassLoader(arrayOf(cordappJarPath), appClassLoader)).toList().filter { cordappJarPath == it.javaClass.protectionDomain.codeSource.location - } + } + DefaultWhitelist // Always add the DefaultWhitelist to the whitelist for an app. } private fun findCustomSchemas(scanResult: ScanResult): Set { diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt index 806b7ac929..e333595f35 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt @@ -55,8 +55,6 @@ class CordappLoaderTest { assertThat(actualCordapp.rpcFlows).isEmpty() assertThat(actualCordapp.schedulableFlows).isEmpty() assertThat(actualCordapp.services).isEmpty() - assertThat(actualCordapp.plugins).hasSize(1) - assertThat(actualCordapp.plugins.first().javaClass.name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedPlugin") assertThat(actualCordapp.jarPath).isEqualTo(isolatedJAR) } diff --git a/samples/irs-demo/src/test/resources/META-INF/services/net.corda.webserver.services.WebServerPluginRegistry b/samples/irs-demo/src/test/resources/META-INF/services/net.corda.webserver.services.WebServerPluginRegistry index 3f23b14e6a..d19d0b4b92 100644 --- a/samples/irs-demo/src/test/resources/META-INF/services/net.corda.webserver.services.WebServerPluginRegistry +++ b/samples/irs-demo/src/test/resources/META-INF/services/net.corda.webserver.services.WebServerPluginRegistry @@ -1,3 +1,2 @@ -# Register a ServiceLoader service extending from net.corda.core.node.CordaPluginRegistry net.corda.irs.plugin.IRSPlugin net.corda.irs.plugin.IRSDemoPlugin \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPlugin.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPlugin.kt index 270f15e57e..daf4cdcb07 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPlugin.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPlugin.kt @@ -1,7 +1,7 @@ package net.corda.vega.plugin import com.fasterxml.jackson.databind.ObjectMapper -import net.corda.core.node.CordaPluginRegistry +import net.corda.core.serialization.SerializationWhitelist import net.corda.finance.plugin.registerFinanceJSONMappers import net.corda.vega.api.PortfolioApi import net.corda.webserver.services.WebServerPluginRegistry @@ -9,7 +9,7 @@ import java.util.function.Function /** * [SimmService] is the object that makes available the flows and services for the Simm agreement / evaluation flow. - * It is loaded via discovery - see [CordaPluginRegistry]. + * It is loaded via discovery - see [SerializationWhitelist]. * It is also the object that enables a human usable web service for demo purpose * It is loaded via discovery see [WebServerPluginRegistry]. */ diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPluginRegistry.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPluginRegistry.kt index 2cbd235203..626de57761 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPluginRegistry.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPluginRegistry.kt @@ -10,35 +10,30 @@ import com.opengamma.strata.market.curve.CurveName import com.opengamma.strata.market.param.CurrencyParameterSensitivities import com.opengamma.strata.market.param.CurrencyParameterSensitivity import com.opengamma.strata.market.param.TenorDateParameterMetadata -import net.corda.core.node.CordaPluginRegistry -import net.corda.core.serialization.SerializationCustomization +import net.corda.core.serialization.SerializationWhitelist import net.corda.vega.analytics.CordaMarketData import net.corda.vega.analytics.InitialMarginTriple import net.corda.webserver.services.WebServerPluginRegistry /** * [SimmService] is the object that makes available the flows and services for the Simm agreement / evaluation flow. - * It is loaded via discovery - see [CordaPluginRegistry]. + * It is loaded via discovery - see [SerializationWhitelist]. * It is also the object that enables a human usable web service for demo purpose * It is loaded via discovery see [WebServerPluginRegistry]. */ -class SimmPluginRegistry : CordaPluginRegistry() { - override fun customizeSerialization(custom: SerializationCustomization): Boolean { - custom.apply { - // OpenGamma classes. - addToWhitelist(MultiCurrencyAmount::class.java, - Ordering.natural>().javaClass, - CurrencyAmount::class.java, - Currency::class.java, - InitialMarginTriple::class.java, - CordaMarketData::class.java, - CurrencyParameterSensitivities::class.java, - CurrencyParameterSensitivity::class.java, - DoubleArray::class.java, - CurveName::class.java, - TenorDateParameterMetadata::class.java, - Tenor::class.java) - } - return true - } +class SimmPluginRegistry : SerializationWhitelist { + override val whitelist = listOf( + MultiCurrencyAmount::class.java, + Ordering.natural>().javaClass, + CurrencyAmount::class.java, + Currency::class.java, + InitialMarginTriple::class.java, + CordaMarketData::class.java, + CurrencyParameterSensitivities::class.java, + CurrencyParameterSensitivity::class.java, + DoubleArray::class.java, + CurveName::class.java, + TenorDateParameterMetadata::class.java, + Tenor::class.java + ) } diff --git a/samples/simm-valuation-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry b/samples/simm-valuation-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry deleted file mode 100644 index e2faa7858d..0000000000 --- a/samples/simm-valuation-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry +++ /dev/null @@ -1,2 +0,0 @@ -# Register a ServiceLoader service extending from net.corda.core.node.CordaPluginRegistry -net.corda.vega.plugin.SimmPluginRegistry diff --git a/samples/simm-valuation-demo/src/main/resources/META-INF/services/net.corda.core.serialization.SerializationWhitelist b/samples/simm-valuation-demo/src/main/resources/META-INF/services/net.corda.core.serialization.SerializationWhitelist new file mode 100644 index 0000000000..78b86947ec --- /dev/null +++ b/samples/simm-valuation-demo/src/main/resources/META-INF/services/net.corda.core.serialization.SerializationWhitelist @@ -0,0 +1 @@ +net.corda.vega.plugin.SimmPluginRegistry diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index 9ef99e42e8..446c4dc09d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -15,26 +15,32 @@ import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.RPCOps import net.corda.core.messaging.SingleMessageRecipient -import net.corda.core.node.CordaPluginRegistry -import net.corda.core.node.services.* +import net.corda.core.node.services.IdentityService +import net.corda.core.node.services.KeyManagementService +import net.corda.core.node.services.NetworkMapCache +import net.corda.core.node.services.NotaryService +import net.corda.core.serialization.SerializationWhitelist import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.loggerFor import net.corda.finance.utils.WorldMapLocation import net.corda.node.internal.AbstractNode import net.corda.node.internal.StartedNode -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.nodeapi.internal.ServiceType import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.E2ETestKeyManagementService import net.corda.node.services.messaging.MessagingService import net.corda.node.services.network.InMemoryNetworkMapService import net.corda.node.services.network.NetworkMapService -import net.corda.node.services.transactions.* +import net.corda.node.services.transactions.BFTNonValidatingNotaryService +import net.corda.node.services.transactions.BFTSMaRt +import net.corda.node.services.transactions.InMemoryTransactionVerifierService +import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.node.utilities.CertificateAndKeyPair +import net.corda.nodeapi.internal.ServiceInfo +import net.corda.nodeapi.internal.ServiceType import net.corda.testing.* import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.apache.activemq.artemis.utils.ReusableLatch @@ -231,11 +237,11 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, override fun myAddresses() = emptyList() - // Allow unit tests to modify the plugin list before the node start, - // so they don't have to ServiceLoad test plugins into all unit tests. - val testPluginRegistries by lazy { super.pluginRegistries.toMutableList() } - override val pluginRegistries: List - get() = testPluginRegistries + // Allow unit tests to modify the serialization whitelist list before the node start, + // so they don't have to ServiceLoad test whitelists into all unit tests. + val testSerializationWhitelists by lazy { super.serializationWhitelists.toMutableList() } + override val serializationWhitelists: List + get() = testSerializationWhitelists // This does not indirect through the NodeInfo object so it can be called before the node is started. // It is used from the network visualiser tool. From 831df4bc3d6e3e7a956ad6990dbca01634a29ed5 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Mon, 2 Oct 2017 11:42:18 +0100 Subject: [PATCH 062/185] Minor: print a deprecation warning when the web server starts. --- webserver/src/main/kotlin/net/corda/webserver/WebServer.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/webserver/src/main/kotlin/net/corda/webserver/WebServer.kt b/webserver/src/main/kotlin/net/corda/webserver/WebServer.kt index 25388e96c1..44dc37ad3a 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/WebServer.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/WebServer.kt @@ -36,6 +36,9 @@ fun main(args: Array) { System.setProperty("log-path", (cmdlineOptions.baseDirectory / "logs/web").toString()) val log = LoggerFactory.getLogger("Main") + println("This Corda-specific web server is deprecated and will be removed in future.") + println("Please switch to a regular web framework like Spring, J2EE or Play Framework.") + println() println("Logs can be found in ${System.getProperty("log-path")}") val conf = try { From efc85f8555bc646012e6dea740fdc1b10788e708 Mon Sep 17 00:00:00 2001 From: Clinton Date: Mon, 2 Oct 2017 16:29:06 +0100 Subject: [PATCH 063/185] Added current API to CI directory (#1772) --- .ci/api-current.txt | 3276 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3276 insertions(+) create mode 100644 .ci/api-current.txt diff --git a/.ci/api-current.txt b/.ci/api-current.txt new file mode 100644 index 0000000000..f90125cf32 --- /dev/null +++ b/.ci/api-current.txt @@ -0,0 +1,3276 @@ +public class net.corda.core.CordaException extends java.lang.Exception implements net.corda.core.CordaThrowable + public () + public (String) + public (String, String, Throwable) + public (String, Throwable) + public void addSuppressed(Throwable[]) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public Throwable getCause() + @org.jetbrains.annotations.Nullable public String getMessage() + @org.jetbrains.annotations.Nullable public String getOriginalExceptionClassName() + @org.jetbrains.annotations.Nullable public String getOriginalMessage() + public int hashCode() + public void setCause(Throwable) + public void setMessage(String) + public void setOriginalExceptionClassName(String) +## +public class net.corda.core.CordaRuntimeException extends java.lang.RuntimeException implements net.corda.core.CordaThrowable + public (String) + public (String, String, Throwable) + public (String, Throwable) + public void addSuppressed(Throwable[]) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public Throwable getCause() + @org.jetbrains.annotations.Nullable public String getMessage() + @org.jetbrains.annotations.Nullable public String getOriginalExceptionClassName() + @org.jetbrains.annotations.Nullable public String getOriginalMessage() + public int hashCode() + public void setCause(Throwable) + public void setMessage(String) + public void setOriginalExceptionClassName(String) +## +public interface net.corda.core.CordaThrowable + public abstract void addSuppressed(Throwable[]) + @org.jetbrains.annotations.Nullable public abstract String getOriginalExceptionClassName() + @org.jetbrains.annotations.Nullable public abstract String getOriginalMessage() + public abstract void setCause(Throwable) + public abstract void setMessage(String) + public abstract void setOriginalExceptionClassName(String) +## +public final class net.corda.core.Utils extends java.lang.Object + @org.jetbrains.annotations.NotNull public static final net.corda.core.concurrent.CordaFuture toFuture(rx.Observable) + @org.jetbrains.annotations.NotNull public static final rx.Observable toObservable(net.corda.core.concurrent.CordaFuture) +## +public final class net.corda.core.concurrent.ConcurrencyUtils extends java.lang.Object + @org.jetbrains.annotations.NotNull public static final net.corda.core.concurrent.CordaFuture firstOf(net.corda.core.concurrent.CordaFuture[], kotlin.jvm.functions.Function1) + @org.jetbrains.annotations.NotNull public static final net.corda.core.concurrent.CordaFuture firstOf(net.corda.core.concurrent.CordaFuture[], org.slf4j.Logger, kotlin.jvm.functions.Function1) + public static final Object match(concurrent.Future, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1) + @org.jetbrains.annotations.NotNull public static final String shortCircuitedTaskFailedMessage = "Short-circuited task failed:" +## +public interface net.corda.core.concurrent.CordaFuture extends java.util.concurrent.Future + public abstract void then(kotlin.jvm.functions.Function1) + @org.jetbrains.annotations.NotNull public abstract concurrent.CompletableFuture toCompletableFuture() +## +public final class net.corda.core.contracts.AlwaysAcceptAttachmentConstraint extends java.lang.Object implements net.corda.core.contracts.AttachmentConstraint + public boolean isSatisfiedBy(net.corda.core.contracts.Attachment) + public static final net.corda.core.contracts.AlwaysAcceptAttachmentConstraint INSTANCE +## +public final class net.corda.core.contracts.Amount extends java.lang.Object implements java.lang.Comparable + public (long, Object) + public (long, java.math.BigDecimal, Object) + public int compareTo(net.corda.core.contracts.Amount) + public final long component1() + @org.jetbrains.annotations.NotNull public final java.math.BigDecimal component2() + @org.jetbrains.annotations.NotNull public final Object component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount copy(long, java.math.BigDecimal, Object) + public boolean equals(Object) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.Amount fromDecimal(java.math.BigDecimal, Object) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.Amount fromDecimal(java.math.BigDecimal, Object, java.math.RoundingMode) + @org.jetbrains.annotations.NotNull public final java.math.BigDecimal getDisplayTokenSize() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.math.BigDecimal getDisplayTokenSize(Object) + public final long getQuantity() + @org.jetbrains.annotations.NotNull public final Object getToken() + public int hashCode() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount minus(net.corda.core.contracts.Amount) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.Amount parseCurrency(String) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount plus(net.corda.core.contracts.Amount) + @org.jetbrains.annotations.NotNull public final List splitEvenly(int) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.Nullable public static final net.corda.core.contracts.Amount sumOrNull(Iterable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.Amount sumOrThrow(Iterable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.Amount sumOrZero(Iterable, Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount times(int) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount times(long) + @org.jetbrains.annotations.NotNull public final java.math.BigDecimal toDecimal() + @org.jetbrains.annotations.NotNull public String toString() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.Amount zero(Object) + public static final net.corda.core.contracts.Amount$Companion Companion +## +public static final class net.corda.core.contracts.Amount$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount fromDecimal(java.math.BigDecimal, Object) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount fromDecimal(java.math.BigDecimal, Object, java.math.RoundingMode) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final java.math.BigDecimal getDisplayTokenSize(Object) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount parseCurrency(String) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.Nullable public final net.corda.core.contracts.Amount sumOrNull(Iterable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount sumOrThrow(Iterable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount sumOrZero(Iterable, Object) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount zero(Object) +## +public final class net.corda.core.contracts.AmountTransfer extends java.lang.Object + public (long, Object, Object, Object) + @org.jetbrains.annotations.NotNull public final List apply(List, Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.AmountTransfer copy(long, Object, Object, Object) + public boolean equals(Object) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.AmountTransfer fromDecimal(java.math.BigDecimal, Object, Object, Object) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.AmountTransfer fromDecimal(java.math.BigDecimal, Object, Object, Object, java.math.RoundingMode) + @org.jetbrains.annotations.NotNull public final Object getDestination() + public final long getQuantityDelta() + @org.jetbrains.annotations.NotNull public final Object getSource() + @org.jetbrains.annotations.NotNull public final Object getToken() + public int hashCode() + @org.jetbrains.annotations.NotNull public final List novate(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.AmountTransfer plus(net.corda.core.contracts.AmountTransfer) + @org.jetbrains.annotations.NotNull public final java.math.BigDecimal toDecimal() + @org.jetbrains.annotations.NotNull public String toString() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.AmountTransfer zero(Object, Object, Object) + public static final net.corda.core.contracts.AmountTransfer$Companion Companion +## +public static final class net.corda.core.contracts.AmountTransfer$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.AmountTransfer fromDecimal(java.math.BigDecimal, Object, Object, Object) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.AmountTransfer fromDecimal(java.math.BigDecimal, Object, Object, Object, java.math.RoundingMode) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.AmountTransfer zero(Object, Object, Object) +## +public interface net.corda.core.contracts.Attachment extends net.corda.core.contracts.NamedByHash + public abstract void extractFile(String, java.io.OutputStream) + @org.jetbrains.annotations.NotNull public abstract List getSigners() + @org.jetbrains.annotations.NotNull public abstract java.io.InputStream open() + @org.jetbrains.annotations.NotNull public abstract jar.JarInputStream openAsJAR() +## +public interface net.corda.core.contracts.AttachmentConstraint + public abstract boolean isSatisfiedBy(net.corda.core.contracts.Attachment) +## +public final class net.corda.core.contracts.AttachmentResolutionException extends net.corda.core.flows.FlowException + public (net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getHash() +## +public final class net.corda.core.contracts.AutomaticHashConstraint extends java.lang.Object implements net.corda.core.contracts.AttachmentConstraint + public boolean isSatisfiedBy(net.corda.core.contracts.Attachment) + public static final net.corda.core.contracts.AutomaticHashConstraint INSTANCE +## +public final class net.corda.core.contracts.Command extends java.lang.Object + public (net.corda.core.contracts.CommandData, java.security.PublicKey) + public (net.corda.core.contracts.CommandData, List) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.CommandData component1() + @org.jetbrains.annotations.NotNull public final List component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Command copy(net.corda.core.contracts.CommandData, List) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final List getSigners() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.CommandData getValue() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() +## +public final class net.corda.core.contracts.CommandAndState extends java.lang.Object + public (net.corda.core.contracts.CommandData, net.corda.core.contracts.OwnableState) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.CommandData component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.OwnableState component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.CommandAndState copy(net.corda.core.contracts.CommandData, net.corda.core.contracts.OwnableState) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.CommandData getCommand() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.OwnableState getOwnableState() + public int hashCode() + public String toString() +## +public interface net.corda.core.contracts.CommandData +## +public final class net.corda.core.contracts.CommandWithParties extends java.lang.Object + public (List, List, net.corda.core.contracts.CommandData) + @org.jetbrains.annotations.NotNull public final List component1() + @org.jetbrains.annotations.NotNull public final List component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.CommandData component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.CommandWithParties copy(List, List, net.corda.core.contracts.CommandData) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final List getSigners() + @org.jetbrains.annotations.NotNull public final List getSigningParties() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.CommandData getValue() + public int hashCode() + public String toString() +## +public final class net.corda.core.contracts.ComponentGroupEnum extends java.lang.Enum + protected (String, int) + public static net.corda.core.contracts.ComponentGroupEnum valueOf(String) + public static net.corda.core.contracts.ComponentGroupEnum[] values() +## +public interface net.corda.core.contracts.Contract + public abstract void verify(net.corda.core.transactions.LedgerTransaction) +## +public final class net.corda.core.contracts.ContractAttachment extends java.lang.Object implements net.corda.core.contracts.Attachment + public (net.corda.core.contracts.Attachment, String) + public void extractFile(String, java.io.OutputStream) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Attachment getAttachment() + @org.jetbrains.annotations.NotNull public final String getContract() + @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId() + @org.jetbrains.annotations.NotNull public List getSigners() + @org.jetbrains.annotations.NotNull public java.io.InputStream open() + @org.jetbrains.annotations.NotNull public jar.JarInputStream openAsJAR() +## +public interface net.corda.core.contracts.ContractState + @org.jetbrains.annotations.NotNull public abstract List getParticipants() +## +public final class net.corda.core.contracts.ContractsDSL extends java.lang.Object + @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.CommandWithParties requireSingleCommand(Collection, Class) + public static final Object requireThat(kotlin.jvm.functions.Function1) +## +public interface net.corda.core.contracts.FungibleAsset extends net.corda.core.contracts.OwnableState + @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.Amount getAmount() + @org.jetbrains.annotations.NotNull public abstract Collection getExitKeys() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.FungibleAsset withNewOwnerAndAmount(net.corda.core.contracts.Amount, net.corda.core.identity.AbstractParty) +## +public final class net.corda.core.contracts.HashAttachmentConstraint extends java.lang.Object implements net.corda.core.contracts.AttachmentConstraint + public (net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.HashAttachmentConstraint copy(net.corda.core.crypto.SecureHash) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getAttachmentId() + public int hashCode() + public boolean isSatisfiedBy(net.corda.core.contracts.Attachment) + public String toString() +## +public final class net.corda.core.contracts.InsufficientBalanceException extends net.corda.core.flows.FlowException + public (net.corda.core.contracts.Amount) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount getAmountMissing() +## +public final class net.corda.core.contracts.Issued extends java.lang.Object + public (net.corda.core.contracts.PartyAndReference, Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.PartyAndReference component1() + @org.jetbrains.annotations.NotNull public final Object component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Issued copy(net.corda.core.contracts.PartyAndReference, Object) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.PartyAndReference getIssuer() + @org.jetbrains.annotations.NotNull public final Object getProduct() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() +## +public @interface net.corda.core.contracts.LegalProseReference + public abstract String uri() +## +public interface net.corda.core.contracts.LinearState extends net.corda.core.contracts.ContractState + @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.UniqueIdentifier getLinearId() +## +public interface net.corda.core.contracts.MoveCommand extends net.corda.core.contracts.CommandData + @org.jetbrains.annotations.Nullable public abstract Class getContract() +## +public interface net.corda.core.contracts.NamedByHash + @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.SecureHash getId() +## +public interface net.corda.core.contracts.OwnableState extends net.corda.core.contracts.ContractState + @org.jetbrains.annotations.NotNull public abstract net.corda.core.identity.AbstractParty getOwner() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.CommandAndState withNewOwner(net.corda.core.identity.AbstractParty) +## +public final class net.corda.core.contracts.PartyAndReference extends java.lang.Object + public (net.corda.core.identity.AbstractParty, net.corda.core.utilities.OpaqueBytes) + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.AbstractParty component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.OpaqueBytes component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.PartyAndReference copy(net.corda.core.identity.AbstractParty, net.corda.core.utilities.OpaqueBytes) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.AbstractParty getParty() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.OpaqueBytes getReference() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() +## +public final class net.corda.core.contracts.PrivacySalt extends net.corda.core.utilities.OpaqueBytes + public () + public (byte[]) +## +public final class net.corda.core.contracts.Requirements extends java.lang.Object + public final void using(String, boolean) + public static final net.corda.core.contracts.Requirements INSTANCE +## +public interface net.corda.core.contracts.SchedulableState extends net.corda.core.contracts.ContractState + @org.jetbrains.annotations.Nullable public abstract net.corda.core.contracts.ScheduledActivity nextScheduledActivity(net.corda.core.contracts.StateRef, net.corda.core.flows.FlowLogicRefFactory) +## +public interface net.corda.core.contracts.Scheduled + @org.jetbrains.annotations.NotNull public abstract java.time.Instant getScheduledAt() +## +public final class net.corda.core.contracts.ScheduledActivity extends java.lang.Object implements net.corda.core.contracts.Scheduled + public (net.corda.core.flows.FlowLogicRef, java.time.Instant) + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowLogicRef component1() + @org.jetbrains.annotations.NotNull public final java.time.Instant component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ScheduledActivity copy(net.corda.core.flows.FlowLogicRef, java.time.Instant) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowLogicRef getLogicRef() + @org.jetbrains.annotations.NotNull public java.time.Instant getScheduledAt() + public int hashCode() + public String toString() +## +public final class net.corda.core.contracts.ScheduledStateRef extends java.lang.Object implements net.corda.core.contracts.Scheduled + public (net.corda.core.contracts.StateRef, java.time.Instant) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateRef component1() + @org.jetbrains.annotations.NotNull public final java.time.Instant component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ScheduledStateRef copy(net.corda.core.contracts.StateRef, java.time.Instant) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateRef getRef() + @org.jetbrains.annotations.NotNull public java.time.Instant getScheduledAt() + public int hashCode() + public String toString() +## +public final class net.corda.core.contracts.SourceAndAmount extends java.lang.Object + public (Object, net.corda.core.contracts.Amount, Object) + @org.jetbrains.annotations.NotNull public final Object component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount component2() + @org.jetbrains.annotations.Nullable public final Object component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.SourceAndAmount copy(Object, net.corda.core.contracts.Amount, Object) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount getAmount() + @org.jetbrains.annotations.Nullable public final Object getRef() + @org.jetbrains.annotations.NotNull public final Object getSource() + public int hashCode() + public String toString() +## +public final class net.corda.core.contracts.StateAndContract extends java.lang.Object + public (net.corda.core.contracts.ContractState, String) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ContractState component1() + @org.jetbrains.annotations.NotNull public final String component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateAndContract copy(net.corda.core.contracts.ContractState, String) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final String getContract() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ContractState getState() + public int hashCode() + public String toString() +## +public final class net.corda.core.contracts.StateAndRef extends java.lang.Object + public (net.corda.core.contracts.TransactionState, net.corda.core.contracts.StateRef) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.TransactionState component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateRef component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateAndRef copy(net.corda.core.contracts.TransactionState, net.corda.core.contracts.StateRef) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateRef getRef() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.TransactionState getState() + public int hashCode() + public String toString() +## +public final class net.corda.core.contracts.StateRef extends java.lang.Object + public (net.corda.core.crypto.SecureHash, int) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component1() + public final int component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateRef copy(net.corda.core.crypto.SecureHash, int) + public boolean equals(Object) + public final int getIndex() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getTxhash() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() +## +public final class net.corda.core.contracts.Structures extends java.lang.Object + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash hash(net.corda.core.contracts.ContractState) + @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.Amount withoutIssuer(net.corda.core.contracts.Amount) +## +public abstract class net.corda.core.contracts.TimeWindow extends java.lang.Object + public () + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.TimeWindow between(java.time.Instant, java.time.Instant) + public abstract boolean contains(java.time.Instant) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.TimeWindow fromOnly(java.time.Instant) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.TimeWindow fromStartAndDuration(java.time.Instant, java.time.Duration) + @org.jetbrains.annotations.Nullable public abstract java.time.Instant getFromTime() + @org.jetbrains.annotations.Nullable public abstract java.time.Instant getMidpoint() + @org.jetbrains.annotations.Nullable public abstract java.time.Instant getUntilTime() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.TimeWindow untilOnly(java.time.Instant) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.TimeWindow withTolerance(java.time.Instant, java.time.Duration) + public static final net.corda.core.contracts.TimeWindow$Companion Companion +## +public static final class net.corda.core.contracts.TimeWindow$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.TimeWindow between(java.time.Instant, java.time.Instant) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.TimeWindow fromOnly(java.time.Instant) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.TimeWindow fromStartAndDuration(java.time.Instant, java.time.Duration) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.TimeWindow untilOnly(java.time.Instant) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.TimeWindow withTolerance(java.time.Instant, java.time.Duration) +## +public interface net.corda.core.contracts.TokenizableAssetInfo + @org.jetbrains.annotations.NotNull public abstract java.math.BigDecimal getDisplayTokenSize() +## +public final class net.corda.core.contracts.TransactionResolutionException extends net.corda.core.flows.FlowException + public (net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getHash() +## +public final class net.corda.core.contracts.TransactionState extends java.lang.Object + @kotlin.jvm.JvmOverloads public (net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party) + @kotlin.jvm.JvmOverloads public (net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party, Integer) + @kotlin.jvm.JvmOverloads public (net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party, Integer, net.corda.core.contracts.AttachmentConstraint) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ContractState component1() + @org.jetbrains.annotations.NotNull public final String component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component3() + @org.jetbrains.annotations.Nullable public final Integer component4() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.AttachmentConstraint component5() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.TransactionState copy(net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party, Integer, net.corda.core.contracts.AttachmentConstraint) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.AttachmentConstraint getConstraint() + @org.jetbrains.annotations.NotNull public final String getContract() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ContractState getData() + @org.jetbrains.annotations.Nullable public final Integer getEncumbrance() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party getNotary() + public int hashCode() + public String toString() +## +public final class net.corda.core.contracts.TransactionStateKt extends java.lang.Object +## +public abstract class net.corda.core.contracts.TransactionVerificationException extends net.corda.core.flows.FlowException + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getTxId() +## +public static final class net.corda.core.contracts.TransactionVerificationException$ContractConstraintRejection extends net.corda.core.contracts.TransactionVerificationException + public (net.corda.core.crypto.SecureHash, String) +## +public static final class net.corda.core.contracts.TransactionVerificationException$ContractCreationError extends net.corda.core.contracts.TransactionVerificationException + public (net.corda.core.crypto.SecureHash, String, Throwable) +## +public static final class net.corda.core.contracts.TransactionVerificationException$ContractRejection extends net.corda.core.contracts.TransactionVerificationException + public (net.corda.core.crypto.SecureHash, net.corda.core.contracts.Contract, Throwable) +## +public static final class net.corda.core.contracts.TransactionVerificationException$Direction extends java.lang.Enum + protected (String, int) + public static net.corda.core.contracts.TransactionVerificationException$Direction valueOf(String) + public static net.corda.core.contracts.TransactionVerificationException$Direction[] values() +## +public static final class net.corda.core.contracts.TransactionVerificationException$DuplicateInputStates extends net.corda.core.contracts.TransactionVerificationException + public (net.corda.core.crypto.SecureHash, net.corda.core.utilities.NonEmptySet) + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.NonEmptySet getDuplicates() +## +public static final class net.corda.core.contracts.TransactionVerificationException$InvalidNotaryChange extends net.corda.core.contracts.TransactionVerificationException + public (net.corda.core.crypto.SecureHash) +## +public static final class net.corda.core.contracts.TransactionVerificationException$MissingAttachmentRejection extends net.corda.core.contracts.TransactionVerificationException + public (net.corda.core.crypto.SecureHash, String) +## +public static final class net.corda.core.contracts.TransactionVerificationException$MoreThanOneNotary extends net.corda.core.contracts.TransactionVerificationException + public (net.corda.core.crypto.SecureHash) +## +public static final class net.corda.core.contracts.TransactionVerificationException$NotaryChangeInWrongTransactionType extends net.corda.core.contracts.TransactionVerificationException + public (net.corda.core.crypto.SecureHash, net.corda.core.identity.Party, net.corda.core.identity.Party) +## +public static final class net.corda.core.contracts.TransactionVerificationException$SignersMissing extends net.corda.core.contracts.TransactionVerificationException + public (net.corda.core.crypto.SecureHash, List) +## +public static final class net.corda.core.contracts.TransactionVerificationException$TransactionMissingEncumbranceException extends net.corda.core.contracts.TransactionVerificationException + public (net.corda.core.crypto.SecureHash, int, net.corda.core.contracts.TransactionVerificationException$Direction) +## +public abstract class net.corda.core.contracts.TypeOnlyCommandData extends java.lang.Object implements net.corda.core.contracts.CommandData + public () + public boolean equals(Object) + public int hashCode() +## +public final class net.corda.core.contracts.UniqueIdentifier extends java.lang.Object implements java.lang.Comparable + public () + public (String, UUID) + public int compareTo(net.corda.core.contracts.UniqueIdentifier) + @org.jetbrains.annotations.Nullable public final String component1() + @org.jetbrains.annotations.NotNull public final UUID component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.UniqueIdentifier copy(String, UUID) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public final String getExternalId() + @org.jetbrains.annotations.NotNull public final UUID getId() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() + public static final net.corda.core.contracts.UniqueIdentifier$Companion Companion +## +public static final class net.corda.core.contracts.UniqueIdentifier$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.UniqueIdentifier fromString(String) +## +public interface net.corda.core.contracts.UpgradedContract extends net.corda.core.contracts.Contract + @org.jetbrains.annotations.NotNull public abstract String getLegacyContract() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.ContractState upgrade(net.corda.core.contracts.ContractState) +## +public interface net.corda.core.cordapp.Cordapp + @org.jetbrains.annotations.NotNull public abstract List getContractClassNames() + @org.jetbrains.annotations.NotNull public abstract List getCordappClasses() + @org.jetbrains.annotations.NotNull public abstract Set getCustomSchemas() + @org.jetbrains.annotations.NotNull public abstract List getInitiatedFlows() + @org.jetbrains.annotations.NotNull public abstract java.net.URL getJarPath() + @org.jetbrains.annotations.NotNull public abstract String getName() + @org.jetbrains.annotations.NotNull public abstract List getPlugins() + @org.jetbrains.annotations.NotNull public abstract List getRpcFlows() + @org.jetbrains.annotations.NotNull public abstract List getSchedulableFlows() + @org.jetbrains.annotations.NotNull public abstract List getServices() +## +public final class net.corda.core.cordapp.CordappContext extends java.lang.Object + public (net.corda.core.cordapp.Cordapp, net.corda.core.crypto.SecureHash, ClassLoader) + @org.jetbrains.annotations.Nullable public final net.corda.core.crypto.SecureHash getAttachmentId() + @org.jetbrains.annotations.NotNull public final ClassLoader getClassLoader() + @org.jetbrains.annotations.NotNull public final net.corda.core.cordapp.Cordapp getCordapp() +## +public interface net.corda.core.cordapp.CordappProvider + @org.jetbrains.annotations.NotNull public abstract net.corda.core.cordapp.CordappContext getAppContext() + @org.jetbrains.annotations.Nullable public abstract net.corda.core.crypto.SecureHash getContractAttachmentID(String) +## +public class net.corda.core.crypto.AddressFormatException extends java.lang.IllegalArgumentException + public () + public (String) +## +public class net.corda.core.crypto.Base58 extends java.lang.Object + public () + public static byte[] decode(String) + public static byte[] decodeChecked(String) + public static java.math.BigInteger decodeToBigInteger(String) + public static String encode(byte[]) +## +public final class net.corda.core.crypto.CompositeKey extends java.lang.Object implements java.security.PublicKey + public final void checkValidity() + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public String getAlgorithm() + @org.jetbrains.annotations.NotNull public final List getChildren() + @org.jetbrains.annotations.NotNull public byte[] getEncoded() + @org.jetbrains.annotations.NotNull public String getFormat() + @org.jetbrains.annotations.NotNull public final Set getLeafKeys() + public final int getThreshold() + public int hashCode() + public final boolean isFulfilledBy(Iterable) + public final boolean isFulfilledBy(java.security.PublicKey) + @org.jetbrains.annotations.NotNull public String toString() + public static final net.corda.core.crypto.CompositeKey$Companion Companion + @org.jetbrains.annotations.NotNull public static final String KEY_ALGORITHM = "COMPOSITE" +## +public static final class net.corda.core.crypto.CompositeKey$Builder extends java.lang.Object + public () + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.CompositeKey$Builder addKey(java.security.PublicKey, int) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.CompositeKey$Builder addKeys(List) + @org.jetbrains.annotations.NotNull public final java.security.PublicKey build(Integer) +## +public static final class net.corda.core.crypto.CompositeKey$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final java.security.PublicKey getInstance(org.bouncycastle.asn1.ASN1Primitive) + @org.jetbrains.annotations.NotNull public final java.security.PublicKey getInstance(byte[]) +## +public static final class net.corda.core.crypto.CompositeKey$NodeAndWeight extends org.bouncycastle.asn1.ASN1Object implements java.lang.Comparable + public (java.security.PublicKey, int) + public int compareTo(net.corda.core.crypto.CompositeKey$NodeAndWeight) + @org.jetbrains.annotations.NotNull public final java.security.PublicKey component1() + public final int component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.CompositeKey$NodeAndWeight copy(java.security.PublicKey, int) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final java.security.PublicKey getNode() + public final int getWeight() + public int hashCode() + @org.jetbrains.annotations.NotNull public org.bouncycastle.asn1.ASN1Primitive toASN1Primitive() + @org.jetbrains.annotations.NotNull public String toString() +## +public final class net.corda.core.crypto.CompositeKeyFactory extends java.security.KeyFactorySpi + public () + @org.jetbrains.annotations.NotNull protected java.security.PrivateKey engineGeneratePrivate(java.security.spec.KeySpec) + @org.jetbrains.annotations.Nullable protected java.security.PublicKey engineGeneratePublic(java.security.spec.KeySpec) + @org.jetbrains.annotations.NotNull protected java.security.spec.KeySpec engineGetKeySpec(java.security.Key, Class) + @org.jetbrains.annotations.NotNull protected java.security.Key engineTranslateKey(java.security.Key) +## +public final class net.corda.core.crypto.CompositeSignature extends java.security.Signature + public () + @kotlin.Deprecated @org.jetbrains.annotations.NotNull protected Object engineGetParameter(String) + protected void engineInitSign(java.security.PrivateKey) + protected void engineInitVerify(java.security.PublicKey) + @kotlin.Deprecated protected void engineSetParameter(String, Object) + protected void engineSetParameter(java.security.spec.AlgorithmParameterSpec) + @org.jetbrains.annotations.NotNull protected byte[] engineSign() + protected void engineUpdate(byte) + protected void engineUpdate(byte[], int, int) + protected boolean engineVerify(byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.Provider$Service getService(java.security.Provider) + public static final net.corda.core.crypto.CompositeSignature$Companion Companion + @org.jetbrains.annotations.NotNull public static final String SIGNATURE_ALGORITHM = "COMPOSITESIG" +## +public static final class net.corda.core.crypto.CompositeSignature$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final java.security.Provider$Service getService(java.security.Provider) +## +public static final class net.corda.core.crypto.CompositeSignature$State extends java.lang.Object + public (java.io.ByteArrayOutputStream, net.corda.core.crypto.CompositeKey) + @org.jetbrains.annotations.NotNull public final java.io.ByteArrayOutputStream component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.CompositeKey component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.CompositeSignature$State copy(java.io.ByteArrayOutputStream, net.corda.core.crypto.CompositeKey) + public final boolean engineVerify(byte[]) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final java.io.ByteArrayOutputStream getBuffer() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.CompositeKey getVerifyKey() + public int hashCode() + public String toString() +## +public final class net.corda.core.crypto.CordaObjectIdentifier extends java.lang.Object + @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final org.bouncycastle.asn1.ASN1ObjectIdentifier COMPOSITE_KEY + @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final org.bouncycastle.asn1.ASN1ObjectIdentifier COMPOSITE_SIGNATURE + public static final net.corda.core.crypto.CordaObjectIdentifier INSTANCE +## +public final class net.corda.core.crypto.CordaSecurityProvider extends java.security.Provider + public () + public static final net.corda.core.crypto.CordaSecurityProvider$Companion Companion + @org.jetbrains.annotations.NotNull public static final String PROVIDER_NAME = "Corda" +## +public static final class net.corda.core.crypto.CordaSecurityProvider$Companion extends java.lang.Object +## +public final class net.corda.core.crypto.Crypto extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.PrivateKey decodePrivateKey(String, byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.PrivateKey decodePrivateKey(net.corda.core.crypto.SignatureScheme, byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.PrivateKey decodePrivateKey(byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.PublicKey decodePublicKey(String, byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.PublicKey decodePublicKey(net.corda.core.crypto.SignatureScheme, byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.PublicKey decodePublicKey(byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.KeyPair deriveKeyPair(java.security.PrivateKey, byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.KeyPair deriveKeyPair(net.corda.core.crypto.SignatureScheme, java.security.PrivateKey, byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.KeyPair deriveKeyPairFromEntropy(java.math.BigInteger) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.KeyPair deriveKeyPairFromEntropy(net.corda.core.crypto.SignatureScheme, java.math.BigInteger) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final byte[] doSign(String, java.security.PrivateKey, byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.TransactionSignature doSign(java.security.KeyPair, net.corda.core.crypto.SignableData) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final byte[] doSign(java.security.PrivateKey, byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final byte[] doSign(net.corda.core.crypto.SignatureScheme, java.security.PrivateKey, byte[]) + @kotlin.jvm.JvmStatic public static final boolean doVerify(String, java.security.PublicKey, byte[], byte[]) + @kotlin.jvm.JvmStatic public static final boolean doVerify(java.security.PublicKey, byte[], byte[]) + @kotlin.jvm.JvmStatic public static final boolean doVerify(net.corda.core.crypto.SecureHash, net.corda.core.crypto.TransactionSignature) + @kotlin.jvm.JvmStatic public static final boolean doVerify(net.corda.core.crypto.SignatureScheme, java.security.PublicKey, byte[], byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.Provider findProvider(String) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme findSignatureScheme(String) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme findSignatureScheme(java.security.PrivateKey) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme findSignatureScheme(java.security.PublicKey) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme findSignatureScheme(org.bouncycastle.asn1.x509.AlgorithmIdentifier) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final java.security.KeyPair generateKeyPair() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.KeyPair generateKeyPair(String) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final java.security.KeyPair generateKeyPair(net.corda.core.crypto.SignatureScheme) + @kotlin.jvm.JvmStatic public static final boolean isSupportedSignatureScheme(net.corda.core.crypto.SignatureScheme) + @kotlin.jvm.JvmStatic public static final boolean isValid(java.security.PublicKey, byte[], byte[]) + @kotlin.jvm.JvmStatic public static final boolean isValid(net.corda.core.crypto.SecureHash, net.corda.core.crypto.TransactionSignature) + @kotlin.jvm.JvmStatic public static final boolean isValid(net.corda.core.crypto.SignatureScheme, java.security.PublicKey, byte[], byte[]) + @kotlin.jvm.JvmStatic public static final boolean publicKeyOnCurve(net.corda.core.crypto.SignatureScheme, java.security.PublicKey) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final List supportedSignatureSchemes() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.PrivateKey toSupportedPrivateKey(java.security.PrivateKey) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.PublicKey toSupportedPublicKey(java.security.PublicKey) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.PublicKey toSupportedPublicKey(org.bouncycastle.asn1.x509.SubjectPublicKeyInfo) + @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme COMPOSITE_KEY + @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme DEFAULT_SIGNATURE_SCHEME + @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme ECDSA_SECP256K1_SHA256 + @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme ECDSA_SECP256R1_SHA256 + @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme EDDSA_ED25519_SHA512 + public static final net.corda.core.crypto.Crypto INSTANCE + @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme RSA_SHA256 + @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final org.bouncycastle.asn1.DLSequence SHA512_256 + @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme SPHINCS256_SHA256 +## +public final class net.corda.core.crypto.CryptoUtils extends java.lang.Object + @org.jetbrains.annotations.NotNull public static final Set byKeys(Iterable) + @org.jetbrains.annotations.NotNull public static final java.security.PrivateKey component1(java.security.KeyPair) + @org.jetbrains.annotations.NotNull public static final java.security.PublicKey component2(java.security.KeyPair) + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash componentHash(net.corda.core.crypto.SecureHash, net.corda.core.utilities.OpaqueBytes) + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash componentHash(net.corda.core.utilities.OpaqueBytes, net.corda.core.contracts.PrivacySalt, int, int) + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash$SHA256 computeNonce(net.corda.core.contracts.PrivacySalt, int, int) + public static final boolean containsAny(java.security.PublicKey, Iterable) + @org.jetbrains.annotations.NotNull public static final java.security.KeyPair entropyToKeyPair(java.math.BigInteger) + @org.jetbrains.annotations.NotNull public static final java.security.KeyPair generateKeyPair() + @org.jetbrains.annotations.NotNull public static final Set getKeys(java.security.PublicKey) + public static final boolean isFulfilledBy(java.security.PublicKey, Iterable) + public static final boolean isFulfilledBy(java.security.PublicKey, java.security.PublicKey) + public static final boolean isValid(java.security.PublicKey, byte[], net.corda.core.crypto.DigitalSignature) + @org.jetbrains.annotations.NotNull public static final java.security.SecureRandom newSecureRandom() + public static final long random63BitValue() + @org.jetbrains.annotations.NotNull public static final byte[] secureRandomBytes(int) + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash serializedHash(Object) + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.TransactionSignature sign(java.security.KeyPair, net.corda.core.crypto.SignableData) + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.DigitalSignature$WithKey sign(java.security.KeyPair, net.corda.core.utilities.OpaqueBytes) + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.DigitalSignature$WithKey sign(java.security.KeyPair, byte[]) + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.DigitalSignature sign(java.security.PrivateKey, byte[]) + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.DigitalSignature$WithKey sign(java.security.PrivateKey, byte[], java.security.PublicKey) + @org.jetbrains.annotations.NotNull public static final String toStringShort(java.security.PublicKey) + public static final boolean verify(java.security.KeyPair, byte[], byte[]) + public static final boolean verify(java.security.PublicKey, byte[], net.corda.core.crypto.DigitalSignature) + public static final boolean verify(java.security.PublicKey, byte[], byte[]) +## +public class net.corda.core.crypto.DigitalSignature extends net.corda.core.utilities.OpaqueBytes + public (byte[]) +## +public static class net.corda.core.crypto.DigitalSignature$WithKey extends net.corda.core.crypto.DigitalSignature + public (java.security.PublicKey, byte[]) + @org.jetbrains.annotations.NotNull public final java.security.PublicKey getBy() + public final boolean isValid(byte[]) + public final boolean verify(net.corda.core.utilities.OpaqueBytes) + public final boolean verify(byte[]) +## +public abstract class net.corda.core.crypto.MerkleTree extends java.lang.Object + @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.SecureHash getHash() + public static final net.corda.core.crypto.MerkleTree$Companion Companion +## +public static final class net.corda.core.crypto.MerkleTree$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.MerkleTree getMerkleTree(List) +## +public static final class net.corda.core.crypto.MerkleTree$Leaf extends net.corda.core.crypto.MerkleTree + public (net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.MerkleTree$Leaf copy(net.corda.core.crypto.SecureHash) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getHash() + public int hashCode() + public String toString() +## +public static final class net.corda.core.crypto.MerkleTree$Node extends net.corda.core.crypto.MerkleTree + public (net.corda.core.crypto.SecureHash, net.corda.core.crypto.MerkleTree, net.corda.core.crypto.MerkleTree) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.MerkleTree component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.MerkleTree component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.MerkleTree$Node copy(net.corda.core.crypto.SecureHash, net.corda.core.crypto.MerkleTree, net.corda.core.crypto.MerkleTree) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getHash() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.MerkleTree getLeft() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.MerkleTree getRight() + public int hashCode() + public String toString() +## +public final class net.corda.core.crypto.MerkleTreeException extends net.corda.core.CordaException + public (String) + @org.jetbrains.annotations.NotNull public final String getReason() +## +public final class net.corda.core.crypto.NullKeys extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.AnonymousParty getNULL_PARTY() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.TransactionSignature getNULL_SIGNATURE() + public static final net.corda.core.crypto.NullKeys INSTANCE +## +public static final class net.corda.core.crypto.NullKeys$NullPublicKey extends java.lang.Object implements java.security.PublicKey, java.lang.Comparable + public int compareTo(java.security.PublicKey) + @org.jetbrains.annotations.NotNull public String getAlgorithm() + @org.jetbrains.annotations.NotNull public byte[] getEncoded() + @org.jetbrains.annotations.NotNull public String getFormat() + @org.jetbrains.annotations.NotNull public String toString() + public static final net.corda.core.crypto.NullKeys$NullPublicKey INSTANCE +## +public final class net.corda.core.crypto.PartialMerkleTree extends java.lang.Object + public (net.corda.core.crypto.PartialMerkleTree$PartialTree) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.PartialMerkleTree$PartialTree getRoot() + public final boolean verify(net.corda.core.crypto.SecureHash, List) + public static final net.corda.core.crypto.PartialMerkleTree$Companion Companion +## +public static final class net.corda.core.crypto.PartialMerkleTree$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.PartialMerkleTree build(net.corda.core.crypto.MerkleTree, List) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash rootAndUsedHashes(net.corda.core.crypto.PartialMerkleTree$PartialTree, List) +## +public abstract static class net.corda.core.crypto.PartialMerkleTree$PartialTree extends java.lang.Object +## +public static final class net.corda.core.crypto.PartialMerkleTree$PartialTree$IncludedLeaf extends net.corda.core.crypto.PartialMerkleTree$PartialTree + public (net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.PartialMerkleTree$PartialTree$IncludedLeaf copy(net.corda.core.crypto.SecureHash) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getHash() + public int hashCode() + public String toString() +## +public static final class net.corda.core.crypto.PartialMerkleTree$PartialTree$Leaf extends net.corda.core.crypto.PartialMerkleTree$PartialTree + public (net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.PartialMerkleTree$PartialTree$Leaf copy(net.corda.core.crypto.SecureHash) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getHash() + public int hashCode() + public String toString() +## +public static final class net.corda.core.crypto.PartialMerkleTree$PartialTree$Node extends net.corda.core.crypto.PartialMerkleTree$PartialTree + public (net.corda.core.crypto.PartialMerkleTree$PartialTree, net.corda.core.crypto.PartialMerkleTree$PartialTree) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.PartialMerkleTree$PartialTree component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.PartialMerkleTree$PartialTree component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.PartialMerkleTree$PartialTree$Node copy(net.corda.core.crypto.PartialMerkleTree$PartialTree, net.corda.core.crypto.PartialMerkleTree$PartialTree) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.PartialMerkleTree$PartialTree getLeft() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.PartialMerkleTree$PartialTree getRight() + public int hashCode() + public String toString() +## +public abstract class net.corda.core.crypto.SecureHash extends net.corda.core.utilities.OpaqueBytes + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash$SHA256 hashConcat(net.corda.core.crypto.SecureHash) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash$SHA256 parse(String) + @org.jetbrains.annotations.NotNull public final String prefixChars(int) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash$SHA256 randomSHA256() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash$SHA256 sha256(String) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash$SHA256 sha256(byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash$SHA256 sha256Twice(byte[]) + @org.jetbrains.annotations.NotNull public String toString() + public static final net.corda.core.crypto.SecureHash$Companion Companion +## +public static final class net.corda.core.crypto.SecureHash$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash$SHA256 getAllOnesHash() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash$SHA256 getZeroHash() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash$SHA256 parse(String) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash$SHA256 randomSHA256() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash$SHA256 sha256(String) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash$SHA256 sha256(byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash$SHA256 sha256Twice(byte[]) +## +public static final class net.corda.core.crypto.SecureHash$SHA256 extends net.corda.core.crypto.SecureHash + public (byte[]) +## +public final class net.corda.core.crypto.SecureHashKt extends java.lang.Object + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash$SHA256 sha256(net.corda.core.utilities.OpaqueBytes) + @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SecureHash$SHA256 sha256(byte[]) +## +public final class net.corda.core.crypto.SignableData extends java.lang.Object + public (net.corda.core.crypto.SecureHash, net.corda.core.crypto.SignatureMetadata) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SignatureMetadata component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SignableData copy(net.corda.core.crypto.SecureHash, net.corda.core.crypto.SignatureMetadata) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SignatureMetadata getSignatureMetadata() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getTxId() + public int hashCode() + public String toString() +## +public final class net.corda.core.crypto.SignatureMetadata extends java.lang.Object + public (int, int) + public final int component1() + public final int component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SignatureMetadata copy(int, int) + public boolean equals(Object) + public final int getPlatformVersion() + public final int getSchemeNumberID() + public int hashCode() + public String toString() +## +public final class net.corda.core.crypto.SignatureScheme extends java.lang.Object + public (int, String, org.bouncycastle.asn1.x509.AlgorithmIdentifier, List, String, String, String, java.security.spec.AlgorithmParameterSpec, Integer, String) + public final int component1() + @org.jetbrains.annotations.NotNull public final String component10() + @org.jetbrains.annotations.NotNull public final String component2() + @org.jetbrains.annotations.NotNull public final org.bouncycastle.asn1.x509.AlgorithmIdentifier component3() + @org.jetbrains.annotations.NotNull public final List component4() + @org.jetbrains.annotations.NotNull public final String component5() + @org.jetbrains.annotations.NotNull public final String component6() + @org.jetbrains.annotations.NotNull public final String component7() + @org.jetbrains.annotations.Nullable public final java.security.spec.AlgorithmParameterSpec component8() + @org.jetbrains.annotations.Nullable public final Integer component9() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SignatureScheme copy(int, String, org.bouncycastle.asn1.x509.AlgorithmIdentifier, List, String, String, String, java.security.spec.AlgorithmParameterSpec, Integer, String) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public final java.security.spec.AlgorithmParameterSpec getAlgSpec() + @org.jetbrains.annotations.NotNull public final String getAlgorithmName() + @org.jetbrains.annotations.NotNull public final List getAlternativeOIDs() + @org.jetbrains.annotations.NotNull public final String getDesc() + @org.jetbrains.annotations.Nullable public final Integer getKeySize() + @org.jetbrains.annotations.NotNull public final String getProviderName() + @org.jetbrains.annotations.NotNull public final String getSchemeCodeName() + public final int getSchemeNumberID() + @org.jetbrains.annotations.NotNull public final String getSignatureName() + @org.jetbrains.annotations.NotNull public final org.bouncycastle.asn1.x509.AlgorithmIdentifier getSignatureOID() + public int hashCode() + public String toString() +## +public class net.corda.core.crypto.SignedData extends java.lang.Object + public (net.corda.core.serialization.SerializedBytes, net.corda.core.crypto.DigitalSignature$WithKey) + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializedBytes getRaw() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.DigitalSignature$WithKey getSig() + @org.jetbrains.annotations.NotNull public final Object verified() + protected void verifyData(Object) +## +public final class net.corda.core.crypto.TransactionSignature extends net.corda.core.crypto.DigitalSignature + public (byte[], java.security.PublicKey, net.corda.core.crypto.SignatureMetadata) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final java.security.PublicKey getBy() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SignatureMetadata getSignatureMetadata() + public int hashCode() + public final boolean isValid(net.corda.core.crypto.SecureHash) + public final boolean verify(net.corda.core.crypto.SecureHash) +## +public final class net.corda.core.crypto.composite.CompositeSignaturesWithKeys extends java.lang.Object + public (List) + @org.jetbrains.annotations.NotNull public final List component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.composite.CompositeSignaturesWithKeys copy(List) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final List getSigs() + public int hashCode() + public String toString() + public static final net.corda.core.crypto.composite.CompositeSignaturesWithKeys$Companion Companion +## +public static final class net.corda.core.crypto.composite.CompositeSignaturesWithKeys$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.composite.CompositeSignaturesWithKeys getEMPTY() +## +public abstract class net.corda.core.flows.AbstractStateReplacementFlow extends java.lang.Object + public () +## +public abstract static class net.corda.core.flows.AbstractStateReplacementFlow$Acceptor extends net.corda.core.flows.FlowLogic + public (net.corda.core.flows.FlowSession) + public (net.corda.core.flows.FlowSession, net.corda.core.utilities.ProgressTracker) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.Nullable public Void call() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowSession getInitiatingSession() + @org.jetbrains.annotations.NotNull public net.corda.core.utilities.ProgressTracker getProgressTracker() + protected abstract void verifyProposal(net.corda.core.transactions.SignedTransaction, net.corda.core.flows.AbstractStateReplacementFlow$Proposal) + public static final net.corda.core.flows.AbstractStateReplacementFlow$Acceptor$Companion Companion +## +public static final class net.corda.core.flows.AbstractStateReplacementFlow$Acceptor$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker tracker() +## +public static final class net.corda.core.flows.AbstractStateReplacementFlow$Acceptor$Companion$APPROVING extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.AbstractStateReplacementFlow$Acceptor$Companion$APPROVING INSTANCE +## +public static final class net.corda.core.flows.AbstractStateReplacementFlow$Acceptor$Companion$VERIFYING extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.AbstractStateReplacementFlow$Acceptor$Companion$VERIFYING INSTANCE +## +public abstract static class net.corda.core.flows.AbstractStateReplacementFlow$Instigator extends net.corda.core.flows.FlowLogic + public (net.corda.core.contracts.StateAndRef, Object, net.corda.core.utilities.ProgressTracker) + @org.jetbrains.annotations.NotNull protected abstract net.corda.core.flows.AbstractStateReplacementFlow$UpgradeTx assembleTx() + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public net.corda.core.contracts.StateAndRef call() + public final Object getModification() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateAndRef getOriginalState() + @org.jetbrains.annotations.NotNull public List getParticipantSessions() + @org.jetbrains.annotations.NotNull public net.corda.core.utilities.ProgressTracker getProgressTracker() + public static final net.corda.core.flows.AbstractStateReplacementFlow$Instigator$Companion Companion +## +public static final class net.corda.core.flows.AbstractStateReplacementFlow$Instigator$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker tracker() +## +public static final class net.corda.core.flows.AbstractStateReplacementFlow$Instigator$Companion$NOTARY extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.AbstractStateReplacementFlow$Instigator$Companion$NOTARY INSTANCE +## +public static final class net.corda.core.flows.AbstractStateReplacementFlow$Instigator$Companion$SIGNING extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.AbstractStateReplacementFlow$Instigator$Companion$SIGNING INSTANCE +## +public static final class net.corda.core.flows.AbstractStateReplacementFlow$Proposal extends java.lang.Object + public (net.corda.core.contracts.StateRef, Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateRef component1() + public final Object component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.AbstractStateReplacementFlow$Proposal copy(net.corda.core.contracts.StateRef, Object) + public boolean equals(Object) + public final Object getModification() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateRef getStateRef() + public int hashCode() + public String toString() +## +public static final class net.corda.core.flows.AbstractStateReplacementFlow$UpgradeTx extends java.lang.Object + public (net.corda.core.transactions.SignedTransaction) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.AbstractStateReplacementFlow$UpgradeTx copy(net.corda.core.transactions.SignedTransaction) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction getStx() + public int hashCode() + public String toString() +## +public final class net.corda.core.flows.AppContext extends java.lang.Object + public (List) + @org.jetbrains.annotations.NotNull public final List component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.AppContext copy(List) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final List getAttachments() + @org.jetbrains.annotations.NotNull public final ClassLoader getClassLoader() + public int hashCode() + public String toString() +## +public final class net.corda.core.flows.CollectSignatureFlow extends net.corda.core.flows.FlowLogic + public (net.corda.core.transactions.SignedTransaction, net.corda.core.flows.FlowSession, List) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public List call() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction getPartiallySignedTx() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowSession getSession() + @org.jetbrains.annotations.NotNull public final List getSigningKeys() +## +public final class net.corda.core.flows.CollectSignaturesFlow extends net.corda.core.flows.FlowLogic + @kotlin.jvm.JvmOverloads public (net.corda.core.transactions.SignedTransaction, Collection) + @kotlin.jvm.JvmOverloads public (net.corda.core.transactions.SignedTransaction, Collection, Iterable) + @kotlin.jvm.JvmOverloads public (net.corda.core.transactions.SignedTransaction, Collection, Iterable, net.corda.core.utilities.ProgressTracker) + @kotlin.jvm.JvmOverloads public (net.corda.core.transactions.SignedTransaction, Collection, net.corda.core.utilities.ProgressTracker) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public net.corda.core.transactions.SignedTransaction call() + @org.jetbrains.annotations.Nullable public final Iterable getMyOptionalKeys() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction getPartiallySignedTx() + @org.jetbrains.annotations.NotNull public net.corda.core.utilities.ProgressTracker getProgressTracker() + @org.jetbrains.annotations.NotNull public final Collection getSessionsToCollectFrom() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.ProgressTracker tracker() + public static final net.corda.core.flows.CollectSignaturesFlow$Companion Companion +## +public static final class net.corda.core.flows.CollectSignaturesFlow$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker tracker() +## +public static final class net.corda.core.flows.CollectSignaturesFlow$Companion$COLLECTING extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.CollectSignaturesFlow$Companion$COLLECTING INSTANCE +## +public static final class net.corda.core.flows.CollectSignaturesFlow$Companion$VERIFYING extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.CollectSignaturesFlow$Companion$VERIFYING INSTANCE +## +public final class net.corda.core.flows.ContractUpgradeFlow extends java.lang.Object + public static final net.corda.core.flows.ContractUpgradeFlow INSTANCE +## +public static final class net.corda.core.flows.ContractUpgradeFlow$Authorise extends net.corda.core.flows.FlowLogic + public (net.corda.core.contracts.StateAndRef, Class) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.Nullable public Void call() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateAndRef getStateAndRef() +## +public static final class net.corda.core.flows.ContractUpgradeFlow$Deauthorise extends net.corda.core.flows.FlowLogic + public (net.corda.core.contracts.StateRef) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.Nullable public Void call() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateRef getStateRef() +## +public static final class net.corda.core.flows.ContractUpgradeFlow$Initiate extends net.corda.core.flows.AbstractStateReplacementFlow$Instigator + public (net.corda.core.contracts.StateAndRef, Class) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull protected net.corda.core.flows.AbstractStateReplacementFlow$UpgradeTx assembleTx() +## +public abstract class net.corda.core.flows.DataVendingFlow extends net.corda.core.flows.FlowLogic + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.Nullable public Void call() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowSession getOtherSideSession() + @org.jetbrains.annotations.NotNull public final Object getPayload() + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull protected net.corda.core.utilities.UntrustworthyData sendPayloadAndReceiveDataRequest(net.corda.core.flows.FlowSession, Object) + @co.paralleluniverse.fibers.Suspendable protected void verifyDataRequest(net.corda.core.internal.FetchDataFlow$Request$Data) +## +public final class net.corda.core.flows.FinalityFlow extends net.corda.core.flows.FlowLogic + public (net.corda.core.transactions.SignedTransaction) + public (net.corda.core.transactions.SignedTransaction, Set) + public (net.corda.core.transactions.SignedTransaction, Set, net.corda.core.utilities.ProgressTracker) + public (net.corda.core.transactions.SignedTransaction, net.corda.core.utilities.ProgressTracker) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public net.corda.core.transactions.SignedTransaction call() + @org.jetbrains.annotations.NotNull public net.corda.core.utilities.ProgressTracker getProgressTracker() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction getTransaction() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.ProgressTracker tracker() + public static final net.corda.core.flows.FinalityFlow$Companion Companion +## +public static final class net.corda.core.flows.FinalityFlow$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker tracker() +## +public static final class net.corda.core.flows.FinalityFlow$Companion$BROADCASTING extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.FinalityFlow$Companion$BROADCASTING INSTANCE +## +public static final class net.corda.core.flows.FinalityFlow$Companion$NOTARISING extends net.corda.core.utilities.ProgressTracker$Step + @org.jetbrains.annotations.NotNull public net.corda.core.utilities.ProgressTracker childProgressTracker() + public static final net.corda.core.flows.FinalityFlow$Companion$NOTARISING INSTANCE +## +public class net.corda.core.flows.FlowException extends net.corda.core.CordaException + public () + public (String) + public (String, Throwable) + public (Throwable) +## +public final class net.corda.core.flows.FlowInfo extends java.lang.Object + public (int, String) + public final int component1() + @org.jetbrains.annotations.NotNull public final String component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowInfo copy(int, String) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final String getAppName() + public final int getFlowVersion() + public int hashCode() + public String toString() +## +public abstract class net.corda.core.flows.FlowInitiator extends java.lang.Object implements java.security.Principal +## +public static final class net.corda.core.flows.FlowInitiator$Peer extends net.corda.core.flows.FlowInitiator + public (net.corda.core.identity.Party) + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowInitiator$Peer copy(net.corda.core.identity.Party) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public String getName() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party getParty() + public int hashCode() + public String toString() +## +public static final class net.corda.core.flows.FlowInitiator$RPC extends net.corda.core.flows.FlowInitiator + public (String) + @org.jetbrains.annotations.NotNull public final String component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowInitiator$RPC copy(String) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public String getName() + @org.jetbrains.annotations.NotNull public final String getUsername() + public int hashCode() + public String toString() +## +public static final class net.corda.core.flows.FlowInitiator$Scheduled extends net.corda.core.flows.FlowInitiator + public (net.corda.core.contracts.ScheduledStateRef) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ScheduledStateRef component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowInitiator$Scheduled copy(net.corda.core.contracts.ScheduledStateRef) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public String getName() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ScheduledStateRef getScheduledState() + public int hashCode() + public String toString() +## +public static final class net.corda.core.flows.FlowInitiator$Shell extends net.corda.core.flows.FlowInitiator + @org.jetbrains.annotations.NotNull public String getName() + public static final net.corda.core.flows.FlowInitiator$Shell INSTANCE +## +public abstract class net.corda.core.flows.FlowLogic extends java.lang.Object + public () + @co.paralleluniverse.fibers.Suspendable public abstract Object call() + public final void checkFlowPermission(String, Map) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.Nullable public final net.corda.core.flows.FlowStackSnapshot flowStackSnapshot() + @kotlin.Deprecated @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowInfo getFlowInfo(net.corda.core.identity.Party) + @org.jetbrains.annotations.NotNull public final org.slf4j.Logger getLogger() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party getOurIdentity() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.PartyAndCertificate getOurIdentityAndCert() + @org.jetbrains.annotations.Nullable public net.corda.core.utilities.ProgressTracker getProgressTracker() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StateMachineRunId getRunId() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.ServiceHub getServiceHub() + @org.jetbrains.annotations.NotNull public final net.corda.core.internal.FlowStateMachine getStateMachine() + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowSession initiateFlow(net.corda.core.identity.Party) + @co.paralleluniverse.fibers.Suspendable public final void persistFlowStackSnapshot() + @kotlin.Deprecated @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public net.corda.core.utilities.UntrustworthyData receive(Class, net.corda.core.identity.Party) + public final void recordAuditEvent(String, String, Map) + @kotlin.Deprecated @co.paralleluniverse.fibers.Suspendable public void send(net.corda.core.identity.Party, Object) + @kotlin.Deprecated @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public net.corda.core.utilities.UntrustworthyData sendAndReceive(Class, net.corda.core.identity.Party, Object) + public final void setStateMachine(net.corda.core.internal.FlowStateMachine) + @co.paralleluniverse.fibers.Suspendable public Object subFlow(net.corda.core.flows.FlowLogic) + @org.jetbrains.annotations.Nullable public final net.corda.core.messaging.DataFeed track() + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction waitForLedgerCommit(net.corda.core.crypto.SecureHash) +## +public interface net.corda.core.flows.FlowLogicRef +## +public interface net.corda.core.flows.FlowLogicRefFactory +## +public abstract class net.corda.core.flows.FlowSession extends java.lang.Object + public () + @org.jetbrains.annotations.NotNull public abstract net.corda.core.identity.Party getCounterparty() + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public abstract net.corda.core.flows.FlowInfo getCounterpartyFlowInfo() + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public abstract net.corda.core.utilities.UntrustworthyData receive(Class) + @co.paralleluniverse.fibers.Suspendable public abstract void send(Object) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public abstract net.corda.core.utilities.UntrustworthyData sendAndReceive(Class, Object) +## +public final class net.corda.core.flows.FlowStackSnapshot extends java.lang.Object + public (java.time.Instant, String, List) + @org.jetbrains.annotations.NotNull public final java.time.Instant component1() + @org.jetbrains.annotations.NotNull public final String component2() + @org.jetbrains.annotations.NotNull public final List component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowStackSnapshot copy(java.time.Instant, String, List) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final String getFlowClass() + @org.jetbrains.annotations.NotNull public final List getStackFrames() + @org.jetbrains.annotations.NotNull public final java.time.Instant getTime() + public int hashCode() + public String toString() +## +public static final class net.corda.core.flows.FlowStackSnapshot$Frame extends java.lang.Object + public (StackTraceElement, List) + @org.jetbrains.annotations.NotNull public final StackTraceElement component1() + @org.jetbrains.annotations.NotNull public final List component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowStackSnapshot$Frame copy(StackTraceElement, List) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final List getStackObjects() + @org.jetbrains.annotations.NotNull public final StackTraceElement getStackTraceElement() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() +## +public final class net.corda.core.flows.IllegalFlowLogicException extends java.lang.IllegalArgumentException + public (Class, String) +## +public @interface net.corda.core.flows.InitiatedBy + public abstract Class value() +## +public @interface net.corda.core.flows.InitiatingFlow + public abstract int version() +## +public final class net.corda.core.flows.NotaryChangeFlow extends net.corda.core.flows.AbstractStateReplacementFlow$Instigator + public (net.corda.core.contracts.StateAndRef, net.corda.core.identity.Party, net.corda.core.utilities.ProgressTracker) + @org.jetbrains.annotations.NotNull protected net.corda.core.flows.AbstractStateReplacementFlow$UpgradeTx assembleTx() +## +public abstract class net.corda.core.flows.NotaryError extends java.lang.Object +## +public static final class net.corda.core.flows.NotaryError$Conflict extends net.corda.core.flows.NotaryError + public (net.corda.core.crypto.SecureHash, net.corda.core.crypto.SignedData) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SignedData component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotaryError$Conflict copy(net.corda.core.crypto.SecureHash, net.corda.core.crypto.SignedData) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SignedData getConflict() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getTxId() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() +## +public static final class net.corda.core.flows.NotaryError$TimeWindowInvalid extends net.corda.core.flows.NotaryError + public static final net.corda.core.flows.NotaryError$TimeWindowInvalid INSTANCE +## +public static final class net.corda.core.flows.NotaryError$TransactionInvalid extends net.corda.core.flows.NotaryError + public (Throwable) + @org.jetbrains.annotations.NotNull public final Throwable component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotaryError$TransactionInvalid copy(Throwable) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final Throwable getCause() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() +## +public static final class net.corda.core.flows.NotaryError$WrongNotary extends net.corda.core.flows.NotaryError + public static final net.corda.core.flows.NotaryError$WrongNotary INSTANCE +## +public final class net.corda.core.flows.NotaryException extends net.corda.core.flows.FlowException + public (net.corda.core.flows.NotaryError) + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotaryError getError() +## +public final class net.corda.core.flows.NotaryFlow extends java.lang.Object + public () +## +public static class net.corda.core.flows.NotaryFlow$Client extends net.corda.core.flows.FlowLogic + public (net.corda.core.transactions.SignedTransaction) + public (net.corda.core.transactions.SignedTransaction, net.corda.core.utilities.ProgressTracker) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public List call() + @org.jetbrains.annotations.NotNull public net.corda.core.utilities.ProgressTracker getProgressTracker() + public static final net.corda.core.flows.NotaryFlow$Client$Companion Companion +## +public static final class net.corda.core.flows.NotaryFlow$Client$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker tracker() +## +public static final class net.corda.core.flows.NotaryFlow$Client$Companion$REQUESTING extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.NotaryFlow$Client$Companion$REQUESTING INSTANCE +## +public static final class net.corda.core.flows.NotaryFlow$Client$Companion$VALIDATING extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.NotaryFlow$Client$Companion$VALIDATING INSTANCE +## +public abstract static class net.corda.core.flows.NotaryFlow$Service extends net.corda.core.flows.FlowLogic + public (net.corda.core.flows.FlowSession, net.corda.core.node.services.TrustedAuthorityNotaryService) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.Nullable public Void call() + @co.paralleluniverse.fibers.Suspendable protected final void checkNotary(net.corda.core.identity.Party) + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowSession getOtherSideSession() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.TrustedAuthorityNotaryService getService() + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public abstract net.corda.core.flows.TransactionParts receiveAndVerifyTx() +## +public final class net.corda.core.flows.ReceiveStateAndRefFlow extends net.corda.core.flows.FlowLogic + public (net.corda.core.flows.FlowSession) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public List call() +## +public final class net.corda.core.flows.ReceiveTransactionFlow extends net.corda.core.flows.FlowLogic + public (net.corda.core.flows.FlowSession) + public (net.corda.core.flows.FlowSession, boolean) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public net.corda.core.transactions.SignedTransaction call() +## +public @interface net.corda.core.flows.SchedulableFlow +## +public class net.corda.core.flows.SendStateAndRefFlow extends net.corda.core.flows.DataVendingFlow + public (net.corda.core.flows.FlowSession, List) +## +public class net.corda.core.flows.SendTransactionFlow extends net.corda.core.flows.DataVendingFlow + public (net.corda.core.flows.FlowSession, net.corda.core.transactions.SignedTransaction) +## +public abstract class net.corda.core.flows.SignTransactionFlow extends net.corda.core.flows.FlowLogic + public (net.corda.core.flows.FlowSession, net.corda.core.utilities.ProgressTracker) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public net.corda.core.transactions.SignedTransaction call() + @co.paralleluniverse.fibers.Suspendable protected abstract void checkTransaction(net.corda.core.transactions.SignedTransaction) + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowSession getOtherSideSession() + @org.jetbrains.annotations.NotNull public net.corda.core.utilities.ProgressTracker getProgressTracker() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.ProgressTracker tracker() + public static final net.corda.core.flows.SignTransactionFlow$Companion Companion +## +public static final class net.corda.core.flows.SignTransactionFlow$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker tracker() +## +public static final class net.corda.core.flows.SignTransactionFlow$Companion$RECEIVING extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.SignTransactionFlow$Companion$RECEIVING INSTANCE +## +public static final class net.corda.core.flows.SignTransactionFlow$Companion$SIGNING extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.SignTransactionFlow$Companion$SIGNING INSTANCE +## +public static final class net.corda.core.flows.SignTransactionFlow$Companion$VERIFYING extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.SignTransactionFlow$Companion$VERIFYING INSTANCE +## +public final class net.corda.core.flows.StackFrameDataToken extends java.lang.Object + public (String) + @org.jetbrains.annotations.NotNull public final String component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StackFrameDataToken copy(String) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final String getClassName() + public int hashCode() + public String toString() +## +public @interface net.corda.core.flows.StartableByRPC +## +public final class net.corda.core.flows.StateMachineRunId extends java.lang.Object + public (UUID) + @org.jetbrains.annotations.NotNull public final UUID component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StateMachineRunId copy(UUID) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final UUID getUuid() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() + public static final net.corda.core.flows.StateMachineRunId$Companion Companion +## +public static final class net.corda.core.flows.StateMachineRunId$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StateMachineRunId createRandom() +## +public class net.corda.core.flows.StateReplacementException extends net.corda.core.flows.FlowException + @kotlin.jvm.JvmOverloads public () + @kotlin.jvm.JvmOverloads public (String) + @kotlin.jvm.JvmOverloads public (String, Throwable) +## +public final class net.corda.core.flows.TransactionParts extends java.lang.Object + public (net.corda.core.crypto.SecureHash, List, net.corda.core.contracts.TimeWindow, net.corda.core.identity.Party) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component1() + @org.jetbrains.annotations.NotNull public final List component2() + @org.jetbrains.annotations.Nullable public final net.corda.core.contracts.TimeWindow component3() + @org.jetbrains.annotations.Nullable public final net.corda.core.identity.Party component4() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.TransactionParts copy(net.corda.core.crypto.SecureHash, List, net.corda.core.contracts.TimeWindow, net.corda.core.identity.Party) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getId() + @org.jetbrains.annotations.NotNull public final List getInputs() + @org.jetbrains.annotations.Nullable public final net.corda.core.identity.Party getNotary() + @org.jetbrains.annotations.Nullable public final net.corda.core.contracts.TimeWindow getTimestamp() + public int hashCode() + public String toString() +## +public final class net.corda.core.flows.UnexpectedFlowEndException extends net.corda.core.CordaRuntimeException + public (String) + public (String, Throwable) +## +public abstract class net.corda.core.identity.AbstractParty extends java.lang.Object + public (java.security.PublicKey) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final java.security.PublicKey getOwningKey() + public int hashCode() + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.CordaX500Name nameOrNull() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.PartyAndReference ref(net.corda.core.utilities.OpaqueBytes) +## +public final class net.corda.core.identity.AnonymousParty extends net.corda.core.identity.AbstractParty + public (java.security.PublicKey) + @org.jetbrains.annotations.Nullable public net.corda.core.identity.CordaX500Name nameOrNull() + @org.jetbrains.annotations.NotNull public net.corda.core.contracts.PartyAndReference ref(net.corda.core.utilities.OpaqueBytes) + @org.jetbrains.annotations.NotNull public String toString() +## +public final class net.corda.core.identity.CordaX500Name extends java.lang.Object + public (String, String, String) + public (String, String, String, String) + public (String, String, String, String, String, String) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.identity.CordaX500Name build(javax.security.auth.x500.X500Principal) + @org.jetbrains.annotations.Nullable public final String component1() + @org.jetbrains.annotations.Nullable public final String component2() + @org.jetbrains.annotations.NotNull public final String component3() + @org.jetbrains.annotations.NotNull public final String component4() + @org.jetbrains.annotations.Nullable public final String component5() + @org.jetbrains.annotations.NotNull public final String component6() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.CordaX500Name copy(String, String, String, String, String, String) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public final String getCommonName() + @org.jetbrains.annotations.NotNull public final String getCountry() + @org.jetbrains.annotations.NotNull public final String getLocality() + @org.jetbrains.annotations.NotNull public final String getOrganisation() + @org.jetbrains.annotations.Nullable public final String getOrganisationUnit() + @org.jetbrains.annotations.Nullable public final String getState() + @org.jetbrains.annotations.NotNull public final javax.security.auth.x500.X500Principal getX500Principal() + public int hashCode() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.identity.CordaX500Name parse(String) + @org.jetbrains.annotations.NotNull public String toString() + public static final net.corda.core.identity.CordaX500Name$Companion Companion + public static final int LENGTH_COUNTRY = 2 + public static final int MAX_LENGTH_COMMON_NAME = 64 + public static final int MAX_LENGTH_LOCALITY = 64 + public static final int MAX_LENGTH_ORGANISATION = 128 + public static final int MAX_LENGTH_ORGANISATION_UNIT = 64 + public static final int MAX_LENGTH_STATE = 64 +## +public static final class net.corda.core.identity.CordaX500Name$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.identity.CordaX500Name build(javax.security.auth.x500.X500Principal) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.identity.CordaX500Name parse(String) +## +public final class net.corda.core.identity.IdentityUtils extends java.lang.Object + @org.jetbrains.annotations.NotNull public static final Map excludeHostNode(net.corda.core.node.ServiceHub, Map) + @org.jetbrains.annotations.NotNull public static final Map excludeNotary(Map, net.corda.core.transactions.SignedTransaction) + @org.jetbrains.annotations.NotNull public static final Map groupAbstractPartyByWellKnownParty(net.corda.core.node.ServiceHub, Collection) + @org.jetbrains.annotations.NotNull public static final Map groupAbstractPartyByWellKnownParty(net.corda.core.node.ServiceHub, Collection, boolean) + @org.jetbrains.annotations.NotNull public static final Map groupPublicKeysByWellKnownParty(net.corda.core.node.ServiceHub, Collection) + @org.jetbrains.annotations.NotNull public static final Map groupPublicKeysByWellKnownParty(net.corda.core.node.ServiceHub, Collection, boolean) +## +public final class net.corda.core.identity.Party extends net.corda.core.identity.AbstractParty + public (java.security.cert.X509Certificate) + public (net.corda.core.identity.CordaX500Name, java.security.PublicKey) + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.AnonymousParty anonymise() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.CordaX500Name getName() + @org.jetbrains.annotations.NotNull public net.corda.core.identity.CordaX500Name nameOrNull() + @org.jetbrains.annotations.NotNull public net.corda.core.contracts.PartyAndReference ref(net.corda.core.utilities.OpaqueBytes) + @org.jetbrains.annotations.NotNull public String toString() +## +public final class net.corda.core.identity.PartyAndCertificate extends java.lang.Object + public (java.security.cert.CertPath) + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component1() + @org.jetbrains.annotations.NotNull public final java.security.cert.X509Certificate component2() + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final java.security.cert.CertPath getCertPath() + @org.jetbrains.annotations.NotNull public final java.security.cert.X509Certificate getCertificate() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.CordaX500Name getName() + @org.jetbrains.annotations.NotNull public final java.security.PublicKey getOwningKey() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party getParty() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() + @org.jetbrains.annotations.NotNull public final java.security.cert.PKIXCertPathValidatorResult verify(java.security.cert.TrustAnchor) +## +public interface net.corda.core.messaging.AllPossibleRecipients extends net.corda.core.messaging.MessageRecipients +## +public interface net.corda.core.messaging.CordaRPCOps extends net.corda.core.messaging.RPCOps + public abstract void addVaultTransactionNote(net.corda.core.crypto.SecureHash, String) + public abstract boolean attachmentExists(net.corda.core.crypto.SecureHash) + public abstract void clearNetworkMapCache() + @org.jetbrains.annotations.NotNull public abstract java.time.Instant currentNodeTime() + public abstract int getProtocolVersion() + @org.jetbrains.annotations.NotNull public abstract Iterable getVaultTransactionNotes(net.corda.core.crypto.SecureHash) + @kotlin.Deprecated @net.corda.core.messaging.RPCReturnsObservables @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed internalVerifiedTransactionsFeed() + @kotlin.Deprecated @org.jetbrains.annotations.NotNull public abstract List internalVerifiedTransactionsSnapshot() + @net.corda.core.messaging.RPCReturnsObservables @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed networkMapFeed() + @org.jetbrains.annotations.NotNull public abstract List networkMapSnapshot() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.NodeInfo nodeInfo() + @org.jetbrains.annotations.Nullable public abstract net.corda.core.node.NodeInfo nodeInfoFromParty(net.corda.core.identity.AbstractParty) + @org.jetbrains.annotations.NotNull public abstract List notaryIdentities() + @org.jetbrains.annotations.NotNull public abstract java.io.InputStream openAttachment(net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public abstract Set partiesFromName(String, boolean) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party partyFromKey(java.security.PublicKey) + @org.jetbrains.annotations.NotNull public abstract List registeredFlows() + @net.corda.core.messaging.RPCReturnsObservables @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed stateMachineRecordedTransactionMappingFeed() + @org.jetbrains.annotations.NotNull public abstract List stateMachineRecordedTransactionMappingSnapshot() + @net.corda.core.messaging.RPCReturnsObservables @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed stateMachinesFeed() + @org.jetbrains.annotations.NotNull public abstract List stateMachinesSnapshot() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.SecureHash uploadAttachment(java.io.InputStream) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page vaultQuery(Class) + @net.corda.core.messaging.RPCReturnsObservables @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page vaultQueryBy(net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.PageSpecification, net.corda.core.node.services.vault.Sort, Class) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page vaultQueryByCriteria(net.corda.core.node.services.vault.QueryCriteria, Class) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page vaultQueryByWithPagingSpec(Class, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.PageSpecification) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page vaultQueryByWithSorting(Class, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.Sort) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed vaultTrack(Class) + @net.corda.core.messaging.RPCReturnsObservables @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed vaultTrackBy(net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.PageSpecification, net.corda.core.node.services.vault.Sort, Class) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed vaultTrackByCriteria(Class, net.corda.core.node.services.vault.QueryCriteria) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed vaultTrackByWithPagingSpec(Class, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.PageSpecification) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed vaultTrackByWithSorting(Class, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.Sort) + @net.corda.core.messaging.RPCReturnsObservables @org.jetbrains.annotations.NotNull public abstract net.corda.core.concurrent.CordaFuture waitUntilNetworkReady() + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party wellKnownPartyFromAnonymous(net.corda.core.identity.AbstractParty) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party wellKnownPartyFromX500Name(net.corda.core.identity.CordaX500Name) +## +public final class net.corda.core.messaging.CordaRPCOpsKt extends java.lang.Object +## +public final class net.corda.core.messaging.DataFeed extends java.lang.Object + public (Object, rx.Observable) + public final Object component1() + @org.jetbrains.annotations.NotNull public final rx.Observable component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.messaging.DataFeed copy(Object, rx.Observable) + public boolean equals(Object) + public final Object getSnapshot() + @org.jetbrains.annotations.NotNull public final rx.Observable getUpdates() + public int hashCode() + public String toString() +## +public interface net.corda.core.messaging.FlowHandle extends java.lang.AutoCloseable + public abstract void close() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.flows.StateMachineRunId getId() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.concurrent.CordaFuture getReturnValue() +## +public final class net.corda.core.messaging.FlowHandleImpl extends java.lang.Object implements net.corda.core.messaging.FlowHandle + public (net.corda.core.flows.StateMachineRunId, net.corda.core.concurrent.CordaFuture) + public void close() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StateMachineRunId component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.concurrent.CordaFuture component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.messaging.FlowHandleImpl copy(net.corda.core.flows.StateMachineRunId, net.corda.core.concurrent.CordaFuture) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.flows.StateMachineRunId getId() + @org.jetbrains.annotations.NotNull public net.corda.core.concurrent.CordaFuture getReturnValue() + public int hashCode() + public String toString() +## +public interface net.corda.core.messaging.FlowProgressHandle extends net.corda.core.messaging.FlowHandle + public abstract void close() + @org.jetbrains.annotations.NotNull public abstract rx.Observable getProgress() +## +public final class net.corda.core.messaging.FlowProgressHandleImpl extends java.lang.Object implements net.corda.core.messaging.FlowProgressHandle + public (net.corda.core.flows.StateMachineRunId, net.corda.core.concurrent.CordaFuture, rx.Observable) + public void close() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StateMachineRunId component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.concurrent.CordaFuture component2() + @org.jetbrains.annotations.NotNull public final rx.Observable component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.messaging.FlowProgressHandleImpl copy(net.corda.core.flows.StateMachineRunId, net.corda.core.concurrent.CordaFuture, rx.Observable) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.flows.StateMachineRunId getId() + @org.jetbrains.annotations.NotNull public rx.Observable getProgress() + @org.jetbrains.annotations.NotNull public net.corda.core.concurrent.CordaFuture getReturnValue() + public int hashCode() + public String toString() +## +public interface net.corda.core.messaging.MessageRecipientGroup extends net.corda.core.messaging.MessageRecipients +## +public interface net.corda.core.messaging.MessageRecipients +## +public interface net.corda.core.messaging.RPCOps + public abstract int getProtocolVersion() +## +public @interface net.corda.core.messaging.RPCReturnsObservables +## +public interface net.corda.core.messaging.SingleMessageRecipient extends net.corda.core.messaging.MessageRecipients +## +public final class net.corda.core.messaging.StateMachineInfo extends java.lang.Object + public (net.corda.core.flows.StateMachineRunId, String, net.corda.core.flows.FlowInitiator, net.corda.core.messaging.DataFeed) + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StateMachineRunId component1() + @org.jetbrains.annotations.NotNull public final String component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowInitiator component3() + @org.jetbrains.annotations.Nullable public final net.corda.core.messaging.DataFeed component4() + @org.jetbrains.annotations.NotNull public final net.corda.core.messaging.StateMachineInfo copy(net.corda.core.flows.StateMachineRunId, String, net.corda.core.flows.FlowInitiator, net.corda.core.messaging.DataFeed) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final String getFlowLogicClassName() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StateMachineRunId getId() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowInitiator getInitiator() + @org.jetbrains.annotations.Nullable public final net.corda.core.messaging.DataFeed getProgressTrackerStepAndUpdates() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() +## +public final class net.corda.core.messaging.StateMachineTransactionMapping extends java.lang.Object + public (net.corda.core.flows.StateMachineRunId, net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StateMachineRunId component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.messaging.StateMachineTransactionMapping copy(net.corda.core.flows.StateMachineRunId, net.corda.core.crypto.SecureHash) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StateMachineRunId getStateMachineRunId() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getTransactionId() + public int hashCode() + public String toString() +## +public abstract class net.corda.core.messaging.StateMachineUpdate extends java.lang.Object + @org.jetbrains.annotations.NotNull public abstract net.corda.core.flows.StateMachineRunId getId() +## +public static final class net.corda.core.messaging.StateMachineUpdate$Added extends net.corda.core.messaging.StateMachineUpdate + public (net.corda.core.messaging.StateMachineInfo) + @org.jetbrains.annotations.NotNull public final net.corda.core.messaging.StateMachineInfo component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.messaging.StateMachineUpdate$Added copy(net.corda.core.messaging.StateMachineInfo) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.flows.StateMachineRunId getId() + @org.jetbrains.annotations.NotNull public final net.corda.core.messaging.StateMachineInfo getStateMachineInfo() + public int hashCode() + public String toString() +## +public static final class net.corda.core.messaging.StateMachineUpdate$Removed extends net.corda.core.messaging.StateMachineUpdate + public (net.corda.core.flows.StateMachineRunId, net.corda.core.utilities.Try) + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StateMachineRunId component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.Try component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.messaging.StateMachineUpdate$Removed copy(net.corda.core.flows.StateMachineRunId, net.corda.core.utilities.Try) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.flows.StateMachineRunId getId() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.Try getResult() + public int hashCode() + public String toString() +## +public abstract class net.corda.core.node.CordaPluginRegistry extends java.lang.Object + public () + public boolean customizeSerialization(net.corda.core.serialization.SerializationCustomization) +## +public final class net.corda.core.node.NodeInfo extends java.lang.Object + public (List, List, int, long) + @org.jetbrains.annotations.NotNull public final List component1() + @org.jetbrains.annotations.NotNull public final List component2() + public final int component3() + public final long component4() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NodeInfo copy(List, List, int, long) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final List getAddresses() + @org.jetbrains.annotations.NotNull public final List getLegalIdentities() + @org.jetbrains.annotations.NotNull public final List getLegalIdentitiesAndCerts() + public final int getPlatformVersion() + public final long getSerial() + public int hashCode() + public final boolean isLegalIdentity(net.corda.core.identity.Party) + public String toString() +## +public interface net.corda.core.node.ServiceHub extends net.corda.core.node.ServicesForResolution + @org.jetbrains.annotations.NotNull public abstract net.corda.core.transactions.SignedTransaction addSignature(net.corda.core.transactions.SignedTransaction) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.transactions.SignedTransaction addSignature(net.corda.core.transactions.SignedTransaction, java.security.PublicKey) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializeAsToken cordaService(Class) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.FilteredTransaction) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.FilteredTransaction, java.security.PublicKey) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.SignedTransaction) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.SignedTransaction, java.security.PublicKey) + @org.jetbrains.annotations.NotNull public abstract java.time.Clock getClock() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.ContractUpgradeService getContractUpgradeService() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.KeyManagementService getKeyManagementService() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.NodeInfo getMyInfo() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.NetworkMapCache getNetworkMapCache() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.TransactionVerifierService getTransactionVerifierService() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.TransactionStorage getValidatedTransactions() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.VaultService getVaultService() + @org.jetbrains.annotations.NotNull public abstract java.sql.Connection jdbcSession() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.TransactionState loadState(net.corda.core.contracts.StateRef) + public abstract void recordTransactions(Iterable) + public abstract void recordTransactions(boolean, Iterable) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.transactions.SignedTransaction signInitialTransaction(net.corda.core.transactions.TransactionBuilder) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.transactions.SignedTransaction signInitialTransaction(net.corda.core.transactions.TransactionBuilder, Iterable) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.transactions.SignedTransaction signInitialTransaction(net.corda.core.transactions.TransactionBuilder, java.security.PublicKey) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.StateAndRef toStateAndRef(net.corda.core.contracts.StateRef) +## +public interface net.corda.core.node.ServicesForResolution + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.AttachmentStorage getAttachments() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.cordapp.CordappProvider getCordappProvider() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.IdentityService getIdentityService() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.TransactionState loadState(net.corda.core.contracts.StateRef) +## +public interface net.corda.core.node.services.AttachmentStorage + @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.SecureHash importAttachment(java.io.InputStream) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.contracts.Attachment openAttachment(net.corda.core.crypto.SecureHash) +## +public final class net.corda.core.node.services.AttachmentStorageKt extends java.lang.Object +## +public interface net.corda.core.node.services.ContractUpgradeService + @org.jetbrains.annotations.Nullable public abstract String getAuthorisedContractUpgrade(net.corda.core.contracts.StateRef) + public abstract void removeAuthorisedContractUpgrade(net.corda.core.contracts.StateRef) + public abstract void storeAuthorisedContractUpgrade(net.corda.core.contracts.StateRef, Class) +## +public @interface net.corda.core.node.services.CordaService +## +public interface net.corda.core.node.services.IdentityService + public abstract void assertOwnership(net.corda.core.identity.Party, net.corda.core.identity.AnonymousParty) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.PartyAndCertificate certificateFromKey(java.security.PublicKey) + @org.jetbrains.annotations.NotNull public abstract Iterable getAllIdentities() + @org.jetbrains.annotations.NotNull public abstract java.security.cert.CertStore getCaCertStore() + @org.jetbrains.annotations.NotNull public abstract java.security.cert.TrustAnchor getTrustAnchor() + @org.jetbrains.annotations.NotNull public abstract java.security.cert.X509Certificate getTrustRoot() + @org.jetbrains.annotations.NotNull public abstract Set partiesFromName(String, boolean) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party partyFromKey(java.security.PublicKey) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.identity.Party requireWellKnownPartyFromAnonymous(net.corda.core.identity.AbstractParty) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.PartyAndCertificate verifyAndRegisterIdentity(net.corda.core.identity.PartyAndCertificate) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party wellKnownPartyFromAnonymous(net.corda.core.contracts.PartyAndReference) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party wellKnownPartyFromAnonymous(net.corda.core.identity.AbstractParty) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party wellKnownPartyFromX500Name(net.corda.core.identity.CordaX500Name) +## +public interface net.corda.core.node.services.KeyManagementService + @org.jetbrains.annotations.NotNull public abstract Iterable filterMyKeys(Iterable) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public abstract java.security.PublicKey freshKey() + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public abstract net.corda.core.identity.PartyAndCertificate freshKeyAndCert(net.corda.core.identity.PartyAndCertificate, boolean) + @org.jetbrains.annotations.NotNull public abstract Set getKeys() + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.TransactionSignature sign(net.corda.core.crypto.SignableData, java.security.PublicKey) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.DigitalSignature$WithKey sign(byte[], java.security.PublicKey) +## +public interface net.corda.core.node.services.NetworkMapCache + public abstract void clearNetworkMapCache() + @org.jetbrains.annotations.NotNull public abstract List getAllNodes() + @org.jetbrains.annotations.NotNull public abstract rx.Observable getChanged() + @org.jetbrains.annotations.Nullable public abstract net.corda.core.node.NodeInfo getNodeByAddress(net.corda.core.utilities.NetworkHostAndPort) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.node.NodeInfo getNodeByLegalIdentity(net.corda.core.identity.AbstractParty) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.node.NodeInfo getNodeByLegalName(net.corda.core.identity.CordaX500Name) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.concurrent.CordaFuture getNodeReady() + @org.jetbrains.annotations.NotNull public abstract List getNodesByLegalIdentityKey(java.security.PublicKey) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party getNotary(net.corda.core.identity.CordaX500Name) + @org.jetbrains.annotations.NotNull public abstract List getNotaryIdentities() + @org.jetbrains.annotations.Nullable public abstract net.corda.core.node.services.PartyInfo getPartyInfo(net.corda.core.identity.Party) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party getPeerByLegalName(net.corda.core.identity.CordaX500Name) + public abstract boolean isNotary(net.corda.core.identity.Party) + public abstract boolean isValidatingNotary(net.corda.core.identity.Party) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed track() +## +public abstract static class net.corda.core.node.services.NetworkMapCache$MapChange extends java.lang.Object + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.NodeInfo getNode() +## +public static final class net.corda.core.node.services.NetworkMapCache$MapChange$Added extends net.corda.core.node.services.NetworkMapCache$MapChange + public (net.corda.core.node.NodeInfo) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NodeInfo component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.NetworkMapCache$MapChange$Added copy(net.corda.core.node.NodeInfo) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.node.NodeInfo getNode() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.NetworkMapCache$MapChange$Modified extends net.corda.core.node.services.NetworkMapCache$MapChange + public (net.corda.core.node.NodeInfo, net.corda.core.node.NodeInfo) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NodeInfo component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NodeInfo component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.NetworkMapCache$MapChange$Modified copy(net.corda.core.node.NodeInfo, net.corda.core.node.NodeInfo) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.node.NodeInfo getNode() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NodeInfo getPreviousNode() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.NetworkMapCache$MapChange$Removed extends net.corda.core.node.services.NetworkMapCache$MapChange + public (net.corda.core.node.NodeInfo) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NodeInfo component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.NetworkMapCache$MapChange$Removed copy(net.corda.core.node.NodeInfo) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.node.NodeInfo getNode() + public int hashCode() + public String toString() +## +public abstract class net.corda.core.node.services.NotaryService extends net.corda.core.serialization.SingletonSerializeAsToken + public () + @org.jetbrains.annotations.NotNull public abstract net.corda.core.flows.FlowLogic createServiceFlow(net.corda.core.flows.FlowSession) + @org.jetbrains.annotations.NotNull public abstract java.security.PublicKey getNotaryIdentityKey() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.ServiceHub getServices() + public abstract void start() + public abstract void stop() +## +public abstract class net.corda.core.node.services.PartyInfo extends java.lang.Object + @org.jetbrains.annotations.NotNull public abstract net.corda.core.identity.Party getParty() +## +public static final class net.corda.core.node.services.PartyInfo$DistributedNode extends net.corda.core.node.services.PartyInfo + public (net.corda.core.identity.Party) + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.PartyInfo$DistributedNode copy(net.corda.core.identity.Party) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.identity.Party getParty() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.PartyInfo$SingleNode extends net.corda.core.node.services.PartyInfo + public (net.corda.core.identity.Party, List) + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component1() + @org.jetbrains.annotations.NotNull public final List component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.PartyInfo$SingleNode copy(net.corda.core.identity.Party, List) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final List getAddresses() + @org.jetbrains.annotations.NotNull public net.corda.core.identity.Party getParty() + public int hashCode() + public String toString() +## +public final class net.corda.core.node.services.StatesNotAvailableException extends net.corda.core.flows.FlowException + public (String, Throwable) + @org.jetbrains.annotations.Nullable public Throwable getCause() + @org.jetbrains.annotations.Nullable public String getMessage() + @org.jetbrains.annotations.NotNull public String toString() +## +public final class net.corda.core.node.services.TimeWindowChecker extends java.lang.Object + public () + public (java.time.Clock) + @org.jetbrains.annotations.NotNull public final java.time.Clock getClock() + public final boolean isValid(net.corda.core.contracts.TimeWindow) +## +public interface net.corda.core.node.services.TransactionStorage + @org.jetbrains.annotations.Nullable public abstract net.corda.core.transactions.SignedTransaction getTransaction(net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public abstract rx.Observable getUpdates() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed track() +## +public interface net.corda.core.node.services.TransactionVerifierService + @org.jetbrains.annotations.NotNull public abstract net.corda.core.concurrent.CordaFuture verify(net.corda.core.transactions.LedgerTransaction) +## +public abstract class net.corda.core.node.services.TrustedAuthorityNotaryService extends net.corda.core.node.services.NotaryService + public () + public final void commitInputStates(List, net.corda.core.crypto.SecureHash, net.corda.core.identity.Party) + @org.jetbrains.annotations.NotNull protected org.slf4j.Logger getLog() + @org.jetbrains.annotations.NotNull protected abstract net.corda.core.node.services.TimeWindowChecker getTimeWindowChecker() + @org.jetbrains.annotations.NotNull protected abstract net.corda.core.node.services.UniquenessProvider getUniquenessProvider() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.TransactionSignature sign(net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.DigitalSignature$WithKey sign(byte[]) + public final void validateTimeWindow(net.corda.core.contracts.TimeWindow) +## +public final class net.corda.core.node.services.UniquenessException extends net.corda.core.CordaException + public (net.corda.core.node.services.UniquenessProvider$Conflict) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.UniquenessProvider$Conflict getError() +## +public interface net.corda.core.node.services.UniquenessProvider + public abstract void commit(List, net.corda.core.crypto.SecureHash, net.corda.core.identity.Party) +## +public static final class net.corda.core.node.services.UniquenessProvider$Conflict extends java.lang.Object + public (Map) + @org.jetbrains.annotations.NotNull public final Map component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.UniquenessProvider$Conflict copy(Map) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final Map getStateHistory() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.UniquenessProvider$ConsumingTx extends java.lang.Object + public (net.corda.core.crypto.SecureHash, int, net.corda.core.identity.Party) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component1() + public final int component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.UniquenessProvider$ConsumingTx copy(net.corda.core.crypto.SecureHash, int, net.corda.core.identity.Party) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getId() + public final int getInputIndex() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party getRequestingParty() + public int hashCode() + public String toString() +## +public final class net.corda.core.node.services.UnknownAnonymousPartyException extends net.corda.core.CordaException + public (String) +## +public final class net.corda.core.node.services.Vault extends java.lang.Object + public (Iterable) + @org.jetbrains.annotations.NotNull public final Iterable getStates() + public static final net.corda.core.node.services.Vault$Companion Companion +## +public static final class net.corda.core.node.services.Vault$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$Update getNoNotaryUpdate() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$Update getNoUpdate() +## +public static final class net.corda.core.node.services.Vault$Page extends java.lang.Object + public (List, List, long, net.corda.core.node.services.Vault$StateStatus, List) + @org.jetbrains.annotations.NotNull public final List component1() + @org.jetbrains.annotations.NotNull public final List component2() + public final long component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$StateStatus component4() + @org.jetbrains.annotations.NotNull public final List component5() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$Page copy(List, List, long, net.corda.core.node.services.Vault$StateStatus, List) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final List getOtherResults() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$StateStatus getStateTypes() + @org.jetbrains.annotations.NotNull public final List getStates() + @org.jetbrains.annotations.NotNull public final List getStatesMetadata() + public final long getTotalStatesAvailable() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.Vault$StateMetadata extends java.lang.Object + public (net.corda.core.contracts.StateRef, String, java.time.Instant, java.time.Instant, net.corda.core.node.services.Vault$StateStatus, net.corda.core.identity.AbstractParty, String, java.time.Instant) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateRef component1() + @org.jetbrains.annotations.NotNull public final String component2() + @org.jetbrains.annotations.NotNull public final java.time.Instant component3() + @org.jetbrains.annotations.Nullable public final java.time.Instant component4() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$StateStatus component5() + @org.jetbrains.annotations.Nullable public final net.corda.core.identity.AbstractParty component6() + @org.jetbrains.annotations.Nullable public final String component7() + @org.jetbrains.annotations.Nullable public final java.time.Instant component8() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$StateMetadata copy(net.corda.core.contracts.StateRef, String, java.time.Instant, java.time.Instant, net.corda.core.node.services.Vault$StateStatus, net.corda.core.identity.AbstractParty, String, java.time.Instant) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public final java.time.Instant getConsumedTime() + @org.jetbrains.annotations.NotNull public final String getContractStateClassName() + @org.jetbrains.annotations.Nullable public final String getLockId() + @org.jetbrains.annotations.Nullable public final java.time.Instant getLockUpdateTime() + @org.jetbrains.annotations.Nullable public final net.corda.core.identity.AbstractParty getNotary() + @org.jetbrains.annotations.NotNull public final java.time.Instant getRecordedTime() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateRef getRef() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$StateStatus getStatus() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.Vault$StateStatus extends java.lang.Enum + protected (String, int) + public static net.corda.core.node.services.Vault$StateStatus valueOf(String) + public static net.corda.core.node.services.Vault$StateStatus[] values() +## +public static final class net.corda.core.node.services.Vault$Update extends java.lang.Object + public (Set, Set, UUID, net.corda.core.node.services.Vault$UpdateType) + @org.jetbrains.annotations.NotNull public final Set component1() + @org.jetbrains.annotations.NotNull public final Set component2() + @org.jetbrains.annotations.Nullable public final UUID component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$UpdateType component4() + public final boolean containsType(Class, net.corda.core.node.services.Vault$StateStatus) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$Update copy(Set, Set, UUID, net.corda.core.node.services.Vault$UpdateType) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final Set getConsumed() + @org.jetbrains.annotations.Nullable public final UUID getFlowId() + @org.jetbrains.annotations.NotNull public final Set getProduced() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$UpdateType getType() + public int hashCode() + public final boolean isEmpty() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$Update plus(net.corda.core.node.services.Vault$Update) + @org.jetbrains.annotations.NotNull public String toString() +## +public static final class net.corda.core.node.services.Vault$UpdateType extends java.lang.Enum + protected (String, int) + public static net.corda.core.node.services.Vault$UpdateType valueOf(String) + public static net.corda.core.node.services.Vault$UpdateType[] values() +## +public final class net.corda.core.node.services.VaultQueryException extends net.corda.core.flows.FlowException + public (String) +## +public interface net.corda.core.node.services.VaultService + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page _queryBy(net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.PageSpecification, net.corda.core.node.services.vault.Sort, Class) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed _trackBy(net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.PageSpecification, net.corda.core.node.services.vault.Sort, Class) + public abstract void addNoteToTransaction(net.corda.core.crypto.SecureHash, String) + @org.jetbrains.annotations.NotNull public abstract rx.Observable getRawUpdates() + @org.jetbrains.annotations.NotNull public abstract Iterable getTransactionNotes(net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public abstract rx.Observable getUpdates() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page queryBy(Class) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page queryBy(Class, net.corda.core.node.services.vault.QueryCriteria) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page queryBy(Class, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.PageSpecification) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page queryBy(Class, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.PageSpecification, net.corda.core.node.services.vault.Sort) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page queryBy(Class, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.Sort) + public abstract void softLockRelease(UUID, net.corda.core.utilities.NonEmptySet) + public abstract void softLockReserve(UUID, net.corda.core.utilities.NonEmptySet) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed trackBy(Class) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed trackBy(Class, net.corda.core.node.services.vault.QueryCriteria) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed trackBy(Class, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.PageSpecification) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed trackBy(Class, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.PageSpecification, net.corda.core.node.services.vault.Sort) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed trackBy(Class, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.Sort) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public abstract List tryLockFungibleStatesForSpending(UUID, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.contracts.Amount, Class) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.concurrent.CordaFuture whenConsumed(net.corda.core.contracts.StateRef) +## +public final class net.corda.core.node.services.VaultServiceKt extends java.lang.Object +## +public final class net.corda.core.node.services.vault.AggregateFunctionType extends java.lang.Enum + protected (String, int) + public static net.corda.core.node.services.vault.AggregateFunctionType valueOf(String) + public static net.corda.core.node.services.vault.AggregateFunctionType[] values() +## +public final class net.corda.core.node.services.vault.BinaryComparisonOperator extends java.lang.Enum implements net.corda.core.node.services.vault.Operator + protected (String, int) + public static net.corda.core.node.services.vault.BinaryComparisonOperator valueOf(String) + public static net.corda.core.node.services.vault.BinaryComparisonOperator[] values() +## +public final class net.corda.core.node.services.vault.BinaryLogicalOperator extends java.lang.Enum implements net.corda.core.node.services.vault.Operator + protected (String, int) + public static net.corda.core.node.services.vault.BinaryLogicalOperator valueOf(String) + public static net.corda.core.node.services.vault.BinaryLogicalOperator[] values() +## +public final class net.corda.core.node.services.vault.Builder extends java.lang.Object + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression avg(reflect.Field) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression avg(reflect.Field, List) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression avg(reflect.Field, List, net.corda.core.node.services.vault.Sort$Direction) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression avg(kotlin.reflect.KProperty1, List, net.corda.core.node.services.vault.Sort$Direction) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$Between between(Comparable, Comparable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression between(reflect.Field, Comparable, Comparable) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression between(kotlin.reflect.KProperty1, Comparable, Comparable) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$BinaryComparison compare(net.corda.core.node.services.vault.BinaryComparisonOperator, Comparable) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression comparePredicate(reflect.Field, net.corda.core.node.services.vault.BinaryComparisonOperator, Comparable) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression comparePredicate(kotlin.reflect.KProperty1, net.corda.core.node.services.vault.BinaryComparisonOperator, Comparable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression count(reflect.Field) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression count(kotlin.reflect.KProperty1) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$EqualityComparison equal(Object) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression equal(reflect.Field, Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression equal(kotlin.reflect.KProperty1, Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression functionPredicate(reflect.Field, net.corda.core.node.services.vault.ColumnPredicate, List, net.corda.core.node.services.vault.Sort$Direction) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression functionPredicate(kotlin.reflect.KProperty1, net.corda.core.node.services.vault.ColumnPredicate, List, net.corda.core.node.services.vault.Sort$Direction) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$BinaryComparison greaterThan(Comparable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression greaterThan(reflect.Field, Comparable) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression greaterThan(kotlin.reflect.KProperty1, Comparable) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$BinaryComparison greaterThanOrEqual(Comparable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression greaterThanOrEqual(reflect.Field, Comparable) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression greaterThanOrEqual(kotlin.reflect.KProperty1, Comparable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression in(reflect.Field, Collection) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$CollectionExpression in(Collection) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression in(kotlin.reflect.KProperty1, Collection) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression isNull(reflect.Field) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression isNull(kotlin.reflect.KProperty1) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$BinaryComparison lessThan(Comparable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression lessThan(reflect.Field, Comparable) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression lessThan(kotlin.reflect.KProperty1, Comparable) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$BinaryComparison lessThanOrEqual(Comparable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression lessThanOrEqual(reflect.Field, Comparable) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression lessThanOrEqual(kotlin.reflect.KProperty1, Comparable) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression like(reflect.Field, String) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression like(kotlin.reflect.KProperty1, String) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression max(reflect.Field) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression max(reflect.Field, List) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression max(reflect.Field, List, net.corda.core.node.services.vault.Sort$Direction) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression max(kotlin.reflect.KProperty1, List, net.corda.core.node.services.vault.Sort$Direction) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression min(reflect.Field) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression min(reflect.Field, List) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression min(reflect.Field, List, net.corda.core.node.services.vault.Sort$Direction) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression min(kotlin.reflect.KProperty1, List, net.corda.core.node.services.vault.Sort$Direction) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$EqualityComparison notEqual(Object) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression notEqual(reflect.Field, Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression notEqual(kotlin.reflect.KProperty1, Object) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression notIn(reflect.Field, Collection) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$CollectionExpression notIn(Collection) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression notIn(kotlin.reflect.KProperty1, Collection) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression notLike(reflect.Field, String) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression notLike(kotlin.reflect.KProperty1, String) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression notNull(reflect.Field) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression notNull(kotlin.reflect.KProperty1) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression predicate(reflect.Field, net.corda.core.node.services.vault.ColumnPredicate) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression predicate(kotlin.reflect.KProperty1, net.corda.core.node.services.vault.ColumnPredicate) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression sum(reflect.Field) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression sum(reflect.Field, List) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression sum(reflect.Field, List, net.corda.core.node.services.vault.Sort$Direction) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression sum(kotlin.reflect.KProperty1, List, net.corda.core.node.services.vault.Sort$Direction) + public static final net.corda.core.node.services.vault.Builder INSTANCE +## +public final class net.corda.core.node.services.vault.CollectionOperator extends java.lang.Enum implements net.corda.core.node.services.vault.Operator + protected (String, int) + public static net.corda.core.node.services.vault.CollectionOperator valueOf(String) + public static net.corda.core.node.services.vault.CollectionOperator[] values() +## +public final class net.corda.core.node.services.vault.Column extends java.lang.Object + public (String, Class) + public (reflect.Field) + public (kotlin.reflect.KProperty1) + @org.jetbrains.annotations.NotNull public final Class getDeclaringClass() + @org.jetbrains.annotations.NotNull public final String getName() +## +public abstract class net.corda.core.node.services.vault.ColumnPredicate extends java.lang.Object +## +public static final class net.corda.core.node.services.vault.ColumnPredicate$AggregateFunction extends net.corda.core.node.services.vault.ColumnPredicate + public (net.corda.core.node.services.vault.AggregateFunctionType) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.AggregateFunctionType component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$AggregateFunction copy(net.corda.core.node.services.vault.AggregateFunctionType) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.AggregateFunctionType getType() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.ColumnPredicate$Between extends net.corda.core.node.services.vault.ColumnPredicate + public (Comparable, Comparable) + @org.jetbrains.annotations.NotNull public final Comparable component1() + @org.jetbrains.annotations.NotNull public final Comparable component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$Between copy(Comparable, Comparable) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final Comparable getRightFromLiteral() + @org.jetbrains.annotations.NotNull public final Comparable getRightToLiteral() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.ColumnPredicate$BinaryComparison extends net.corda.core.node.services.vault.ColumnPredicate + public (net.corda.core.node.services.vault.BinaryComparisonOperator, Comparable) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.BinaryComparisonOperator component1() + @org.jetbrains.annotations.NotNull public final Comparable component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$BinaryComparison copy(net.corda.core.node.services.vault.BinaryComparisonOperator, Comparable) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.BinaryComparisonOperator getOperator() + @org.jetbrains.annotations.NotNull public final Comparable getRightLiteral() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.ColumnPredicate$CollectionExpression extends net.corda.core.node.services.vault.ColumnPredicate + public (net.corda.core.node.services.vault.CollectionOperator, Collection) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CollectionOperator component1() + @org.jetbrains.annotations.NotNull public final Collection component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$CollectionExpression copy(net.corda.core.node.services.vault.CollectionOperator, Collection) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CollectionOperator getOperator() + @org.jetbrains.annotations.NotNull public final Collection getRightLiteral() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.ColumnPredicate$EqualityComparison extends net.corda.core.node.services.vault.ColumnPredicate + public (net.corda.core.node.services.vault.EqualityComparisonOperator, Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.EqualityComparisonOperator component1() + public final Object component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$EqualityComparison copy(net.corda.core.node.services.vault.EqualityComparisonOperator, Object) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.EqualityComparisonOperator getOperator() + public final Object getRightLiteral() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.ColumnPredicate$Likeness extends net.corda.core.node.services.vault.ColumnPredicate + public (net.corda.core.node.services.vault.LikenessOperator, String) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.LikenessOperator component1() + @org.jetbrains.annotations.NotNull public final String component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$Likeness copy(net.corda.core.node.services.vault.LikenessOperator, String) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.LikenessOperator getOperator() + @org.jetbrains.annotations.NotNull public final String getRightLiteral() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.ColumnPredicate$NullExpression extends net.corda.core.node.services.vault.ColumnPredicate + public (net.corda.core.node.services.vault.NullOperator) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.NullOperator component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$NullExpression copy(net.corda.core.node.services.vault.NullOperator) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.NullOperator getOperator() + public int hashCode() + public String toString() +## +public abstract class net.corda.core.node.services.vault.CriteriaExpression extends java.lang.Object +## +public static final class net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression extends net.corda.core.node.services.vault.CriteriaExpression + public (net.corda.core.node.services.vault.Column, net.corda.core.node.services.vault.ColumnPredicate, List, net.corda.core.node.services.vault.Sort$Direction) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.Column component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate component2() + @org.jetbrains.annotations.Nullable public final List component3() + @org.jetbrains.annotations.Nullable public final net.corda.core.node.services.vault.Sort$Direction component4() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression copy(net.corda.core.node.services.vault.Column, net.corda.core.node.services.vault.ColumnPredicate, List, net.corda.core.node.services.vault.Sort$Direction) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.Column getColumn() + @org.jetbrains.annotations.Nullable public final List getGroupByColumns() + @org.jetbrains.annotations.Nullable public final net.corda.core.node.services.vault.Sort$Direction getOrderBy() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate getPredicate() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.CriteriaExpression$BinaryLogical extends net.corda.core.node.services.vault.CriteriaExpression + public (net.corda.core.node.services.vault.CriteriaExpression, net.corda.core.node.services.vault.CriteriaExpression, net.corda.core.node.services.vault.BinaryLogicalOperator) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.BinaryLogicalOperator component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$BinaryLogical copy(net.corda.core.node.services.vault.CriteriaExpression, net.corda.core.node.services.vault.CriteriaExpression, net.corda.core.node.services.vault.BinaryLogicalOperator) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression getLeft() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.BinaryLogicalOperator getOperator() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression getRight() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression extends net.corda.core.node.services.vault.CriteriaExpression + public (net.corda.core.node.services.vault.Column, net.corda.core.node.services.vault.ColumnPredicate) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.Column component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression copy(net.corda.core.node.services.vault.Column, net.corda.core.node.services.vault.ColumnPredicate) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.Column getColumn() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate getPredicate() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.CriteriaExpression$Not extends net.corda.core.node.services.vault.CriteriaExpression + public (net.corda.core.node.services.vault.CriteriaExpression) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$Not copy(net.corda.core.node.services.vault.CriteriaExpression) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression getExpression() + public int hashCode() + public String toString() +## +public final class net.corda.core.node.services.vault.EqualityComparisonOperator extends java.lang.Enum implements net.corda.core.node.services.vault.Operator + protected (String, int) + public static net.corda.core.node.services.vault.EqualityComparisonOperator valueOf(String) + public static net.corda.core.node.services.vault.EqualityComparisonOperator[] values() +## +public interface net.corda.core.node.services.vault.IQueryCriteriaParser + @org.jetbrains.annotations.NotNull public abstract Collection parse(net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.Sort) + @org.jetbrains.annotations.NotNull public abstract Collection parseAnd(net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.QueryCriteria) + @org.jetbrains.annotations.NotNull public abstract Collection parseCriteria(net.corda.core.node.services.vault.QueryCriteria$CommonQueryCriteria) + @org.jetbrains.annotations.NotNull public abstract Collection parseCriteria(net.corda.core.node.services.vault.QueryCriteria$FungibleAssetQueryCriteria) + @org.jetbrains.annotations.NotNull public abstract Collection parseCriteria(net.corda.core.node.services.vault.QueryCriteria$LinearStateQueryCriteria) + @org.jetbrains.annotations.NotNull public abstract Collection parseCriteria(net.corda.core.node.services.vault.QueryCriteria$VaultCustomQueryCriteria) + @org.jetbrains.annotations.NotNull public abstract Collection parseCriteria(net.corda.core.node.services.vault.QueryCriteria$VaultQueryCriteria) + @org.jetbrains.annotations.NotNull public abstract Collection parseOr(net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.QueryCriteria) +## +public final class net.corda.core.node.services.vault.LikenessOperator extends java.lang.Enum implements net.corda.core.node.services.vault.Operator + protected (String, int) + public static net.corda.core.node.services.vault.LikenessOperator valueOf(String) + public static net.corda.core.node.services.vault.LikenessOperator[] values() +## +public final class net.corda.core.node.services.vault.NullOperator extends java.lang.Enum implements net.corda.core.node.services.vault.Operator + protected (String, int) + public static net.corda.core.node.services.vault.NullOperator valueOf(String) + public static net.corda.core.node.services.vault.NullOperator[] values() +## +public interface net.corda.core.node.services.vault.Operator +## +public final class net.corda.core.node.services.vault.PageSpecification extends java.lang.Object + public () + public (int, int) + public final int component1() + public final int component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.PageSpecification copy(int, int) + public boolean equals(Object) + public final int getPageNumber() + public final int getPageSize() + public int hashCode() + public final boolean isDefault() + public String toString() +## +public abstract class net.corda.core.node.services.vault.QueryCriteria extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria and(net.corda.core.node.services.vault.QueryCriteria) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria or(net.corda.core.node.services.vault.QueryCriteria) + @org.jetbrains.annotations.NotNull public abstract Collection visit(net.corda.core.node.services.vault.IQueryCriteriaParser) +## +public abstract static class net.corda.core.node.services.vault.QueryCriteria$CommonQueryCriteria extends net.corda.core.node.services.vault.QueryCriteria + public () + @org.jetbrains.annotations.Nullable public abstract Set getContractStateTypes() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$StateStatus getStatus() + @org.jetbrains.annotations.NotNull public Collection visit(net.corda.core.node.services.vault.IQueryCriteriaParser) +## +public static final class net.corda.core.node.services.vault.QueryCriteria$FungibleAssetQueryCriteria extends net.corda.core.node.services.vault.QueryCriteria$CommonQueryCriteria + @kotlin.jvm.JvmOverloads public () + @kotlin.jvm.JvmOverloads public (List) + @kotlin.jvm.JvmOverloads public (List, List) + @kotlin.jvm.JvmOverloads public (List, List, net.corda.core.node.services.vault.ColumnPredicate) + @kotlin.jvm.JvmOverloads public (List, List, net.corda.core.node.services.vault.ColumnPredicate, List) + @kotlin.jvm.JvmOverloads public (List, List, net.corda.core.node.services.vault.ColumnPredicate, List, List) + @kotlin.jvm.JvmOverloads public (List, List, net.corda.core.node.services.vault.ColumnPredicate, List, List, net.corda.core.node.services.Vault$StateStatus) + @kotlin.jvm.JvmOverloads public (List, List, net.corda.core.node.services.vault.ColumnPredicate, List, List, net.corda.core.node.services.Vault$StateStatus, Set) + @org.jetbrains.annotations.Nullable public final List component1() + @org.jetbrains.annotations.Nullable public final List component2() + @org.jetbrains.annotations.Nullable public final net.corda.core.node.services.vault.ColumnPredicate component3() + @org.jetbrains.annotations.Nullable public final List component4() + @org.jetbrains.annotations.Nullable public final List component5() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$StateStatus component6() + @org.jetbrains.annotations.Nullable public final Set component7() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria$FungibleAssetQueryCriteria copy(List, List, net.corda.core.node.services.vault.ColumnPredicate, List, List, net.corda.core.node.services.Vault$StateStatus, Set) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public Set getContractStateTypes() + @org.jetbrains.annotations.Nullable public final List getIssuer() + @org.jetbrains.annotations.Nullable public final List getIssuerRef() + @org.jetbrains.annotations.Nullable public final List getOwner() + @org.jetbrains.annotations.Nullable public final List getParticipants() + @org.jetbrains.annotations.Nullable public final net.corda.core.node.services.vault.ColumnPredicate getQuantity() + @org.jetbrains.annotations.NotNull public net.corda.core.node.services.Vault$StateStatus getStatus() + public int hashCode() + public String toString() + @org.jetbrains.annotations.NotNull public Collection visit(net.corda.core.node.services.vault.IQueryCriteriaParser) +## +public static final class net.corda.core.node.services.vault.QueryCriteria$LinearStateQueryCriteria extends net.corda.core.node.services.vault.QueryCriteria$CommonQueryCriteria + @kotlin.jvm.JvmOverloads public () + @kotlin.jvm.JvmOverloads public (List) + @kotlin.jvm.JvmOverloads public (List, List) + @kotlin.jvm.JvmOverloads public (List, List, List) + @kotlin.jvm.JvmOverloads public (List, List, List, net.corda.core.node.services.Vault$StateStatus) + @kotlin.jvm.JvmOverloads public (List, List, List, net.corda.core.node.services.Vault$StateStatus, Set) + public (List, List, net.corda.core.node.services.Vault$StateStatus, Set) + @org.jetbrains.annotations.Nullable public final List component1() + @org.jetbrains.annotations.Nullable public final List component2() + @org.jetbrains.annotations.Nullable public final List component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$StateStatus component4() + @org.jetbrains.annotations.Nullable public final Set component5() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria$LinearStateQueryCriteria copy(List, List, List, net.corda.core.node.services.Vault$StateStatus, Set) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public Set getContractStateTypes() + @org.jetbrains.annotations.Nullable public final List getExternalId() + @org.jetbrains.annotations.Nullable public final List getParticipants() + @org.jetbrains.annotations.NotNull public net.corda.core.node.services.Vault$StateStatus getStatus() + @org.jetbrains.annotations.Nullable public final List getUuid() + public int hashCode() + public String toString() + @org.jetbrains.annotations.NotNull public Collection visit(net.corda.core.node.services.vault.IQueryCriteriaParser) +## +public static final class net.corda.core.node.services.vault.QueryCriteria$SoftLockingCondition extends java.lang.Object + public (net.corda.core.node.services.vault.QueryCriteria$SoftLockingType, List) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria$SoftLockingType component1() + @org.jetbrains.annotations.NotNull public final List component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria$SoftLockingCondition copy(net.corda.core.node.services.vault.QueryCriteria$SoftLockingType, List) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final List getLockIds() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria$SoftLockingType getType() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.QueryCriteria$SoftLockingType extends java.lang.Enum + protected (String, int) + public static net.corda.core.node.services.vault.QueryCriteria$SoftLockingType valueOf(String) + public static net.corda.core.node.services.vault.QueryCriteria$SoftLockingType[] values() +## +public static final class net.corda.core.node.services.vault.QueryCriteria$TimeCondition extends java.lang.Object + public (net.corda.core.node.services.vault.QueryCriteria$TimeInstantType, net.corda.core.node.services.vault.ColumnPredicate) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria$TimeInstantType component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria$TimeCondition copy(net.corda.core.node.services.vault.QueryCriteria$TimeInstantType, net.corda.core.node.services.vault.ColumnPredicate) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate getPredicate() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria$TimeInstantType getType() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.QueryCriteria$TimeInstantType extends java.lang.Enum + protected (String, int) + public static net.corda.core.node.services.vault.QueryCriteria$TimeInstantType valueOf(String) + public static net.corda.core.node.services.vault.QueryCriteria$TimeInstantType[] values() +## +public static final class net.corda.core.node.services.vault.QueryCriteria$VaultCustomQueryCriteria extends net.corda.core.node.services.vault.QueryCriteria$CommonQueryCriteria + @kotlin.jvm.JvmOverloads public (net.corda.core.node.services.vault.CriteriaExpression) + @kotlin.jvm.JvmOverloads public (net.corda.core.node.services.vault.CriteriaExpression, net.corda.core.node.services.Vault$StateStatus) + @kotlin.jvm.JvmOverloads public (net.corda.core.node.services.vault.CriteriaExpression, net.corda.core.node.services.Vault$StateStatus, Set) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$StateStatus component2() + @org.jetbrains.annotations.Nullable public final Set component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria$VaultCustomQueryCriteria copy(net.corda.core.node.services.vault.CriteriaExpression, net.corda.core.node.services.Vault$StateStatus, Set) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public Set getContractStateTypes() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression getExpression() + @org.jetbrains.annotations.NotNull public net.corda.core.node.services.Vault$StateStatus getStatus() + public int hashCode() + public String toString() + @org.jetbrains.annotations.NotNull public Collection visit(net.corda.core.node.services.vault.IQueryCriteriaParser) +## +public static final class net.corda.core.node.services.vault.QueryCriteria$VaultQueryCriteria extends net.corda.core.node.services.vault.QueryCriteria$CommonQueryCriteria + @kotlin.jvm.JvmOverloads public () + @kotlin.jvm.JvmOverloads public (net.corda.core.node.services.Vault$StateStatus) + @kotlin.jvm.JvmOverloads public (net.corda.core.node.services.Vault$StateStatus, Set) + @kotlin.jvm.JvmOverloads public (net.corda.core.node.services.Vault$StateStatus, Set, List) + @kotlin.jvm.JvmOverloads public (net.corda.core.node.services.Vault$StateStatus, Set, List, List) + @kotlin.jvm.JvmOverloads public (net.corda.core.node.services.Vault$StateStatus, Set, List, List, net.corda.core.node.services.vault.QueryCriteria$SoftLockingCondition) + @kotlin.jvm.JvmOverloads public (net.corda.core.node.services.Vault$StateStatus, Set, List, List, net.corda.core.node.services.vault.QueryCriteria$SoftLockingCondition, net.corda.core.node.services.vault.QueryCriteria$TimeCondition) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$StateStatus component1() + @org.jetbrains.annotations.Nullable public final Set component2() + @org.jetbrains.annotations.Nullable public final List component3() + @org.jetbrains.annotations.Nullable public final List component4() + @org.jetbrains.annotations.Nullable public final net.corda.core.node.services.vault.QueryCriteria$SoftLockingCondition component5() + @org.jetbrains.annotations.Nullable public final net.corda.core.node.services.vault.QueryCriteria$TimeCondition component6() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria$VaultQueryCriteria copy(net.corda.core.node.services.Vault$StateStatus, Set, List, List, net.corda.core.node.services.vault.QueryCriteria$SoftLockingCondition, net.corda.core.node.services.vault.QueryCriteria$TimeCondition) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public Set getContractStateTypes() + @org.jetbrains.annotations.Nullable public final List getNotary() + @org.jetbrains.annotations.Nullable public final net.corda.core.node.services.vault.QueryCriteria$SoftLockingCondition getSoftLockingCondition() + @org.jetbrains.annotations.Nullable public final List getStateRefs() + @org.jetbrains.annotations.NotNull public net.corda.core.node.services.Vault$StateStatus getStatus() + @org.jetbrains.annotations.Nullable public final net.corda.core.node.services.vault.QueryCriteria$TimeCondition getTimeCondition() + public int hashCode() + public String toString() + @org.jetbrains.annotations.NotNull public Collection visit(net.corda.core.node.services.vault.IQueryCriteriaParser) +## +public final class net.corda.core.node.services.vault.QueryCriteriaUtils extends java.lang.Object + public static final Object builder(kotlin.jvm.functions.Function1) + @org.jetbrains.annotations.NotNull public static final String getColumnName(net.corda.core.node.services.vault.Column) + @org.jetbrains.annotations.NotNull public static final Class resolveEnclosingObjectFromColumn(net.corda.core.node.services.vault.Column) + @org.jetbrains.annotations.NotNull public static final Class resolveEnclosingObjectFromExpression(net.corda.core.node.services.vault.CriteriaExpression) + public static final int DEFAULT_PAGE_NUM = 1 + public static final int DEFAULT_PAGE_SIZE = 200 + public static final int MAX_PAGE_SIZE = 2147483647 +## +public final class net.corda.core.node.services.vault.Sort extends java.lang.Object + public (Collection) + @org.jetbrains.annotations.NotNull public final Collection component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.Sort copy(Collection) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final Collection getColumns() + public int hashCode() + public String toString() +## +public static interface net.corda.core.node.services.vault.Sort$Attribute +## +public static final class net.corda.core.node.services.vault.Sort$CommonStateAttribute extends java.lang.Enum implements net.corda.core.node.services.vault.Sort$Attribute + protected (String, int, String, String) + @org.jetbrains.annotations.Nullable public final String getAttributeChild() + @org.jetbrains.annotations.NotNull public final String getAttributeParent() + public static net.corda.core.node.services.vault.Sort$CommonStateAttribute valueOf(String) + public static net.corda.core.node.services.vault.Sort$CommonStateAttribute[] values() +## +public static final class net.corda.core.node.services.vault.Sort$Direction extends java.lang.Enum + protected (String, int) + public static net.corda.core.node.services.vault.Sort$Direction valueOf(String) + public static net.corda.core.node.services.vault.Sort$Direction[] values() +## +public static final class net.corda.core.node.services.vault.Sort$FungibleStateAttribute extends java.lang.Enum implements net.corda.core.node.services.vault.Sort$Attribute + protected (String, int, String) + @org.jetbrains.annotations.NotNull public final String getAttributeName() + public static net.corda.core.node.services.vault.Sort$FungibleStateAttribute valueOf(String) + public static net.corda.core.node.services.vault.Sort$FungibleStateAttribute[] values() +## +public static final class net.corda.core.node.services.vault.Sort$LinearStateAttribute extends java.lang.Enum implements net.corda.core.node.services.vault.Sort$Attribute + protected (String, int, String) + @org.jetbrains.annotations.NotNull public final String getAttributeName() + public static net.corda.core.node.services.vault.Sort$LinearStateAttribute valueOf(String) + public static net.corda.core.node.services.vault.Sort$LinearStateAttribute[] values() +## +public static final class net.corda.core.node.services.vault.Sort$SortColumn extends java.lang.Object + public (net.corda.core.node.services.vault.SortAttribute, net.corda.core.node.services.vault.Sort$Direction) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.SortAttribute component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.Sort$Direction component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.Sort$SortColumn copy(net.corda.core.node.services.vault.SortAttribute, net.corda.core.node.services.vault.Sort$Direction) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.Sort$Direction getDirection() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.SortAttribute getSortAttribute() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.Sort$VaultStateAttribute extends java.lang.Enum implements net.corda.core.node.services.vault.Sort$Attribute + protected (String, int, String) + @org.jetbrains.annotations.NotNull public final String getAttributeName() + public static net.corda.core.node.services.vault.Sort$VaultStateAttribute valueOf(String) + public static net.corda.core.node.services.vault.Sort$VaultStateAttribute[] values() +## +public abstract class net.corda.core.node.services.vault.SortAttribute extends java.lang.Object +## +public static final class net.corda.core.node.services.vault.SortAttribute$Custom extends net.corda.core.node.services.vault.SortAttribute + public (Class, String) + @org.jetbrains.annotations.NotNull public final Class component1() + @org.jetbrains.annotations.NotNull public final String component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.SortAttribute$Custom copy(Class, String) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final Class getEntityStateClass() + @org.jetbrains.annotations.NotNull public final String getEntityStateColumnName() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.SortAttribute$Standard extends net.corda.core.node.services.vault.SortAttribute + public (net.corda.core.node.services.vault.Sort$Attribute) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.Sort$Attribute component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.SortAttribute$Standard copy(net.corda.core.node.services.vault.Sort$Attribute) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.Sort$Attribute getAttribute() + public int hashCode() + public String toString() +## +public final class net.corda.core.schemas.CommonSchema extends java.lang.Object + public static final net.corda.core.schemas.CommonSchema INSTANCE +## +public final class net.corda.core.schemas.CommonSchemaV1 extends net.corda.core.schemas.MappedSchema + public static final net.corda.core.schemas.CommonSchemaV1 INSTANCE +## +public static class net.corda.core.schemas.CommonSchemaV1$FungibleState extends net.corda.core.schemas.PersistentState + public (Set, net.corda.core.identity.AbstractParty, long, net.corda.core.identity.AbstractParty, byte[]) + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.AbstractParty getIssuer() + @org.jetbrains.annotations.NotNull public final byte[] getIssuerRef() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.AbstractParty getOwner() + @org.jetbrains.annotations.Nullable public final Set getParticipants() + public final long getQuantity() + public final void setIssuer(net.corda.core.identity.AbstractParty) + public final void setIssuerRef(byte[]) + public final void setOwner(net.corda.core.identity.AbstractParty) + public final void setParticipants(Set) + public final void setQuantity(long) +## +public static class net.corda.core.schemas.CommonSchemaV1$LinearState extends net.corda.core.schemas.PersistentState + public (Set, String, UUID) + public (net.corda.core.contracts.UniqueIdentifier, Set) + @org.jetbrains.annotations.Nullable public final String getExternalId() + @org.jetbrains.annotations.Nullable public final Set getParticipants() + @org.jetbrains.annotations.NotNull public final UUID getUuid() + public final void setExternalId(String) + public final void setParticipants(Set) + public final void setUuid(UUID) +## +public class net.corda.core.schemas.MappedSchema extends java.lang.Object + public (Class, int, Iterable) + @org.jetbrains.annotations.NotNull public final Iterable getMappedTypes() + @org.jetbrains.annotations.NotNull public final String getName() + public final int getVersion() + @org.jetbrains.annotations.NotNull public String toString() +## +public final class net.corda.core.schemas.NodeInfoSchema extends java.lang.Object + public static final net.corda.core.schemas.NodeInfoSchema INSTANCE +## +public final class net.corda.core.schemas.NodeInfoSchemaV1 extends net.corda.core.schemas.MappedSchema + public static final net.corda.core.schemas.NodeInfoSchemaV1 INSTANCE +## +public static final class net.corda.core.schemas.NodeInfoSchemaV1$DBHostAndPort extends java.lang.Object + public () + public (net.corda.core.schemas.NodeInfoSchemaV1$PKHostAndPort) + @org.jetbrains.annotations.NotNull public final net.corda.core.schemas.NodeInfoSchemaV1$DBHostAndPort copy(net.corda.core.schemas.NodeInfoSchemaV1$PKHostAndPort) + public boolean equals(Object) + public int hashCode() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.NetworkHostAndPort toHostAndPort() + public String toString() + public static final net.corda.core.schemas.NodeInfoSchemaV1$DBHostAndPort$Companion Companion +## +public static final class net.corda.core.schemas.NodeInfoSchemaV1$DBHostAndPort$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.schemas.NodeInfoSchemaV1$DBHostAndPort fromHostAndPort(net.corda.core.utilities.NetworkHostAndPort) +## +public static final class net.corda.core.schemas.NodeInfoSchemaV1$DBPartyAndCertificate extends java.lang.Object + public () + public (String, String, byte[], boolean, Set) + public (net.corda.core.identity.PartyAndCertificate, boolean) + @org.jetbrains.annotations.NotNull public final String component1() + @org.jetbrains.annotations.NotNull public final String component2() + @org.jetbrains.annotations.NotNull public final byte[] component3() + public final boolean component4() + @org.jetbrains.annotations.NotNull public final net.corda.core.schemas.NodeInfoSchemaV1$DBPartyAndCertificate copy(String, String, byte[], boolean, Set) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final String getName() + @org.jetbrains.annotations.NotNull public final String getOwningKey() + @org.jetbrains.annotations.NotNull public final byte[] getPartyCertBinary() + public int hashCode() + public final boolean isMain() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.PartyAndCertificate toLegalIdentityAndCert() + public String toString() +## +public static final class net.corda.core.schemas.NodeInfoSchemaV1$PKHostAndPort extends java.lang.Object implements java.io.Serializable + public () + public (String, Integer) + @org.jetbrains.annotations.Nullable public final String component1() + @org.jetbrains.annotations.Nullable public final Integer component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.schemas.NodeInfoSchemaV1$PKHostAndPort copy(String, Integer) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public final String getHost() + @org.jetbrains.annotations.Nullable public final Integer getPort() + public int hashCode() + public String toString() +## +public static final class net.corda.core.schemas.NodeInfoSchemaV1$PersistentNodeInfo extends java.lang.Object + public () + public (int, List, List, int, long) + @org.jetbrains.annotations.NotNull public final List getAddresses() + public final int getId() + @org.jetbrains.annotations.NotNull public final List getLegalIdentitiesAndCerts() + public final int getPlatformVersion() + public final long getSerial() + public final void setId(int) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NodeInfo toNodeInfo() +## +public class net.corda.core.schemas.PersistentState extends java.lang.Object implements net.corda.core.schemas.StatePersistable + public () + public (net.corda.core.schemas.PersistentStateRef) + @org.jetbrains.annotations.Nullable public final net.corda.core.schemas.PersistentStateRef getStateRef() + public final void setStateRef(net.corda.core.schemas.PersistentStateRef) +## +public final class net.corda.core.schemas.PersistentStateRef extends java.lang.Object implements java.io.Serializable + public () + public (String, Integer) + public (net.corda.core.contracts.StateRef) + @org.jetbrains.annotations.Nullable public final String component1() + @org.jetbrains.annotations.Nullable public final Integer component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.schemas.PersistentStateRef copy(String, Integer) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public final Integer getIndex() + @org.jetbrains.annotations.Nullable public final String getTxId() + public int hashCode() + public final void setIndex(Integer) + public final void setTxId(String) + public String toString() +## +public interface net.corda.core.schemas.QueryableState extends net.corda.core.contracts.ContractState + @org.jetbrains.annotations.NotNull public abstract net.corda.core.schemas.PersistentState generateMappedObject(net.corda.core.schemas.MappedSchema) + @org.jetbrains.annotations.NotNull public abstract Iterable supportedSchemas() +## +public interface net.corda.core.schemas.StatePersistable +## +public interface net.corda.core.serialization.ClassWhitelist + public abstract boolean hasListed(Class) +## +public @interface net.corda.core.serialization.CordaSerializable +## +public @interface net.corda.core.serialization.DeprecatedConstructorForDeserialization + public abstract int version() +## +public final class net.corda.core.serialization.MissingAttachmentsException extends net.corda.core.CordaException + public (List) + @org.jetbrains.annotations.NotNull public final List getIds() +## +public final class net.corda.core.serialization.SerializationAPIKt extends java.lang.Object + @org.jetbrains.annotations.NotNull public static final net.corda.core.serialization.SerializedBytes serialize(Object, net.corda.core.serialization.SerializationFactory, net.corda.core.serialization.SerializationContext) +## +public interface net.corda.core.serialization.SerializationContext + @org.jetbrains.annotations.NotNull public abstract ClassLoader getDeserializationClassLoader() + public abstract boolean getObjectReferencesEnabled() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.utilities.ByteSequence getPreferredSerializationVersion() + @org.jetbrains.annotations.NotNull public abstract Map getProperties() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationContext$UseCase getUseCase() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.ClassWhitelist getWhitelist() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationContext withAttachmentsClassLoader(List) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationContext withClassLoader(ClassLoader) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationContext withPreferredSerializationVersion(net.corda.core.utilities.ByteSequence) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationContext withProperty(Object, Object) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationContext withWhitelisted(Class) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationContext withoutReferences() +## +public static final class net.corda.core.serialization.SerializationContext$UseCase extends java.lang.Enum + protected (String, int) + public static net.corda.core.serialization.SerializationContext$UseCase valueOf(String) + public static net.corda.core.serialization.SerializationContext$UseCase[] values() +## +public interface net.corda.core.serialization.SerializationCustomization +## +public final class net.corda.core.serialization.SerializationDefaults extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext getCHECKPOINT_CONTEXT() + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext getP2P_CONTEXT() + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext getRPC_CLIENT_CONTEXT() + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext getRPC_SERVER_CONTEXT() + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationFactory getSERIALIZATION_FACTORY() + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext getSTORAGE_CONTEXT() + public final void setCHECKPOINT_CONTEXT(net.corda.core.serialization.SerializationContext) + public final void setP2P_CONTEXT(net.corda.core.serialization.SerializationContext) + public final void setRPC_CLIENT_CONTEXT(net.corda.core.serialization.SerializationContext) + public final void setRPC_SERVER_CONTEXT(net.corda.core.serialization.SerializationContext) + public final void setSERIALIZATION_FACTORY(net.corda.core.serialization.SerializationFactory) + public final void setSTORAGE_CONTEXT(net.corda.core.serialization.SerializationContext) + public static final net.corda.core.serialization.SerializationDefaults INSTANCE +## +public abstract class net.corda.core.serialization.SerializationFactory extends java.lang.Object + public () + public final Object asCurrent(kotlin.jvm.functions.Function1) + @org.jetbrains.annotations.NotNull public abstract Object deserialize(net.corda.core.utilities.ByteSequence, Class, net.corda.core.serialization.SerializationContext) + @org.jetbrains.annotations.Nullable public final net.corda.core.serialization.SerializationContext getCurrentContext() + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext getDefaultContext() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializedBytes serialize(Object, net.corda.core.serialization.SerializationContext) + public final Object withCurrentContext(net.corda.core.serialization.SerializationContext, kotlin.jvm.functions.Function0) + public static final net.corda.core.serialization.SerializationFactory$Companion Companion +## +public static final class net.corda.core.serialization.SerializationFactory$Companion extends java.lang.Object + @org.jetbrains.annotations.Nullable public final net.corda.core.serialization.SerializationFactory getCurrentFactory() + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationFactory getDefaultFactory() +## +public interface net.corda.core.serialization.SerializationToken + @org.jetbrains.annotations.NotNull public abstract Object fromToken(net.corda.core.serialization.SerializeAsTokenContext) +## +public interface net.corda.core.serialization.SerializeAsToken + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationToken toToken(net.corda.core.serialization.SerializeAsTokenContext) +## +public interface net.corda.core.serialization.SerializeAsTokenContext + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.ServiceHub getServiceHub() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializeAsToken getSingleton(String) + public abstract void putSingleton(net.corda.core.serialization.SerializeAsToken) +## +public final class net.corda.core.serialization.SerializedBytes extends net.corda.core.utilities.OpaqueBytes + public (byte[]) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getHash() +## +public final class net.corda.core.serialization.SingletonSerializationToken extends java.lang.Object implements net.corda.core.serialization.SerializationToken + @org.jetbrains.annotations.NotNull public net.corda.core.serialization.SerializeAsToken fromToken(net.corda.core.serialization.SerializeAsTokenContext) + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SingletonSerializationToken registerWithContext(net.corda.core.serialization.SerializeAsTokenContext, net.corda.core.serialization.SerializeAsToken) + public static final net.corda.core.serialization.SingletonSerializationToken$Companion Companion +## +public static final class net.corda.core.serialization.SingletonSerializationToken$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SingletonSerializationToken singletonSerializationToken(Class) +## +public abstract class net.corda.core.serialization.SingletonSerializeAsToken extends java.lang.Object implements net.corda.core.serialization.SerializeAsToken + public () + @org.jetbrains.annotations.NotNull public net.corda.core.serialization.SingletonSerializationToken toToken(net.corda.core.serialization.SerializeAsTokenContext) +## +public abstract class net.corda.core.transactions.BaseTransaction extends java.lang.Object implements net.corda.core.contracts.NamedByHash + public () + protected void checkBaseInvariants() + @org.jetbrains.annotations.NotNull public final List filterOutRefs(Class, function.Predicate) + @org.jetbrains.annotations.NotNull public final List filterOutputs(Class, function.Predicate) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateAndRef findOutRef(Class, function.Predicate) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ContractState findOutput(Class, function.Predicate) + @org.jetbrains.annotations.NotNull public abstract List getInputs() + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party getNotary() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ContractState getOutput(int) + @org.jetbrains.annotations.NotNull public final List getOutputStates() + @org.jetbrains.annotations.NotNull public abstract List getOutputs() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateAndRef outRef(int) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateAndRef outRef(net.corda.core.contracts.ContractState) + @org.jetbrains.annotations.NotNull public final List outRefsOfType(Class) + @org.jetbrains.annotations.NotNull public final List outputsOfType(Class) + @org.jetbrains.annotations.NotNull public String toString() +## +public class net.corda.core.transactions.ComponentGroup extends java.lang.Object + public (int, List) + @org.jetbrains.annotations.NotNull public List getComponents() + public int getGroupIndex() +## +public final class net.corda.core.transactions.ComponentVisibilityException extends net.corda.core.CordaException + public (net.corda.core.crypto.SecureHash, String) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getId() + @org.jetbrains.annotations.NotNull public final String getReason() +## +public abstract class net.corda.core.transactions.CoreTransaction extends net.corda.core.transactions.BaseTransaction + public () + @org.jetbrains.annotations.NotNull public abstract List getInputs() +## +public final class net.corda.core.transactions.FilteredComponentGroup extends net.corda.core.transactions.ComponentGroup + public (int, List, List, net.corda.core.crypto.PartialMerkleTree) + public final int component1() + @org.jetbrains.annotations.NotNull public final List component2() + @org.jetbrains.annotations.NotNull public final List component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.PartialMerkleTree component4() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.FilteredComponentGroup copy(int, List, List, net.corda.core.crypto.PartialMerkleTree) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public List getComponents() + public int getGroupIndex() + @org.jetbrains.annotations.NotNull public final List getNonces() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.PartialMerkleTree getPartialMerkleTree() + public int hashCode() + public String toString() +## +public final class net.corda.core.transactions.FilteredTransaction extends net.corda.core.transactions.TraversableTransaction + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.transactions.FilteredTransaction buildFilteredTransaction(net.corda.core.transactions.WireTransaction, function.Predicate) + public final void checkAllComponentsVisible(net.corda.core.contracts.ComponentGroupEnum) + public final boolean checkWithFun(kotlin.jvm.functions.Function1) + @org.jetbrains.annotations.NotNull public final List getFilteredComponentGroups() + @org.jetbrains.annotations.NotNull public final List getGroupHashes() + @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId() + public final void verify() + public static final net.corda.core.transactions.FilteredTransaction$Companion Companion +## +public static final class net.corda.core.transactions.FilteredTransaction$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.FilteredTransaction buildFilteredTransaction(net.corda.core.transactions.WireTransaction, function.Predicate) +## +public final class net.corda.core.transactions.FilteredTransactionVerificationException extends net.corda.core.CordaException + public (net.corda.core.crypto.SecureHash, String) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getId() + @org.jetbrains.annotations.NotNull public final String getReason() +## +public abstract class net.corda.core.transactions.FullTransaction extends net.corda.core.transactions.BaseTransaction + public () + protected void checkBaseInvariants() + @org.jetbrains.annotations.NotNull public abstract List getInputs() +## +public final class net.corda.core.transactions.LedgerTransaction extends net.corda.core.transactions.FullTransaction + public (List, List, List, List, net.corda.core.crypto.SecureHash, net.corda.core.identity.Party, net.corda.core.contracts.TimeWindow, net.corda.core.contracts.PrivacySalt) + @org.jetbrains.annotations.NotNull public final List commandsOfType(Class) + @org.jetbrains.annotations.NotNull public final List component1() + @org.jetbrains.annotations.NotNull public final List component2() + @org.jetbrains.annotations.NotNull public final List component3() + @org.jetbrains.annotations.NotNull public final List component4() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component5() + @org.jetbrains.annotations.Nullable public final net.corda.core.identity.Party component6() + @org.jetbrains.annotations.Nullable public final net.corda.core.contracts.TimeWindow component7() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.PrivacySalt component8() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction copy(List, List, List, List, net.corda.core.crypto.SecureHash, net.corda.core.identity.Party, net.corda.core.contracts.TimeWindow, net.corda.core.contracts.PrivacySalt) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final List filterCommands(Class, function.Predicate) + @org.jetbrains.annotations.NotNull public final List filterInRefs(Class, function.Predicate) + @org.jetbrains.annotations.NotNull public final List filterInputs(Class, function.Predicate) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Command findCommand(Class, function.Predicate) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateAndRef findInRef(Class, function.Predicate) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ContractState findInput(Class, function.Predicate) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Attachment getAttachment(int) + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Attachment getAttachment(net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public final List getAttachments() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Command getCommand(int) + @org.jetbrains.annotations.NotNull public final List getCommands() + @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ContractState getInput(int) + @org.jetbrains.annotations.NotNull public final List getInputStates() + @org.jetbrains.annotations.NotNull public List getInputs() + @org.jetbrains.annotations.Nullable public net.corda.core.identity.Party getNotary() + @org.jetbrains.annotations.NotNull public List getOutputs() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.PrivacySalt getPrivacySalt() + @org.jetbrains.annotations.Nullable public final net.corda.core.contracts.TimeWindow getTimeWindow() + @org.jetbrains.annotations.NotNull public final List groupStates(Class, kotlin.jvm.functions.Function1) + public int hashCode() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.StateAndRef inRef(int) + @org.jetbrains.annotations.NotNull public final List inRefsOfType(Class) + @org.jetbrains.annotations.NotNull public final List inputsOfType(Class) + public String toString() + public final void verify() +## +public static final class net.corda.core.transactions.LedgerTransaction$InOutGroup extends java.lang.Object + public (List, List, Object) + @org.jetbrains.annotations.NotNull public final List component1() + @org.jetbrains.annotations.NotNull public final List component2() + @org.jetbrains.annotations.NotNull public final Object component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction$InOutGroup copy(List, List, Object) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final Object getGroupingKey() + @org.jetbrains.annotations.NotNull public final List getInputs() + @org.jetbrains.annotations.NotNull public final List getOutputs() + public int hashCode() + public String toString() +## +public final class net.corda.core.transactions.MissingContractAttachments extends net.corda.core.flows.FlowException + public (List) + @org.jetbrains.annotations.NotNull public final List getStates() +## +public final class net.corda.core.transactions.NotaryChangeLedgerTransaction extends net.corda.core.transactions.FullTransaction implements net.corda.core.transactions.TransactionWithSignatures + public (List, net.corda.core.identity.Party, net.corda.core.identity.Party, net.corda.core.crypto.SecureHash, List) + public void checkSignaturesAreValid() + @org.jetbrains.annotations.NotNull public final List component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component4() + @org.jetbrains.annotations.NotNull public final List component5() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeLedgerTransaction copy(List, net.corda.core.identity.Party, net.corda.core.identity.Party, net.corda.core.crypto.SecureHash, List) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId() + @org.jetbrains.annotations.NotNull public List getInputs() + @org.jetbrains.annotations.NotNull public List getKeyDescriptions(Set) + @org.jetbrains.annotations.NotNull public Set getMissingSigners() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party getNewNotary() + @org.jetbrains.annotations.NotNull public net.corda.core.identity.Party getNotary() + @org.jetbrains.annotations.NotNull public List getOutputs() + @org.jetbrains.annotations.NotNull public Set getRequiredSigningKeys() + @org.jetbrains.annotations.NotNull public List getSigs() + public int hashCode() + public String toString() + public void verifyRequiredSignatures() +## +public final class net.corda.core.transactions.NotaryChangeWireTransaction extends net.corda.core.transactions.CoreTransaction + public (List, net.corda.core.identity.Party, net.corda.core.identity.Party) + @org.jetbrains.annotations.NotNull public final List component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeWireTransaction copy(List, net.corda.core.identity.Party, net.corda.core.identity.Party) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId() + @org.jetbrains.annotations.NotNull public List getInputs() + @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party getNewNotary() + @org.jetbrains.annotations.NotNull public net.corda.core.identity.Party getNotary() + @org.jetbrains.annotations.NotNull public List getOutputs() + public int hashCode() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeLedgerTransaction resolve(net.corda.core.node.ServiceHub, List) + public String toString() +## +public final class net.corda.core.transactions.SignedTransaction extends java.lang.Object implements net.corda.core.transactions.TransactionWithSignatures + public (net.corda.core.serialization.SerializedBytes, List) + public (net.corda.core.transactions.CoreTransaction, List) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.FilteredTransaction buildFilteredTransaction(function.Predicate) + public void checkSignaturesAreValid() + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializedBytes component1() + @org.jetbrains.annotations.NotNull public final List component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction copy(net.corda.core.serialization.SerializedBytes, List) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId() + @org.jetbrains.annotations.NotNull public final List getInputs() + @org.jetbrains.annotations.NotNull public ArrayList getKeyDescriptions(Set) + @org.jetbrains.annotations.NotNull public Set getMissingSigners() + @org.jetbrains.annotations.Nullable public final net.corda.core.identity.Party getNotary() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeWireTransaction getNotaryChangeTx() + @org.jetbrains.annotations.NotNull public Set getRequiredSigningKeys() + @org.jetbrains.annotations.NotNull public List getSigs() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.WireTransaction getTx() + @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializedBytes getTxBits() + public int hashCode() + public final boolean isNotaryChangeTransaction() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction plus(Collection) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction plus(net.corda.core.crypto.TransactionSignature) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeLedgerTransaction resolveNotaryChangeTransaction(net.corda.core.node.ServiceHub) + @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServiceHub) + @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServiceHub, boolean) + @org.jetbrains.annotations.NotNull public String toString() + @kotlin.jvm.JvmOverloads public final void verify(net.corda.core.node.ServiceHub) + @kotlin.jvm.JvmOverloads public final void verify(net.corda.core.node.ServiceHub, boolean) + public void verifyRequiredSignatures() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction withAdditionalSignature(java.security.KeyPair, net.corda.core.crypto.SignatureMetadata) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction withAdditionalSignature(net.corda.core.crypto.TransactionSignature) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction withAdditionalSignatures(Iterable) + public static final net.corda.core.transactions.SignedTransaction$Companion Companion +## +public static final class net.corda.core.transactions.SignedTransaction$SignaturesMissingException extends java.security.SignatureException implements net.corda.core.CordaThrowable, net.corda.core.contracts.NamedByHash + public (Set, List, net.corda.core.crypto.SecureHash) + public void addSuppressed(Throwable[]) + @org.jetbrains.annotations.NotNull public final List getDescriptions() + @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId() + @org.jetbrains.annotations.NotNull public final Set getMissing() + @org.jetbrains.annotations.Nullable public String getOriginalExceptionClassName() + @org.jetbrains.annotations.Nullable public String getOriginalMessage() + public void setCause(Throwable) + public void setMessage(String) + public void setOriginalExceptionClassName(String) +## +public class net.corda.core.transactions.TransactionBuilder extends java.lang.Object + public () + public (net.corda.core.identity.Party) + public (net.corda.core.identity.Party, UUID, List, List, List, List, net.corda.core.contracts.TimeWindow, net.corda.core.contracts.PrivacySalt) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addAttachment(net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addCommand(net.corda.core.contracts.Command) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addCommand(net.corda.core.contracts.CommandData, List) + @org.jetbrains.annotations.NotNull public net.corda.core.transactions.TransactionBuilder addInputState(net.corda.core.contracts.StateAndRef) + @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.ContractState, String) + @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.ContractState, String, net.corda.core.contracts.AttachmentConstraint) + @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party) + @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party, Integer) + @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party, Integer, net.corda.core.contracts.AttachmentConstraint) + @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.TransactionState) + @org.jetbrains.annotations.NotNull public final List attachments() + @org.jetbrains.annotations.NotNull public final List commands() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder copy() + @org.jetbrains.annotations.NotNull protected final List getAttachments() + @org.jetbrains.annotations.NotNull protected final List getCommands() + @org.jetbrains.annotations.NotNull protected final List getInputs() + @org.jetbrains.annotations.NotNull public final UUID getLockId() + @org.jetbrains.annotations.Nullable public final net.corda.core.identity.Party getNotary() + @org.jetbrains.annotations.NotNull protected final List getOutputs() + @org.jetbrains.annotations.NotNull protected final net.corda.core.contracts.PrivacySalt getPrivacySalt() + @org.jetbrains.annotations.Nullable protected final net.corda.core.contracts.TimeWindow getWindow() + @org.jetbrains.annotations.NotNull public final List inputStates() + @org.jetbrains.annotations.NotNull public final List outputStates() + public final void setLockId(UUID) + public final void setNotary(net.corda.core.identity.Party) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder setPrivacySalt(net.corda.core.contracts.PrivacySalt) + protected final void setPrivacySalt(net.corda.core.contracts.PrivacySalt) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder setTimeWindow(java.time.Instant, java.time.Duration) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder setTimeWindow(net.corda.core.contracts.TimeWindow) + protected final void setWindow(net.corda.core.contracts.TimeWindow) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServiceHub) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction toSignedTransaction(net.corda.core.node.services.KeyManagementService, java.security.PublicKey, net.corda.core.crypto.SignatureMetadata, net.corda.core.node.ServicesForResolution) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.WireTransaction toWireTransaction(net.corda.core.node.ServicesForResolution) + public final void verify(net.corda.core.node.ServiceHub) +## +public interface net.corda.core.transactions.TransactionWithSignatures extends net.corda.core.contracts.NamedByHash + public abstract void checkSignaturesAreValid() + @org.jetbrains.annotations.NotNull public abstract List getKeyDescriptions(Set) + @org.jetbrains.annotations.NotNull public abstract Set getMissingSigners() + @org.jetbrains.annotations.NotNull public abstract Set getRequiredSigningKeys() + @org.jetbrains.annotations.NotNull public abstract List getSigs() + public abstract void verifyRequiredSignatures() +## +public abstract class net.corda.core.transactions.TraversableTransaction extends net.corda.core.transactions.CoreTransaction + public (List) + @org.jetbrains.annotations.NotNull public final List getAttachments() + @org.jetbrains.annotations.NotNull public final List getAvailableComponentGroups() + @org.jetbrains.annotations.NotNull public final List getCommands() + @org.jetbrains.annotations.NotNull public List getComponentGroups() + @org.jetbrains.annotations.NotNull public List getInputs() + @org.jetbrains.annotations.Nullable public net.corda.core.identity.Party getNotary() + @org.jetbrains.annotations.NotNull public List getOutputs() + @org.jetbrains.annotations.Nullable public final net.corda.core.contracts.TimeWindow getTimeWindow() +## +public final class net.corda.core.transactions.WireTransaction extends net.corda.core.transactions.TraversableTransaction + @kotlin.Deprecated public (List, List, List, List, net.corda.core.identity.Party, net.corda.core.contracts.TimeWindow, net.corda.core.contracts.PrivacySalt) + public (List, net.corda.core.contracts.PrivacySalt) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.FilteredTransaction buildFilteredTransaction(function.Predicate) + public final void checkSignature(net.corda.core.crypto.TransactionSignature) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.MerkleTree getMerkleTree() + @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.PrivacySalt getPrivacySalt() + @org.jetbrains.annotations.NotNull public final Set getRequiredSigningKeys() + public int hashCode() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServicesForResolution) + @org.jetbrains.annotations.NotNull public String toString() + public static final net.corda.core.transactions.WireTransaction$Companion Companion +## +public static final class net.corda.core.transactions.WireTransaction$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final List createComponentGroups(List, List, List, List, net.corda.core.identity.Party, net.corda.core.contracts.TimeWindow) +## +public final class net.corda.core.utilities.ByteArrays extends java.lang.Object + @org.jetbrains.annotations.NotNull public static final byte[] parseAsHex(String) + @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.ByteSequence sequence(byte[], int, int) + @org.jetbrains.annotations.NotNull public static final String toHexString(byte[]) +## +public abstract class net.corda.core.utilities.ByteSequence extends java.lang.Object implements java.lang.Comparable + public int compareTo(net.corda.core.utilities.ByteSequence) + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence copy() + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public abstract byte[] getBytes() + public abstract int getOffset() + public abstract int getSize() + public int hashCode() + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.ByteSequence of(byte[]) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.ByteSequence of(byte[], int) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.ByteSequence of(byte[], int, int) + @org.jetbrains.annotations.NotNull public final java.io.ByteArrayInputStream open() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence subSequence(int, int) + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence take(int) + @org.jetbrains.annotations.NotNull public String toString() + public static final net.corda.core.utilities.ByteSequence$Companion Companion +## +public static final class net.corda.core.utilities.ByteSequence$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence of(byte[]) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence of(byte[], int) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence of(byte[], int, int) +## +public final class net.corda.core.utilities.EncodingUtils extends java.lang.Object + @org.jetbrains.annotations.NotNull public static final byte[] base58ToByteArray(String) + @org.jetbrains.annotations.NotNull public static final String base58ToRealString(String) + @org.jetbrains.annotations.NotNull public static final String base58toBase64(String) + @org.jetbrains.annotations.NotNull public static final String base58toHex(String) + @org.jetbrains.annotations.NotNull public static final byte[] base64ToByteArray(String) + @org.jetbrains.annotations.NotNull public static final String base64ToRealString(String) + @org.jetbrains.annotations.NotNull public static final String base64toBase58(String) + @org.jetbrains.annotations.NotNull public static final String base64toHex(String) + @org.jetbrains.annotations.NotNull public static final String hexToBase58(String) + @org.jetbrains.annotations.NotNull public static final String hexToBase64(String) + @org.jetbrains.annotations.NotNull public static final byte[] hexToByteArray(String) + @org.jetbrains.annotations.NotNull public static final String hexToRealString(String) + @org.jetbrains.annotations.NotNull public static final java.security.PublicKey parsePublicKeyBase58(String) + @org.jetbrains.annotations.NotNull public static final String toBase58(byte[]) + @org.jetbrains.annotations.NotNull public static final String toBase58String(java.security.PublicKey) + @org.jetbrains.annotations.NotNull public static final String toBase64(byte[]) + @org.jetbrains.annotations.NotNull public static final String toHex(byte[]) + @org.jetbrains.annotations.NotNull public static final byte[] toSHA256Bytes(java.security.PublicKey) +## +public final class net.corda.core.utilities.KotlinUtilsKt extends java.lang.Object + public static final void debug(org.slf4j.Logger, kotlin.jvm.functions.Function0) + public static final int exactAdd(int, int) + public static final long exactAdd(long, long) + @org.jetbrains.annotations.NotNull public static final java.time.Duration getDays(int) + @org.jetbrains.annotations.NotNull public static final java.time.Duration getHours(int) + @org.jetbrains.annotations.NotNull public static final java.time.Duration getMillis(int) + @org.jetbrains.annotations.NotNull public static final java.time.Duration getMinutes(int) + public static final Object getOrThrow(concurrent.Future, java.time.Duration) + @org.jetbrains.annotations.NotNull public static final java.time.Duration getSeconds(int) + @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.NonEmptySet toNonEmptySet(Collection) + public static final void trace(org.slf4j.Logger, kotlin.jvm.functions.Function0) + @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.PropertyDelegate transient(kotlin.jvm.functions.Function0) +## +public final class net.corda.core.utilities.NetworkHostAndPort extends java.lang.Object + public (String, int) + @org.jetbrains.annotations.NotNull public final String component1() + public final int component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.NetworkHostAndPort copy(String, int) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final String getHost() + public final int getPort() + public int hashCode() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.NetworkHostAndPort parse(String) + @org.jetbrains.annotations.NotNull public String toString() + public static final net.corda.core.utilities.NetworkHostAndPort$Companion Companion + @org.jetbrains.annotations.NotNull public static final String INVALID_PORT_FORMAT = "Invalid port: %s" + @org.jetbrains.annotations.NotNull public static final String MISSING_PORT_FORMAT = "Missing port: %s" + @org.jetbrains.annotations.NotNull public static final String UNPARSEABLE_ADDRESS_FORMAT = "Unparseable address: %s" +## +public static final class net.corda.core.utilities.NetworkHostAndPort$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.NetworkHostAndPort parse(String) +## +public final class net.corda.core.utilities.NonEmptySet extends java.lang.Object implements kotlin.jvm.internal.markers.KMappedMarker, java.util.Set + public boolean add(Object) + public boolean addAll(Collection) + public void clear() + public boolean contains(Object) + public boolean containsAll(Collection) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.NonEmptySet copyOf(Collection) + public boolean equals(Object) + public void forEach(function.Consumer) + public int getSize() + public int hashCode() + public final Object head() + public boolean isEmpty() + @org.jetbrains.annotations.NotNull public Iterator iterator() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.NonEmptySet of(Object) + @org.jetbrains.annotations.NotNull public stream.Stream parallelStream() + public boolean remove(Object) + public boolean removeAll(Collection) + public boolean retainAll(Collection) + @org.jetbrains.annotations.NotNull public Spliterator spliterator() + @org.jetbrains.annotations.NotNull public stream.Stream stream() + public Object[] toArray() + public Object[] toArray(Object[]) + @org.jetbrains.annotations.NotNull public String toString() + public static final net.corda.core.utilities.NonEmptySet$Companion Companion +## +public static final class net.corda.core.utilities.NonEmptySet$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.NonEmptySet copyOf(Collection) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.NonEmptySet of(Object) +## +public static final class net.corda.core.utilities.NonEmptySet$iterator$1 extends java.lang.Object implements java.util.Iterator, kotlin.jvm.internal.markers.KMappedMarker + public boolean hasNext() + public Object next() + public void remove() +## +public class net.corda.core.utilities.OpaqueBytes extends net.corda.core.utilities.ByteSequence + public (byte[]) + @org.jetbrains.annotations.NotNull public byte[] getBytes() + public int getOffset() + public int getSize() + public static final net.corda.core.utilities.OpaqueBytes$Companion Companion +## +public static final class net.corda.core.utilities.OpaqueBytes$Companion extends java.lang.Object +## +public final class net.corda.core.utilities.OpaqueBytesSubSequence extends net.corda.core.utilities.ByteSequence + public (byte[], int, int) + @org.jetbrains.annotations.NotNull public byte[] getBytes() + public int getOffset() + public int getSize() +## +public final class net.corda.core.utilities.ProgressTracker extends java.lang.Object + public final void endWithError(Throwable) + @org.jetbrains.annotations.NotNull public final List getAllSteps() + @org.jetbrains.annotations.NotNull public final rx.Observable getChanges() + @org.jetbrains.annotations.Nullable public final net.corda.core.utilities.ProgressTracker getChildProgressTracker(net.corda.core.utilities.ProgressTracker$Step) + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Step getCurrentStep() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Step getCurrentStepRecursive() + public final boolean getHasEnded() + @org.jetbrains.annotations.Nullable public final net.corda.core.utilities.ProgressTracker getParent() + public final int getStepIndex() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Step[] getSteps() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker getTopLevelTracker() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Step nextStep() + public final void setChildProgressTracker(net.corda.core.utilities.ProgressTracker$Step, net.corda.core.utilities.ProgressTracker) + public final void setCurrentStep(net.corda.core.utilities.ProgressTracker$Step) +## +public abstract static class net.corda.core.utilities.ProgressTracker$Change extends java.lang.Object +## +public static final class net.corda.core.utilities.ProgressTracker$Change$Position extends net.corda.core.utilities.ProgressTracker$Change + public (net.corda.core.utilities.ProgressTracker, net.corda.core.utilities.ProgressTracker$Step) + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Step component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Change$Position copy(net.corda.core.utilities.ProgressTracker, net.corda.core.utilities.ProgressTracker$Step) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Step getNewStep() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker getTracker() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() +## +public static final class net.corda.core.utilities.ProgressTracker$Change$Rendering extends net.corda.core.utilities.ProgressTracker$Change + public (net.corda.core.utilities.ProgressTracker, net.corda.core.utilities.ProgressTracker$Step) + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Step component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Change$Rendering copy(net.corda.core.utilities.ProgressTracker, net.corda.core.utilities.ProgressTracker$Step) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Step getOfStep() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker getTracker() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() +## +public static final class net.corda.core.utilities.ProgressTracker$Change$Structural extends net.corda.core.utilities.ProgressTracker$Change + public (net.corda.core.utilities.ProgressTracker, net.corda.core.utilities.ProgressTracker$Step) + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Step component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Change$Structural copy(net.corda.core.utilities.ProgressTracker, net.corda.core.utilities.ProgressTracker$Step) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker$Step getParent() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ProgressTracker getTracker() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() +## +public static final class net.corda.core.utilities.ProgressTracker$DONE extends net.corda.core.utilities.ProgressTracker$Step + public boolean equals(Object) + public static final net.corda.core.utilities.ProgressTracker$DONE INSTANCE +## +public static class net.corda.core.utilities.ProgressTracker$Step extends java.lang.Object + public (String) + @org.jetbrains.annotations.Nullable public net.corda.core.utilities.ProgressTracker childProgressTracker() + @org.jetbrains.annotations.NotNull public rx.Observable getChanges() + @org.jetbrains.annotations.NotNull public Map getExtraAuditData() + @org.jetbrains.annotations.NotNull public String getLabel() +## +public static final class net.corda.core.utilities.ProgressTracker$UNSTARTED extends net.corda.core.utilities.ProgressTracker$Step + public boolean equals(Object) + public static final net.corda.core.utilities.ProgressTracker$UNSTARTED INSTANCE +## +public interface net.corda.core.utilities.PropertyDelegate + public abstract Object getValue(Object, kotlin.reflect.KProperty) +## +public abstract class net.corda.core.utilities.Try extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.Try combine(net.corda.core.utilities.Try, kotlin.jvm.functions.Function2) + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.Try flatMap(kotlin.jvm.functions.Function1) + public abstract Object getOrThrow() + public abstract boolean isFailure() + public abstract boolean isSuccess() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.Try map(kotlin.jvm.functions.Function1) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.Try on(kotlin.jvm.functions.Function0) + public static final net.corda.core.utilities.Try$Companion Companion +## +public static final class net.corda.core.utilities.Try$Companion extends java.lang.Object + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.Try on(kotlin.jvm.functions.Function0) +## +public static final class net.corda.core.utilities.Try$Failure extends net.corda.core.utilities.Try + public (Throwable) + @org.jetbrains.annotations.NotNull public final Throwable component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.Try$Failure copy(Throwable) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final Throwable getException() + public Object getOrThrow() + public int hashCode() + public boolean isFailure() + public boolean isSuccess() + @org.jetbrains.annotations.NotNull public String toString() +## +public static final class net.corda.core.utilities.Try$Success extends net.corda.core.utilities.Try + public (Object) + public final Object component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.Try$Success copy(Object) + public boolean equals(Object) + public Object getOrThrow() + public final Object getValue() + public int hashCode() + public boolean isFailure() + public boolean isSuccess() + @org.jetbrains.annotations.NotNull public String toString() +## +public final class net.corda.core.utilities.UntrustworthyData extends java.lang.Object + public (Object) + public final Object getFromUntrustedWorld() + @co.paralleluniverse.fibers.Suspendable public final Object unwrap(net.corda.core.utilities.UntrustworthyData$Validator) +## +public static interface net.corda.core.utilities.UntrustworthyData$Validator extends java.io.Serializable + @co.paralleluniverse.fibers.Suspendable public abstract Object validate(Object) +## +public final class net.corda.core.utilities.UntrustworthyDataKt extends java.lang.Object + public static final Object unwrap(net.corda.core.utilities.UntrustworthyData, kotlin.jvm.functions.Function1) +## +public interface net.corda.core.utilities.VariablePropertyDelegate extends net.corda.core.utilities.PropertyDelegate + public abstract void setValue(Object, kotlin.reflect.KProperty, Object) +## +public final class net.corda.client.jackson.JacksonSupport extends java.lang.Object + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createDefaultMapper(net.corda.core.messaging.CordaRPCOps) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createDefaultMapper(net.corda.core.messaging.CordaRPCOps, com.fasterxml.jackson.core.JsonFactory) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createDefaultMapper(net.corda.core.messaging.CordaRPCOps, com.fasterxml.jackson.core.JsonFactory, boolean) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createInMemoryMapper(net.corda.core.node.services.IdentityService) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createInMemoryMapper(net.corda.core.node.services.IdentityService, com.fasterxml.jackson.core.JsonFactory) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createInMemoryMapper(net.corda.core.node.services.IdentityService, com.fasterxml.jackson.core.JsonFactory, boolean) + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createNonRpcMapper() + @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createNonRpcMapper(com.fasterxml.jackson.core.JsonFactory) + @org.jetbrains.annotations.NotNull public final com.fasterxml.jackson.databind.Module getCordaModule() + public static final net.corda.client.jackson.JacksonSupport INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$AmountDeserializer extends com.fasterxml.jackson.databind.JsonDeserializer + @org.jetbrains.annotations.NotNull public net.corda.core.contracts.Amount deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) + public static final net.corda.client.jackson.JacksonSupport$AmountDeserializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$AmountSerializer extends com.fasterxml.jackson.databind.JsonSerializer + public void serialize(net.corda.core.contracts.Amount, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + public static final net.corda.client.jackson.JacksonSupport$AmountSerializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$AnonymousPartyDeserializer extends com.fasterxml.jackson.databind.JsonDeserializer + @org.jetbrains.annotations.NotNull public net.corda.core.identity.AnonymousParty deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) + public static final net.corda.client.jackson.JacksonSupport$AnonymousPartyDeserializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$AnonymousPartySerializer extends com.fasterxml.jackson.databind.JsonSerializer + public void serialize(net.corda.core.identity.AnonymousParty, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + public static final net.corda.client.jackson.JacksonSupport$AnonymousPartySerializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$CordaX500NameDeserializer extends com.fasterxml.jackson.databind.JsonDeserializer + @org.jetbrains.annotations.NotNull public net.corda.core.identity.CordaX500Name deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) + public static final net.corda.client.jackson.JacksonSupport$CordaX500NameDeserializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$CordaX500NameSerializer extends com.fasterxml.jackson.databind.JsonSerializer + public void serialize(net.corda.core.identity.CordaX500Name, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + public static final net.corda.client.jackson.JacksonSupport$CordaX500NameSerializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$IdentityObjectMapper extends com.fasterxml.jackson.databind.ObjectMapper implements net.corda.client.jackson.JacksonSupport$PartyObjectMapper + public (net.corda.core.node.services.IdentityService, com.fasterxml.jackson.core.JsonFactory, boolean) + public final boolean getFuzzyIdentityMatch() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.IdentityService getIdentityService() + @org.jetbrains.annotations.NotNull public Set partiesFromName(String) + @org.jetbrains.annotations.Nullable public net.corda.core.identity.Party partyFromKey(java.security.PublicKey) + @org.jetbrains.annotations.Nullable public net.corda.core.identity.Party wellKnownPartyFromX500Name(net.corda.core.identity.CordaX500Name) +## +public static final class net.corda.client.jackson.JacksonSupport$NoPartyObjectMapper extends com.fasterxml.jackson.databind.ObjectMapper implements net.corda.client.jackson.JacksonSupport$PartyObjectMapper + public (com.fasterxml.jackson.core.JsonFactory) + @org.jetbrains.annotations.NotNull public Void partiesFromName(String) + @org.jetbrains.annotations.Nullable public net.corda.core.identity.Party partyFromKey(java.security.PublicKey) + @org.jetbrains.annotations.Nullable public net.corda.core.identity.Party wellKnownPartyFromX500Name(net.corda.core.identity.CordaX500Name) +## +public static final class net.corda.client.jackson.JacksonSupport$NodeInfoDeserializer extends com.fasterxml.jackson.databind.JsonDeserializer + @org.jetbrains.annotations.NotNull public net.corda.core.node.NodeInfo deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) + public static final net.corda.client.jackson.JacksonSupport$NodeInfoDeserializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$NodeInfoSerializer extends com.fasterxml.jackson.databind.JsonSerializer + public void serialize(net.corda.core.node.NodeInfo, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + public static final net.corda.client.jackson.JacksonSupport$NodeInfoSerializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$OpaqueBytesDeserializer extends com.fasterxml.jackson.databind.JsonDeserializer + @org.jetbrains.annotations.NotNull public net.corda.core.utilities.OpaqueBytes deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) + public static final net.corda.client.jackson.JacksonSupport$OpaqueBytesDeserializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$OpaqueBytesSerializer extends com.fasterxml.jackson.databind.JsonSerializer + public void serialize(net.corda.core.utilities.OpaqueBytes, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + public static final net.corda.client.jackson.JacksonSupport$OpaqueBytesSerializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$PartyDeserializer extends com.fasterxml.jackson.databind.JsonDeserializer + @org.jetbrains.annotations.NotNull public net.corda.core.identity.Party deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) + public static final net.corda.client.jackson.JacksonSupport$PartyDeserializer INSTANCE +## +public static interface net.corda.client.jackson.JacksonSupport$PartyObjectMapper + @org.jetbrains.annotations.NotNull public abstract Set partiesFromName(String) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party partyFromKey(java.security.PublicKey) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party wellKnownPartyFromX500Name(net.corda.core.identity.CordaX500Name) +## +public static final class net.corda.client.jackson.JacksonSupport$PartySerializer extends com.fasterxml.jackson.databind.JsonSerializer + public void serialize(net.corda.core.identity.Party, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + public static final net.corda.client.jackson.JacksonSupport$PartySerializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$PublicKeyDeserializer extends com.fasterxml.jackson.databind.JsonDeserializer + @org.jetbrains.annotations.NotNull public java.security.PublicKey deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) + public static final net.corda.client.jackson.JacksonSupport$PublicKeyDeserializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$PublicKeySerializer extends com.fasterxml.jackson.databind.JsonSerializer + public void serialize(java.security.PublicKey, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + public static final net.corda.client.jackson.JacksonSupport$PublicKeySerializer INSTANCE +## +public static final class net.corda.client.jackson.JacksonSupport$RpcObjectMapper extends com.fasterxml.jackson.databind.ObjectMapper implements net.corda.client.jackson.JacksonSupport$PartyObjectMapper + public (net.corda.core.messaging.CordaRPCOps, com.fasterxml.jackson.core.JsonFactory, boolean) + public final boolean getFuzzyIdentityMatch() + @org.jetbrains.annotations.NotNull public final net.corda.core.messaging.CordaRPCOps getRpc() + @org.jetbrains.annotations.NotNull public Set partiesFromName(String) + @org.jetbrains.annotations.Nullable public net.corda.core.identity.Party partyFromKey(java.security.PublicKey) + @org.jetbrains.annotations.Nullable public net.corda.core.identity.Party wellKnownPartyFromX500Name(net.corda.core.identity.CordaX500Name) +## +public static final class net.corda.client.jackson.JacksonSupport$SecureHashDeserializer extends com.fasterxml.jackson.databind.JsonDeserializer + public () + @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) +## +public static final class net.corda.client.jackson.JacksonSupport$SecureHashSerializer extends com.fasterxml.jackson.databind.JsonSerializer + public void serialize(net.corda.core.crypto.SecureHash, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + public static final net.corda.client.jackson.JacksonSupport$SecureHashSerializer INSTANCE +## +public abstract static class net.corda.client.jackson.JacksonSupport$SignedTransactionMixin extends java.lang.Object + public () + @com.fasterxml.jackson.annotation.JsonIgnore @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.SecureHash getId() + @com.fasterxml.jackson.annotation.JsonIgnore @org.jetbrains.annotations.NotNull public abstract List getInputs() + @com.fasterxml.jackson.annotation.JsonIgnore @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party getNotary() + @com.fasterxml.jackson.annotation.JsonIgnore @org.jetbrains.annotations.NotNull public abstract net.corda.core.transactions.NotaryChangeWireTransaction getNotaryChangeTx() + @com.fasterxml.jackson.annotation.JsonIgnore @org.jetbrains.annotations.NotNull public abstract Set getRequiredSigningKeys() + @com.fasterxml.jackson.annotation.JsonProperty @org.jetbrains.annotations.NotNull protected abstract List getSigs() + @com.fasterxml.jackson.annotation.JsonProperty @org.jetbrains.annotations.NotNull protected abstract net.corda.core.transactions.CoreTransaction getTransaction() + @com.fasterxml.jackson.annotation.JsonIgnore @org.jetbrains.annotations.NotNull public abstract net.corda.core.transactions.WireTransaction getTx() + @com.fasterxml.jackson.annotation.JsonIgnore @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializedBytes getTxBits() +## +public static final class net.corda.client.jackson.JacksonSupport$ToStringSerializer extends com.fasterxml.jackson.databind.JsonSerializer + public void serialize(Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + public static final net.corda.client.jackson.JacksonSupport$ToStringSerializer INSTANCE +## +public abstract static class net.corda.client.jackson.JacksonSupport$WireTransactionMixin extends java.lang.Object + public () + @com.fasterxml.jackson.annotation.JsonIgnore @org.jetbrains.annotations.NotNull public abstract List getAvailableComponentHashes() + @com.fasterxml.jackson.annotation.JsonIgnore @org.jetbrains.annotations.NotNull public abstract List getAvailableComponents() + @com.fasterxml.jackson.annotation.JsonIgnore @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.MerkleTree getMerkleTree() + @com.fasterxml.jackson.annotation.JsonIgnore @org.jetbrains.annotations.NotNull public abstract List getOutputStates() +## +public class net.corda.client.jackson.StringToMethodCallParser extends java.lang.Object + @kotlin.jvm.JvmOverloads public (Class) + @kotlin.jvm.JvmOverloads public (Class, com.fasterxml.jackson.databind.ObjectMapper) + public (kotlin.reflect.KClass) + @org.jetbrains.annotations.NotNull public final Map getAvailableCommands() + @org.jetbrains.annotations.NotNull protected final com.google.common.collect.Multimap getMethodMap() + @org.jetbrains.annotations.NotNull public final Map getMethodParamNames() + @org.jetbrains.annotations.NotNull public List paramNamesFromConstructor(reflect.Constructor) + @org.jetbrains.annotations.NotNull public List paramNamesFromMethod(reflect.Method) + @org.jetbrains.annotations.NotNull public final net.corda.client.jackson.StringToMethodCallParser$ParsedMethodCall parse(Object, String) + @org.jetbrains.annotations.NotNull public final Object[] parseArguments(String, List, String) + public static final net.corda.client.jackson.StringToMethodCallParser$Companion Companion +## +public static final class net.corda.client.jackson.StringToMethodCallParser$Companion extends java.lang.Object +## +public final class net.corda.client.jackson.StringToMethodCallParser$ParsedMethodCall extends java.lang.Object implements java.util.concurrent.Callable + public (net.corda.client.jackson.StringToMethodCallParser, Object, reflect.Method, Object[]) + @org.jetbrains.annotations.Nullable public Object call() + @org.jetbrains.annotations.NotNull public final Object[] getArgs() + @org.jetbrains.annotations.NotNull public final reflect.Method getMethod() + @org.jetbrains.annotations.Nullable public final Object invoke() +## +public static class net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException extends net.corda.core.CordaException + public (String, Throwable) +## +public static final class net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException$FailedParse extends net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException + public (Exception) +## +public static final class net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException$MissingParameter extends net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException + public (String, String, String) + @org.jetbrains.annotations.NotNull public final String getParamName() +## +public static final class net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException$ReflectionDataMissing extends net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException + public (String, int) +## +public static final class net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException$TooManyParameters extends net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException + public (String, String) +## +public static final class net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException$UnknownMethod extends net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException + public (String) + @org.jetbrains.annotations.NotNull public final String getMethodName() +## +public final class net.corda.client.rpc.CordaRPCClient extends java.lang.Object + @kotlin.jvm.JvmOverloads public (net.corda.core.utilities.NetworkHostAndPort) + @kotlin.jvm.JvmOverloads public (net.corda.core.utilities.NetworkHostAndPort, net.corda.client.rpc.CordaRPCClientConfiguration) + @org.jetbrains.annotations.NotNull public final net.corda.client.rpc.CordaRPCConnection start(String, String) + public final Object use(String, String, kotlin.jvm.functions.Function1) +## +public final class net.corda.client.rpc.CordaRPCClientConfiguration extends java.lang.Object + public (java.time.Duration) + @org.jetbrains.annotations.NotNull public final java.time.Duration component1() + @org.jetbrains.annotations.NotNull public final net.corda.client.rpc.CordaRPCClientConfiguration copy(java.time.Duration) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final java.time.Duration getConnectionMaxRetryInterval() + public int hashCode() + public String toString() + public static final net.corda.client.rpc.CordaRPCClientConfiguration$Companion Companion + @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final net.corda.client.rpc.CordaRPCClientConfiguration DEFAULT +## +public static final class net.corda.client.rpc.CordaRPCClientConfiguration$Companion extends java.lang.Object +## +public final class net.corda.client.rpc.CordaRPCConnection extends java.lang.Object implements net.corda.client.rpc.RPCConnection + public (net.corda.client.rpc.RPCConnection) + public void close() + public void forceClose() + @org.jetbrains.annotations.NotNull public net.corda.core.messaging.CordaRPCOps getProxy() + public int getServerProtocolVersion() + public void notifyServerAndClose() +## +public final class net.corda.client.rpc.PermissionException extends net.corda.core.CordaRuntimeException + public (String) +## +public interface net.corda.client.rpc.RPCConnection extends java.io.Closeable + public abstract void forceClose() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.RPCOps getProxy() + public abstract int getServerProtocolVersion() + public abstract void notifyServerAndClose() +## +public class net.corda.client.rpc.RPCException extends net.corda.core.CordaRuntimeException + public (String) + public (String, Throwable) +## +public @interface net.corda.client.rpc.RPCSinceVersion + public abstract int version() +## +public final class net.corda.client.rpc.UtilsKt extends java.lang.Object + public static final void notUsed(rx.Observable) +## From 659b447362898ca6d42d4a9f712c6b53cfef4d60 Mon Sep 17 00:00:00 2001 From: Clinton Date: Fri, 29 Sep 2017 17:18:06 +0100 Subject: [PATCH 064/185] V1 tests and fixes for the ContractConstraints work (#1739) * V1 tests and fixes for the ContractConstraints work * More fixes. --- .../net/corda/core/flows/FlowLogicRef.kt | 14 +--- .../contracts/isolated/IsolatedDummyFlow.kt | 2 +- .../node/services/AttachmentLoadingTests.kt | 83 ++++++++++++------- .../net/corda/node/internal/AbstractNode.kt | 9 +- .../statemachine/FlowLogicRefFactoryImpl.kt | 13 +-- .../statemachine/StateMachineManager.kt | 12 ++- 6 files changed, 73 insertions(+), 60 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt index 3f9996c081..0b90909180 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt @@ -24,16 +24,4 @@ class IllegalFlowLogicException(type: Class<*>, msg: String) : IllegalArgumentEx */ // TODO: align this with the existing [FlowRef] in the bank-side API (probably replace some of the API classes) @CordaSerializable -interface FlowLogicRef - - -/** - * This is just some way to track what attachments need to be in the class loader, but may later include some app - * properties loaded from the attachments. And perhaps the authenticated user for an API call? - */ -@CordaSerializable -data class AppContext(val attachments: List) { - // TODO: build a real [AttachmentsClassLoader] etc - val classLoader: ClassLoader - get() = this.javaClass.classLoader -} +interface FlowLogicRef \ No newline at end of file diff --git a/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/IsolatedDummyFlow.kt b/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/IsolatedDummyFlow.kt index 855c06cea4..97c35c2af3 100644 --- a/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/IsolatedDummyFlow.kt +++ b/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/IsolatedDummyFlow.kt @@ -31,4 +31,4 @@ class IsolatedDummyFlow { stx.verify(serviceHub) } } -} \ No newline at end of file +} diff --git a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt index 353203e557..0d6bc46545 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 @@ -1,10 +1,12 @@ package net.corda.node.services import net.corda.client.rpc.RPCException +import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.Contract import net.corda.core.contracts.PartyAndReference import net.corda.core.cordapp.CordappProvider import net.corda.core.flows.FlowLogic +import net.corda.core.flows.UnexpectedFlowEndException import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.concurrent.transpose @@ -16,12 +18,18 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.loggerFor +import net.corda.node.internal.StartedNode import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl +import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User +import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_NOTARY import net.corda.testing.TestDependencyInjectionBase +import net.corda.testing.driver.DriverDSL +import net.corda.testing.driver.DriverDSLExposedInterface +import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver import net.corda.testing.node.MockServices import net.corda.testing.resetTestSerialization @@ -30,6 +38,8 @@ import org.junit.Before import org.junit.Test import java.net.URLClassLoader import java.nio.file.Files +import java.sql.Driver +import kotlin.test.assertFails import kotlin.test.assertFailsWith class AttachmentLoadingTests : TestDependencyInjectionBase() { @@ -45,6 +55,13 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() { val logger = loggerFor() val isolatedJAR = AttachmentLoadingTests::class.java.getResource("isolated.jar")!! val ISOLATED_CONTRACT_ID = "net.corda.finance.contracts.isolated.AnotherDummyContract" + + val bankAName = CordaX500Name("BankA", "Zurich", "CH") + val bankBName = CordaX500Name("BankB", "Zurich", "CH") + val notaryName = CordaX500Name("Notary", "Zurich", "CH") + val flowInitiatorClass = + Class.forName("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator", true, URLClassLoader(arrayOf(isolatedJAR))) + .asSubclass(FlowLogic::class.java) } private lateinit var services: Services @@ -75,40 +92,42 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() { @Test fun `test that attachments retrieved over the network are not used for code`() { driver(initialiseSerialization = false) { - val bankAName = CordaX500Name("BankA", "Zurich", "CH") - val bankBName = CordaX500Name("BankB", "Zurich", "CH") - // Copy the app jar to the first node. The second won't have it. - val path = (baseDirectory(bankAName.toString()) / "plugins").createDirectories() / "isolated.jar" - logger.info("Installing isolated jar to $path") - isolatedJAR.openStream().buffered().use { input -> - Files.newOutputStream(path).buffered().use { output -> - input.copyTo(output) - } - } + installIsolatedCordappTo(bankAName) + val (bankA, bankB, _) = createTwoNodesAndNotary() - val admin = User("admin", "admin", permissions = setOf("ALL")) - val (bankA, bankB) = listOf( - startNode(providedName = bankAName, rpcUsers = listOf(admin)), - startNode(providedName = bankBName, rpcUsers = listOf(admin)) - ).transpose().getOrThrow() // Wait for all nodes to start up. - - val clazz = - Class.forName("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator", true, URLClassLoader(arrayOf(isolatedJAR))) - .asSubclass(FlowLogic::class.java) - - try { - bankA.rpcClientToNode().start("admin", "admin").use { rpc -> - val proxy = rpc.proxy - val party = proxy.wellKnownPartyFromX500Name(bankBName)!! - - assertFailsWith("net.corda.client.rpc.RPCException: net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator") { - proxy.startFlowDynamic(clazz, party).returnValue.getOrThrow() - } - } - } finally { - bankA.stop() - bankB.stop() + assertFailsWith("Party C=CH,L=Zurich,O=BankB rejected session request: Don't know net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator") { + bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow() } } } + + @Test + fun `tests that if the attachment is loaded on both sides already that a flow can run`() { + driver(initialiseSerialization = false) { + installIsolatedCordappTo(bankAName) + installIsolatedCordappTo(bankBName) + val (bankA, bankB, _) = createTwoNodesAndNotary() + bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow() + } + } + + private fun DriverDSLExposedInterface.installIsolatedCordappTo(nodeName: CordaX500Name) { + // Copy the app jar to the first node. The second won't have it. + val path = (baseDirectory(nodeName.toString()) / "plugins").createDirectories() / "isolated.jar" + logger.info("Installing isolated jar to $path") + isolatedJAR.openStream().buffered().use { input -> + Files.newOutputStream(path).buffered().use { output -> + input.copyTo(output) + } + } + } + + private fun DriverDSLExposedInterface.createTwoNodesAndNotary(): List { + val adminUser = User("admin", "admin", permissions = setOf("ALL")) + return listOf( + startNode(providedName = bankAName, rpcUsers = listOf(adminUser)), + startNode(providedName = bankBName, rpcUsers = listOf(adminUser)), + startNode(providedName = notaryName, rpcUsers = listOf(adminUser), advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) + ).transpose().getOrThrow() // Wait for all nodes to start up. + } } 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 f0711e97a7..9aacb16a83 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -58,10 +58,7 @@ import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService -import net.corda.node.services.statemachine.FlowStateMachineImpl -import net.corda.node.services.statemachine.StateMachineManager -import net.corda.node.services.statemachine.appName -import net.corda.node.services.statemachine.flowVersionAndInitiatingClass +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 @@ -190,7 +187,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, checkpointStorage, serverThread, database, - busyNodeLatch) + busyNodeLatch, + cordappLoader.appClassLoader) smm.tokenizableServices.addAll(tokenizableServices) @@ -213,6 +211,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, registerCordappFlows() _services.rpcFlows += cordappProvider.cordapps.flatMap { it.rpcFlows } registerCustomSchemas(cordappProvider.cordapps.flatMap { it.customSchemas }.toSet()) + FlowLogicRefFactoryImpl.classloader = cordappLoader.appClassLoader runOnStop += network::stop StartedNodeImpl(this, _services, info, checkpointStorage, smm, attachments, inNodeNetworkMapService, network, database, rpcOps) diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt index 6d16a94db5..a3c36fa3a4 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt @@ -2,6 +2,7 @@ package net.corda.node.services.statemachine import net.corda.core.internal.VisibleForTesting import com.google.common.primitives.Primitives +import net.corda.core.cordapp.CordappContext import net.corda.core.flows.* import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken @@ -17,7 +18,7 @@ import kotlin.reflect.jvm.javaType * The internal concrete implementation of the FlowLogicRef marker interface. */ @CordaSerializable -data class FlowLogicRefImpl internal constructor(val flowLogicClassName: String, val appContext: AppContext, val args: Map) : FlowLogicRef +data class FlowLogicRefImpl internal constructor(val flowLogicClassName: String, val args: Map) : FlowLogicRef /** * A class for conversion to and from [FlowLogic] and [FlowLogicRef] instances. @@ -32,6 +33,9 @@ data class FlowLogicRefImpl internal constructor(val flowLogicClassName: String, * in response to a potential malicious use or buggy update to an app etc. */ object FlowLogicRefFactoryImpl : SingletonSerializeAsToken(), FlowLogicRefFactory { + // TODO: Replace with a per app classloader/cordapp provider/cordapp loader - this will do for now + var classloader = javaClass.classLoader + override fun create(flowClass: Class>, vararg args: Any?): FlowLogicRef { if (!flowClass.isAnnotationPresent(SchedulableFlow::class.java)) { throw IllegalFlowLogicException(flowClass, "because it's not a schedulable flow") @@ -73,17 +77,14 @@ object FlowLogicRefFactoryImpl : SingletonSerializeAsToken(), FlowLogicRefFactor */ @VisibleForTesting internal fun createKotlin(type: Class>, args: Map): FlowLogicRef { - // TODO: we need to capture something about the class loader or "application context" into the ref, - // perhaps as some sort of ThreadLocal style object. For now, just create an empty one. - val appContext = AppContext(emptyList()) // Check we can find a constructor and populate the args to it, but don't call it createConstructor(type, args) - return FlowLogicRefImpl(type.name, appContext, args) + return FlowLogicRefImpl(type.name, args) } fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> { if (ref !is FlowLogicRefImpl) throw IllegalFlowLogicException(ref.javaClass, "FlowLogicRef was not created via correct FlowLogicRefFactory interface") - val klass = Class.forName(ref.flowLogicClassName, true, ref.appContext.classLoader).asSubclass(FlowLogic::class.java) + val klass = Class.forName(ref.flowLogicClassName, true, classloader).asSubclass(FlowLogic::class.java) return createConstructor(klass, ref.args)() } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index 11c3d219bc..28e2c58958 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -77,7 +77,8 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, val checkpointStorage: CheckpointStorage, val executor: AffinityExecutor, val database: CordaPersistence, - private val unfinishedFibers: ReusableLatch = ReusableLatch()) { + private val unfinishedFibers: ReusableLatch = ReusableLatch(), + private val classloader: ClassLoader = javaClass.classLoader) { inner class FiberScheduler : FiberExecutorScheduler("Same thread scheduler", executor) @@ -377,7 +378,12 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, updateCheckpoint(fiber) session to initiatedFlowFactory } catch (e: SessionRejectException) { - logger.warn("${e.logMessage}: $sessionInit") + // TODO: Handle this more gracefully + try { + logger.warn("${e.logMessage}: $sessionInit") + } catch (e: Throwable) { + logger.warn("Problematic session init message during logging", e) + } sendSessionReject(e.rejectMessage) return } catch (e: Exception) { @@ -400,7 +406,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, private fun getInitiatedFlowFactory(sessionInit: SessionInit): InitiatedFlowFactory<*> { val initiatingFlowClass = try { - Class.forName(sessionInit.initiatingFlowClass).asSubclass(FlowLogic::class.java) + Class.forName(sessionInit.initiatingFlowClass, true, classloader).asSubclass(FlowLogic::class.java) } catch (e: ClassNotFoundException) { throw SessionRejectException("Don't know ${sessionInit.initiatingFlowClass}") } catch (e: ClassCastException) { From fee156cc4a8f22f9357a5188f1aa87033de9aba5 Mon Sep 17 00:00:00 2001 From: Clinton Date: Sat, 30 Sep 2017 13:10:58 +0100 Subject: [PATCH 065/185] Added a contract constraints section to the key concepts doc. (#1704) Documentation for contract constraints. Added to index. Review fixes round 1. More review fixes. Review fixes. Explained package contents. review fixes. Addressed RGB's final review comments. Updated source code type to 'java' --- docs/packages.md | 12 ++++++------ docs/source/key-concepts-contract-constraints.rst | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/packages.md b/docs/packages.md index eb039e5237..9186dea33a 100644 --- a/docs/packages.md +++ b/docs/packages.md @@ -28,12 +28,6 @@ Internal, do not use. These APIs and implementations which are currently being r Exception types and some utilities for working with observables and futures. -# Package net.corda.core.cordapp - -This package contains the interface to CorDapps from within a node. A CorDapp can access its own context by using -the CordappProvider.getAppContext() class. These classes are not intended to be constructed manually and no interface -to do this will be provided. - # Package net.corda.core.concurrent Provides a simplified [java.util.concurrent.Future] class that allows registration of a callback to execute when the future @@ -47,6 +41,12 @@ with [Contract], or see the examples in [net.corda.finance.contracts]. Corda smart contracts are a combination of state held on the distributed ledger, and verification logic which defines which transformations of state are valid. +# Package net.corda.core.cordapp + +This package contains the interface to CorDapps from within a node. A CorDapp can access it's own context by using +the CordappProvider.getAppContext() class. These classes are not intended to be constructed manually and no interface +to do this will be provided. + # Package net.corda.core.crypto Cryptography data and utility classes used for signing, verifying, key management and data integrity checks. diff --git a/docs/source/key-concepts-contract-constraints.rst b/docs/source/key-concepts-contract-constraints.rst index 5518e45ff6..6c3e536264 100644 --- a/docs/source/key-concepts-contract-constraints.rst +++ b/docs/source/key-concepts-contract-constraints.rst @@ -15,7 +15,7 @@ A typical constraint is the hash of the CorDapp JAR that contains the contract a include constraints that require specific signers of the JAR, or both the signer and the hash. Constraints can be specified when constructing a transaction; if unspecified, an automatic constraint is used. -A ``TransactionState`` has a ``constraint`` field that represents that state's attachment constraint. When a party +``TransactionState``s have a ``constraint`` field that represents that state's attachment constraint. When a party constructs a ``TransactionState`` without specifying the constraint parameter a default value (``AutomaticHashConstraint``) is used. This default will be automatically resolved to a specific ``HashAttachmentConstraint`` that contains the hash of the attachment which contains the contract of that @@ -80,8 +80,8 @@ attachment JAR. This allows for trusting of attachments from trusted entities. Limitations ----------- -An ``AttachmentConstraint`` is verified by running the ``AttachmentConstraint.isSatisfiedBy`` method. When this is -called it is provided only the relevant attachment by the transaction that is verifying it. +``AttachmentConstraint``s are verified by running the ``AttachmentConstraint.isSatisfiedBy`` method. When this is called +it is provided only the relevant attachment by the transaction that is verifying it. Testing ------- From ed06443ce29cae112b44a24cae99c650c418fddf Mon Sep 17 00:00:00 2001 From: Clinton Date: Sun, 1 Oct 2017 22:26:01 +0100 Subject: [PATCH 066/185] Update Readme (#1756) * Update Readme Minor tweaks to Readme -- consistent capitalisation and more descriptive list of features (also reordered to put the important things first) * Copied master readme. * Update Readme Minor tweaks to Readme -- consistent capitalisation and more descriptive list of features (also reordered to put the important things first) --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 25b9d8e2e5..efa598a1f5 100644 --- a/README.md +++ b/README.md @@ -8,25 +8,25 @@ Corda is a decentralised database system in which nodes trust each other as litt ## Features -* A P2P network of nodes -* Smart contracts -* Flow framework -* "Notary" infrastructure to validate uniqueness of transactions -* Written as a platform for distributed apps called CorDapps +* Smart contracts that can be written in Java and other JVM languages +* Flow framework to manage communication and negotiation between participants +* Peer-to-peer network of nodes +* "Notary" infrastructure to validate uniqueness and sequencing of transactions without global broadcast +* Enables the development and deployment of distributed apps called CorDapps * Written in [Kotlin](https://kotlinlang.org), targeting the JVM ## Getting started -1. Read the [Getting started](https://docs.corda.net/getting-set-up.html) documentation -2. Run the [example CorDapp](https://docs.corda.net/tutorial-cordapp.html) +1. Read the [Getting Started](https://docs.corda.net/getting-set-up.html) documentation +2. Run the [Example CorDapp](https://docs.corda.net/tutorial-cordapp.html) 3. Read about Corda's [Key Concepts](https://docs.corda.net/key-concepts.html) 3. Follow the [Hello, World! tutorial](https://docs.corda.net/hello-world-index.html) ## Useful links -* [Project website](https://corda.net) +* [Project Website](https://corda.net) * [Documentation](https://docs.corda.net) -* [Slack channel](https://slack.corda.net/) +* [Slack Channel](https://slack.corda.net/) * [Stack Overflow tag](https://stackoverflow.com/questions/tagged/corda) * [Forum](https://discourse.corda.net) * [Meetups](https://www.meetup.com/pro/corda/) From 655f2be8b36eddf8255604e823cda6471060409b Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Mon, 2 Oct 2017 16:12:19 +0100 Subject: [PATCH 067/185] Fixed flaky test --- .../kotlin/net/corda/node/services/AttachmentLoadingTests.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 0d6bc46545..ecbe731158 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 @@ -124,10 +124,12 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() { private fun DriverDSLExposedInterface.createTwoNodesAndNotary(): List { val adminUser = User("admin", "admin", permissions = setOf("ALL")) - return listOf( + val nodes = listOf( startNode(providedName = bankAName, rpcUsers = listOf(adminUser)), startNode(providedName = bankBName, rpcUsers = listOf(adminUser)), startNode(providedName = notaryName, rpcUsers = listOf(adminUser), advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) ).transpose().getOrThrow() // Wait for all nodes to start up. + nodes.forEach { it.rpc.waitUntilNetworkReady() } + return nodes } } From 2d53859745d92e4921a33d2c176e1e4aeb658ec3 Mon Sep 17 00:00:00 2001 From: Clinton Date: Thu, 28 Sep 2017 18:30:17 +0100 Subject: [PATCH 068/185] Fixes bugs with contract constraints (#1696) * Added schedulable flows to cordapp scanning Fixed a bug where the core flows are included in every cordapp. Added a test to prove the scheduled flows are loaded correctly. Added scheduled flow support to cordapp. Renabled broken test. Fixed test to prove cordapps aren't retreived from network. Review fixes. Fixed a test issue caused by gradle having slightly different paths to IntelliJ * Fixed test for real this time. --- .../contracts/isolated/IsolatedDummyFlow.kt | 1 + .../nodeapi/internal/serialization/Kryo.kt | 2 +- .../net/corda/node/services/isolated.jar | Bin 12477 -> 12672 bytes .../internal/cordapp/CordappLoaderTest.kt | 2 ++ 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/IsolatedDummyFlow.kt b/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/IsolatedDummyFlow.kt index 97c35c2af3..a065b49c43 100644 --- a/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/IsolatedDummyFlow.kt +++ b/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/IsolatedDummyFlow.kt @@ -9,6 +9,7 @@ import net.corda.core.identity.Party * loaded or blocked. */ class IsolatedDummyFlow { + @StartableByRPC @InitiatingFlow class Initiator(val toWhom: Party) : FlowLogic() { @Suspendable diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt index 06820db6c2..82234df40b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt @@ -521,7 +521,7 @@ object LoggerSerializer : Serializer() { object ClassSerializer : Serializer>() { override fun read(kryo: Kryo, input: Input, type: Class>): Class<*> { val className = input.readString() - return Class.forName(className) + return Class.forName(className, true, kryo.classLoader) } override fun write(kryo: Kryo, output: Output, clazz: Class<*>) { diff --git a/node/src/integration-test/resources/net/corda/node/services/isolated.jar b/node/src/integration-test/resources/net/corda/node/services/isolated.jar index b1dfc71dac4cce43d4ba4bdb48c7b448d8ee1293..05544ab868067c1082eb80574f69c6dad2604b51 100644 GIT binary patch delta 5714 zcmZ8lWl$7q*j>7oMmnThmQF#sms|vv?q=zBr8}39P`VYQ1SzFKBo>h7lG4p02ngTR zx!-r^e($g6nK^Ujo#%OH&UsIl^-J5dXgDNg48Ipd-**z-t!P

amz=9zbc|du*N~ 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 CvIQ#u diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt index e333595f35..628077d8c1 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt @@ -55,6 +55,8 @@ class CordappLoaderTest { assertThat(actualCordapp.rpcFlows).isEmpty() assertThat(actualCordapp.schedulableFlows).isEmpty() assertThat(actualCordapp.services).isEmpty() + assertThat(actualCordapp.serializationWhitelists).hasSize(1) + assertThat(actualCordapp.serializationWhitelists.first().javaClass.name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedPlugin") assertThat(actualCordapp.jarPath).isEqualTo(isolatedJAR) } From 9e5d8044b4bf4e6cfe31d29837849c57c80981d0 Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Mon, 2 Oct 2017 17:59:01 +0100 Subject: [PATCH 069/185] Fixed test broken by merge. --- .../kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt index 628077d8c1..f2352d475d 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt @@ -56,7 +56,7 @@ class CordappLoaderTest { assertThat(actualCordapp.schedulableFlows).isEmpty() assertThat(actualCordapp.services).isEmpty() assertThat(actualCordapp.serializationWhitelists).hasSize(1) - assertThat(actualCordapp.serializationWhitelists.first().javaClass.name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedPlugin") + assertThat(actualCordapp.serializationWhitelists.first().javaClass.name).isEqualTo("net.corda.nodeapi.internal.serialization.DefaultWhitelist") assertThat(actualCordapp.jarPath).isEqualTo(isolatedJAR) } From 1e7474d08edf90574e746b10602c6fd41f7f0f3c Mon Sep 17 00:00:00 2001 From: cburlinchon <31621751+cburlinchon@users.noreply.github.com> Date: Tue, 3 Oct 2017 10:00:56 +0100 Subject: [PATCH 070/185] Use latest crash shell which fixes error when using multiple commands or hitting keys accidentally (#1729) --- node/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/build.gradle b/node/build.gradle index 276efe01df..10bd5877ff 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -157,7 +157,7 @@ dependencies { compile "io.netty:netty-all:$netty_version" // CRaSH: An embeddable monitoring and admin shell with support for adding new commands written in Groovy. - compile("com.github.corda.crash:crash.shell:9d242da2a10e686f33a3aefc69e4768824ad0716") { + compile("com.github.corda.crash:crash.shell:d5da86ba1b38e9c33af2a621dd15ba286307bec4") { exclude group: "org.slf4j", module: "slf4j-jdk14" } From 24b773eec1e90a988da1ec36c97d5d65c9f2a737 Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Mon, 2 Oct 2017 18:47:10 +0100 Subject: [PATCH 071/185] Added new API check script and API version --- .ci/api-current.txt | 47 +++++++++++++++------------------------- .ci/check-api-changes.sh | 17 +++++++++++++++ 2 files changed, 34 insertions(+), 30 deletions(-) create mode 100644 .ci/check-api-changes.sh diff --git a/.ci/api-current.txt b/.ci/api-current.txt index f90125cf32..6d69bc4289 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -462,9 +462,9 @@ public interface net.corda.core.cordapp.Cordapp @org.jetbrains.annotations.NotNull public abstract List getInitiatedFlows() @org.jetbrains.annotations.NotNull public abstract java.net.URL getJarPath() @org.jetbrains.annotations.NotNull public abstract String getName() - @org.jetbrains.annotations.NotNull public abstract List getPlugins() @org.jetbrains.annotations.NotNull public abstract List getRpcFlows() @org.jetbrains.annotations.NotNull public abstract List getSchedulableFlows() + @org.jetbrains.annotations.NotNull public abstract List getSerializationWhitelists() @org.jetbrains.annotations.NotNull public abstract List getServices() ## public final class net.corda.core.cordapp.CordappContext extends java.lang.Object @@ -564,6 +564,19 @@ public static final class net.corda.core.crypto.CompositeSignature$State extends public int hashCode() public String toString() ## +public final class net.corda.core.crypto.CompositeSignaturesWithKeys extends java.lang.Object + public (List) + @org.jetbrains.annotations.NotNull public final List component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.CompositeSignaturesWithKeys copy(List) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final List getSigs() + public int hashCode() + public String toString() + public static final net.corda.core.crypto.CompositeSignaturesWithKeys$Companion Companion +## +public static final class net.corda.core.crypto.CompositeSignaturesWithKeys$Companion extends java.lang.Object + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.CompositeSignaturesWithKeys getEMPTY() +## public final class net.corda.core.crypto.CordaObjectIdentifier extends java.lang.Object @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final org.bouncycastle.asn1.ASN1ObjectIdentifier COMPOSITE_KEY @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final org.bouncycastle.asn1.ASN1ObjectIdentifier COMPOSITE_SIGNATURE @@ -839,19 +852,6 @@ public final class net.corda.core.crypto.TransactionSignature extends net.corda. public final boolean isValid(net.corda.core.crypto.SecureHash) public final boolean verify(net.corda.core.crypto.SecureHash) ## -public final class net.corda.core.crypto.composite.CompositeSignaturesWithKeys extends java.lang.Object - public (List) - @org.jetbrains.annotations.NotNull public final List component1() - @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.composite.CompositeSignaturesWithKeys copy(List) - public boolean equals(Object) - @org.jetbrains.annotations.NotNull public final List getSigs() - public int hashCode() - public String toString() - public static final net.corda.core.crypto.composite.CompositeSignaturesWithKeys$Companion Companion -## -public static final class net.corda.core.crypto.composite.CompositeSignaturesWithKeys$Companion extends java.lang.Object - @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.composite.CompositeSignaturesWithKeys getEMPTY() -## public abstract class net.corda.core.flows.AbstractStateReplacementFlow extends java.lang.Object public () ## @@ -912,16 +912,6 @@ public static final class net.corda.core.flows.AbstractStateReplacementFlow$Upgr public int hashCode() public String toString() ## -public final class net.corda.core.flows.AppContext extends java.lang.Object - public (List) - @org.jetbrains.annotations.NotNull public final List component1() - @org.jetbrains.annotations.NotNull public final net.corda.core.flows.AppContext copy(List) - public boolean equals(Object) - @org.jetbrains.annotations.NotNull public final List getAttachments() - @org.jetbrains.annotations.NotNull public final ClassLoader getClassLoader() - public int hashCode() - public String toString() -## public final class net.corda.core.flows.CollectSignatureFlow extends net.corda.core.flows.FlowLogic public (net.corda.core.transactions.SignedTransaction, net.corda.core.flows.FlowSession, List) @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public List call() @@ -1495,10 +1485,6 @@ public static final class net.corda.core.messaging.StateMachineUpdate$Removed ex public int hashCode() public String toString() ## -public abstract class net.corda.core.node.CordaPluginRegistry extends java.lang.Object - public () - public boolean customizeSerialization(net.corda.core.serialization.SerializationCustomization) -## public final class net.corda.core.node.NodeInfo extends java.lang.Object public (List, List, int, long) @org.jetbrains.annotations.NotNull public final List component1() @@ -2462,8 +2448,6 @@ public static final class net.corda.core.serialization.SerializationContext$UseC public static net.corda.core.serialization.SerializationContext$UseCase valueOf(String) public static net.corda.core.serialization.SerializationContext$UseCase[] values() ## -public interface net.corda.core.serialization.SerializationCustomization -## public final class net.corda.core.serialization.SerializationDefaults extends java.lang.Object @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext getCHECKPOINT_CONTEXT() @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializationContext getP2P_CONTEXT() @@ -2496,6 +2480,9 @@ public static final class net.corda.core.serialization.SerializationFactory$Comp public interface net.corda.core.serialization.SerializationToken @org.jetbrains.annotations.NotNull public abstract Object fromToken(net.corda.core.serialization.SerializeAsTokenContext) ## +public interface net.corda.core.serialization.SerializationWhitelist + @org.jetbrains.annotations.NotNull public abstract List getWhitelist() +## public interface net.corda.core.serialization.SerializeAsToken @org.jetbrains.annotations.NotNull public abstract net.corda.core.serialization.SerializationToken toToken(net.corda.core.serialization.SerializeAsTokenContext) ## diff --git a/.ci/check-api-changes.sh b/.ci/check-api-changes.sh new file mode 100644 index 0000000000..a4273b9429 --- /dev/null +++ b/.ci/check-api-changes.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +echo "Starting API Diff" + +apiCurrent=./api-current.txt +if [ ! -f $apiCurrent ]; then + echo "Missing $apiCurrent file - cannot check API diff. Please rebase or add it to this release or ensure working dir is .ci/" + exit -1 +fi + +diffContents=`diff -u $apiCurrent ../build/api/api-corda-*.txt` +echo "Diff contents: " +echo "$diffContents" +removals=`echo "$diffContents" | grep "^-" | wc -l` +echo "Number of API removals/changes: "$removals +echo "Exiting with exit code" $removals +exit $removals \ No newline at end of file From 457f95f1881dc5894c48addf8e00376def42642c Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Tue, 3 Oct 2017 09:38:12 +0100 Subject: [PATCH 072/185] Removed the network map service type as it's no longer needed. The absence or presence of the networkMapService config is what determines if a node is the network map or not. --- .../kotlin/net/corda/core/flows/AttachmentTests.kt | 5 ++--- .../serialization/AttachmentSerializationTest.kt | 5 ++--- .../kotlin/net/corda/docs/CustomVaultQueryTest.kt | 7 +++---- .../corda/docs/FxTransactionBuildTutorialTest.kt | 3 +-- .../docs/WorkflowTransactionBuildTutorialTest.kt | 3 +-- .../net/corda/nodeapi/internal/ServiceType.kt | 1 - .../corda/node/services/BFTNotaryServiceTests.kt | 3 +-- .../kotlin/net/corda/node/internal/AbstractNode.kt | 5 +---- .../kotlin/net/corda/node/internal/NodeStartup.kt | 14 ++++++-------- .../node/services/config/NodeConfiguration.kt | 13 +++++++------ .../node/services/network/NetworkMapService.kt | 3 --- .../kotlin/net/corda/node/CordaRPCOpsImplTest.kt | 9 ++++----- .../corda/node/messaging/InMemoryMessagingTests.kt | 10 ++++------ .../net/corda/node/services/NotaryChangeTests.kt | 3 +-- .../node/services/events/ScheduledFlowTests.kt | 3 +-- .../network/AbstractNetworkMapServiceTest.kt | 11 +++-------- .../node/services/network/NetworkMapCacheTest.kt | 6 ++---- .../services/statemachine/FlowFrameworkTests.kt | 3 +-- .../services/transactions/NotaryServiceTests.kt | 4 +--- .../transactions/ValidatingNotaryServiceTests.kt | 9 +++------ .../net/corda/netmap/simulation/Simulation.kt | 12 +++++------- .../net/corda/testing/FlowStackSnapshotTest.kt | 3 +-- .../main/kotlin/net/corda/testing/driver/Driver.kt | 4 +--- .../main/kotlin/net/corda/testing/node/MockNode.kt | 2 +- .../kotlin/net/corda/testing/node/NodeBasedTest.kt | 7 +++---- .../net/corda/demobench/model/InstallFactory.kt | 4 +--- .../net/corda/demobench/model/NodeController.kt | 3 --- 27 files changed, 56 insertions(+), 99 deletions(-) diff --git a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt index dd5f1e6783..bf5c6af06f 100644 --- a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt @@ -11,7 +11,6 @@ import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode import net.corda.node.services.config.NodeConfiguration -import net.corda.node.services.network.NetworkMapService import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.utilities.DatabaseTransactionManager @@ -45,7 +44,7 @@ class AttachmentTests { mockNet.stopNodes() } - fun fakeAttachment(): ByteArray { + private fun fakeAttachment(): ByteArray { val bs = ByteArrayOutputStream() val js = JarOutputStream(bs) js.putNextEntry(ZipEntry("file1.txt")) @@ -127,7 +126,7 @@ class AttachmentTests { override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false } } } - }, advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type))) + }, advertisedServices = *arrayOf(ServiceInfo(SimpleNotaryService.type))) val bobNode = mockNet.createNode(aliceNode.network.myAddress, legalName = BOB.name) // Ensure that registration was successful before progressing any further diff --git a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt index 5692f69722..37183b1fd8 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt @@ -14,11 +14,10 @@ import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.internal.StartedNode -import net.corda.nodeapi.internal.ServiceInfo import net.corda.node.services.config.NodeConfiguration -import net.corda.node.services.network.NetworkMapService import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.utilities.DatabaseTransactionManager +import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.chooseIdentity import net.corda.testing.node.MockNetwork import org.junit.After @@ -70,7 +69,7 @@ class AttachmentSerializationTest { @Before fun setUp() { mockNet = MockNetwork() - server = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) + server = mockNet.createNode() client = mockNet.createNode(server.network.myAddress) client.internals.disableDBCloseOnStop() // Otherwise the in-memory database may disappear (taking the checkpoint with it) while we reboot the client. mockNet.runNetwork() 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 c8a8c89fc6..3105f151cb 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 @@ -7,11 +7,10 @@ import net.corda.core.utilities.getOrThrow import net.corda.finance.* import net.corda.finance.contracts.getCashBalances import net.corda.finance.flows.CashIssueFlow -import net.corda.node.internal.StartedNode import net.corda.finance.schemas.CashSchemaV1 -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.network.NetworkMapService +import net.corda.node.internal.StartedNode import net.corda.node.services.transactions.ValidatingNotaryService +import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.node.MockNetwork import org.junit.After @@ -36,7 +35,7 @@ class CustomVaultQueryTest { notaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, overrideServices = mapOf(notaryService to DUMMY_NOTARY_KEY), - advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), notaryService)) + advertisedServices = *arrayOf(notaryService)) nodeA = mockNet.createPartyNode(notaryNode.network.myAddress) nodeB = mockNet.createPartyNode(notaryNode.network.myAddress) 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 15c16c32b1..4548af15a3 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 @@ -9,7 +9,6 @@ import net.corda.finance.contracts.getCashBalances import net.corda.finance.flows.CashIssueFlow import net.corda.finance.schemas.CashSchemaV1 import net.corda.node.internal.StartedNode -import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* @@ -34,7 +33,7 @@ class FxTransactionBuildTutorialTest { notaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, overrideServices = mapOf(notaryService to DUMMY_NOTARY_KEY), - advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), notaryService)) + advertisedServices = *arrayOf(notaryService)) nodeA = mockNet.createPartyNode(notaryNode.network.myAddress) nodeB = mockNet.createPartyNode(notaryNode.network.myAddress) nodeA.internals.registerCustomSchemas(setOf(CashSchemaV1)) 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 831e8db3fb..4242dd79e3 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,7 +9,6 @@ import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.toFuture import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode -import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* @@ -39,7 +38,7 @@ class WorkflowTransactionBuildTutorialTest { notaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, overrideServices = mapOf(Pair(notaryService, DUMMY_NOTARY_KEY)), - advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), notaryService)) + advertisedServices = *arrayOf(notaryService)) nodeA = mockNet.createPartyNode(notaryNode.network.myAddress) nodeB = mockNet.createPartyNode(notaryNode.network.myAddress) nodeA.internals.registerInitiatedFlow(RecordCompletionFlow::class.java) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ServiceType.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ServiceType.kt index 65cdf86820..7ec0a1ce8f 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ServiceType.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ServiceType.kt @@ -27,7 +27,6 @@ class ServiceType private constructor(val id: String) { } val notary: ServiceType = corda.getSubType("notary") - val networkMap: ServiceType = corda.getSubType("network_map") fun parse(id: String): ServiceType = ServiceType(id) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt index a28f1e3a9c..97859a5cdb 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt @@ -18,7 +18,6 @@ import net.corda.core.utilities.Try import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode import net.corda.node.services.config.BFTSMaRtConfiguration -import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.BFTNonValidatingNotaryService import net.corda.node.services.transactions.minClusterSize import net.corda.node.services.transactions.minCorrectReplicas @@ -42,7 +41,7 @@ class BFTNotaryServiceTests { } private val mockNet = MockNetwork() - private val node = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) + private val node = mockNet.createNode() @After fun stopNodes() { 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 9aacb16a83..3e9ef56ae4 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -484,7 +484,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, private fun makeNetworkServices(tokenizableServices: MutableList) { val serviceTypes = advertisedServices.map { it.type } - inNodeNetworkMapService = if (NetworkMapService.type in serviceTypes) makeNetworkMapService() else NullNetworkMapService + inNodeNetworkMapService = if (configuration.networkMapService == null) makeNetworkMapService() else NullNetworkMapService val notaryServiceType = serviceTypes.singleOrNull { it.isNotary() } if (notaryServiceType != null) { val service = makeCoreNotaryService(notaryServiceType) @@ -526,9 +526,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, * updates) if one has been supplied. */ protected open fun registerWithNetworkMap(): CordaFuture { - require(networkMapAddress != null || NetworkMapService.type in advertisedServices.map { it.type }) { - "Initial network map address must indicate a node that provides a network map service" - } val address: SingleMessageRecipient = networkMapAddress ?: network.getAddressOfParty(PartyInfo.SingleNode(services.myInfo.legalIdentitiesAndCerts.first().party, info.addresses)) as SingleMessageRecipient // Register for updates, even if we're the one running the network map. 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 d180abaa41..51cc7b454b 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -73,7 +73,7 @@ open class NodeStartup(val args: Array) { cmdlineOptions.baseDirectory.createDirectories() startNode(conf, versionInfo, startTime, cmdlineOptions) } catch (e: Exception) { - if (e.message?.startsWith("Unknown named curve:") ?: false) { + if (e.message?.startsWith("Unknown named curve:") == true) { logger.error("Exception during node startup - ${e.message}. " + "This is a known OpenJDK issue on some Linux distributions, please use OpenJDK from zulu.org or Oracle JDK.") } else @@ -150,13 +150,12 @@ open class NodeStartup(val args: Array) { } open protected fun loadConfigFile(cmdlineOptions: CmdLineOptions): FullNodeConfiguration { - val conf = try { - cmdlineOptions.loadConfig() + try { + return cmdlineOptions.loadConfig() } catch (e: ConfigException) { println("Unable to load the configuration file: ${e.rootCause.message}") exitProcess(2) } - return conf } open protected fun banJavaSerialisation(conf: FullNodeConfiguration) { @@ -167,13 +166,12 @@ open class NodeStartup(val args: Array) { // 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 - val versionInfo = VersionInfo( + return VersionInfo( manifestValue("Corda-Platform-Version")?.toInt() ?: 1, manifestValue("Corda-Release-Version") ?: "Unknown", manifestValue("Corda-Revision") ?: "Unknown", manifestValue("Corda-Vendor") ?: "Unknown" ) - return versionInfo } private fun enforceSingleNodeIsRunning(baseDirectory: Path) { @@ -260,10 +258,10 @@ open class NodeStartup(val args: Array) { } private fun printPluginsAndServices(node: Node) { - node.configuration.extraAdvertisedServiceIds.filter { it.startsWith("corda.notary.") || it.startsWith("corda.network_map") }.let { + node.configuration.extraAdvertisedServiceIds.filter { it.startsWith("corda.notary.") }.let { if (it.isNotEmpty()) Node.printBasicNodeInfo("Providing additional services", it.joinToString()) } - Node.printBasicNodeInfo("Loaded CorDapps", node.cordappProvider.cordapps.map { it.name }.joinToString()) + Node.printBasicNodeInfo("Loaded CorDapps", node.cordappProvider.cordapps.joinToString { it.name }) } open fun drawBanner(versionInfo: VersionInfo) { 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 7d500b2202..cbaa194aad 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 @@ -3,12 +3,11 @@ package net.corda.node.services.config import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.internal.NetworkMapInfo -import net.corda.nodeapi.internal.ServiceInfo import net.corda.node.services.messaging.CertificateChainCheckPolicy -import net.corda.node.services.network.NetworkMapService import net.corda.nodeapi.User import net.corda.nodeapi.config.NodeSSLConfiguration import net.corda.nodeapi.config.OldConfig +import net.corda.nodeapi.internal.ServiceInfo import java.net.URL import java.nio.file.Path import java.util.* @@ -22,6 +21,10 @@ interface NodeConfiguration : NodeSSLConfiguration { // myLegalName should be only used in the initial network registration, we should use the name from the certificate instead of this. // TODO: Remove this so we don't accidentally use this identity in the code? val myLegalName: CordaX500Name + /** + * If null then configure the node to run as the netwok map service, otherwise use this to connect to the network map + * service. + */ val networkMapService: NetworkMapInfo? val minimumPlatformVersion: Int val emailAddress: String @@ -93,12 +96,10 @@ data class FullNodeConfiguration( } fun calculateServices(): Set { - val advertisedServices = extraAdvertisedServiceIds + return extraAdvertisedServiceIds .filter(String::isNotBlank) .map { ServiceInfo.parse(it) } - .toMutableSet() - if (networkMapService == null) advertisedServices += ServiceInfo(NetworkMapService.type) - return advertisedServices + .toSet() } } diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt index 3e9d3984ae..966d537954 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt @@ -19,7 +19,6 @@ import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor -import net.corda.nodeapi.internal.ServiceType import net.corda.node.services.api.AbstractNodeService import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.messaging.MessageHandlerRegistration @@ -72,8 +71,6 @@ interface NetworkMapService { const val PUSH_TOPIC = "platform.network_map.push" // Base topic for messages acknowledging pushed updates const val PUSH_ACK_TOPIC = "platform.network_map.push_ack" - - val type = ServiceType.networkMap } data class FetchMapRequest(val subscribe: Boolean, diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index ecd2e604d0..8fae3f60cc 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -1,6 +1,7 @@ package net.corda.node import co.paralleluniverse.fibers.Suspendable +import net.corda.client.rpc.PermissionException import net.corda.core.contracts.Amount import net.corda.core.contracts.ContractState import net.corda.core.contracts.Issued @@ -24,14 +25,12 @@ import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.internal.CordaRPCOpsImpl import net.corda.node.internal.StartedNode +import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT import net.corda.node.services.messaging.RpcContext -import net.corda.node.services.network.NetworkMapService -import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.nodeapi.internal.ServiceInfo import net.corda.node.services.transactions.SimpleNotaryService -import net.corda.client.rpc.PermissionException import net.corda.nodeapi.User +import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode @@ -68,7 +67,7 @@ class CordaRPCOpsImplTest { setCordappPackages("net.corda.finance.contracts.asset") mockNet = MockNetwork() - val networkMap = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) + val networkMap = mockNet.createNode() aliceNode = mockNet.createNode(networkMapAddress = networkMap.network.myAddress) notaryNode = mockNet.createNode(advertisedServices = ServiceInfo(SimpleNotaryService.type), networkMapAddress = networkMap.network.myAddress) rpc = CordaRPCOpsImpl(aliceNode.services, aliceNode.smm, aliceNode.database) diff --git a/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt b/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt index 95c26319c2..e949ca11d2 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt @@ -1,10 +1,8 @@ package net.corda.node.messaging -import net.corda.nodeapi.internal.ServiceInfo import net.corda.node.services.messaging.Message import net.corda.node.services.messaging.TopicStringValidator import net.corda.node.services.messaging.createMessage -import net.corda.node.services.network.NetworkMapService import net.corda.testing.node.MockNetwork import net.corda.testing.resetTestSerialization import org.junit.After @@ -49,7 +47,7 @@ class InMemoryMessagingTests { @Test fun basics() { - val node1 = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) + val node1 = mockNet.createNode() val node2 = mockNet.createNode(networkMapAddress = node1.network.myAddress) val node3 = mockNet.createNode(networkMapAddress = node1.network.myAddress) @@ -78,7 +76,7 @@ class InMemoryMessagingTests { @Test fun broadcast() { - val node1 = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) + val node1 = mockNet.createNode() val node2 = mockNet.createNode(networkMapAddress = node1.network.myAddress) val node3 = mockNet.createNode(networkMapAddress = node1.network.myAddress) @@ -97,9 +95,9 @@ class InMemoryMessagingTests { */ @Test fun `skip unhandled messages`() { - val node1 = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) + val node1 = mockNet.createNode() val node2 = mockNet.createNode(networkMapAddress = node1.network.myAddress) - var received: Int = 0 + var received = 0 node1.network.addMessageHandler("valid_message") { _, _ -> received++ diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index 024445b343..e1a560c1ca 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -12,7 +12,6 @@ import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds import net.corda.node.internal.StartedNode -import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* @@ -42,7 +41,7 @@ class NotaryChangeTests { mockNet = MockNetwork() oldNotaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, - advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type))) + advertisedServices = *arrayOf(ServiceInfo(ValidatingNotaryService.type))) clientNodeA = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress) clientNodeB = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress) newNotaryNode = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress, advertisedServices = ServiceInfo(ValidatingNotaryService.type)) diff --git a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt index 072fd74a05..9e4215a814 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt @@ -15,7 +15,6 @@ import net.corda.core.node.services.vault.SortAttribute import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode -import net.corda.node.services.network.NetworkMapService import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.internal.ServiceInfo @@ -98,7 +97,7 @@ class ScheduledFlowTests { mockNet = MockNetwork(threadPerNode = true) notaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, - advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type))) + advertisedServices = *arrayOf(ServiceInfo(ValidatingNotaryService.type))) val a = mockNet.createUnstartedNode(notaryNode.network.myAddress) val b = mockNet.createUnstartedNode(notaryNode.network.myAddress) diff --git a/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt index 7b133c965f..403a149183 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt @@ -7,7 +7,6 @@ import net.corda.core.node.NodeInfo import net.corda.core.serialization.deserialize import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode -import net.corda.nodeapi.internal.ServiceInfo import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.send @@ -25,12 +24,8 @@ import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.utilities.AddOrRemove import net.corda.node.utilities.AddOrRemove.ADD import net.corda.node.utilities.AddOrRemove.REMOVE -import net.corda.testing.ALICE -import net.corda.testing.BOB -import net.corda.testing.CHARLIE -import net.corda.testing.DUMMY_MAP -import net.corda.testing.chooseIdentity -import net.corda.testing.chooseIdentityAndCert +import net.corda.nodeapi.internal.ServiceInfo +import net.corda.testing.* import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode import org.assertj.core.api.Assertions.assertThat @@ -58,7 +53,7 @@ abstract class AbstractNetworkMapServiceTest mapServiceNode = mockNet.createNode( nodeFactory = nodeFactory, legalName = DUMMY_MAP.name, - advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type))) + advertisedServices = *arrayOf(ServiceInfo(SimpleNotaryService.type))) alice = mockNet.createNode(mapServiceNode.network.myAddress, nodeFactory = nodeFactory, legalName = ALICE.name) mockNet.runNetwork() lastSerial = System.currentTimeMillis() 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 01a65de71e..2baba6ff3c 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,10 +2,8 @@ package net.corda.node.services.network import net.corda.core.node.services.NetworkMapCache import net.corda.core.utilities.getOrThrow -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.ALICE import net.corda.testing.BOB -import net.corda.testing.DUMMY_NOTARY import net.corda.testing.chooseIdentity import net.corda.testing.node.MockNetwork import org.assertj.core.api.Assertions.assertThat @@ -40,8 +38,8 @@ class NetworkMapCacheTest { @Test fun `key collision`() { val entropy = BigInteger.valueOf(24012017L) - val aliceNode = mockNet.createNode(nodeFactory = MockNetwork.DefaultFactory, legalName = ALICE.name, entropyRoot = entropy, advertisedServices = ServiceInfo(NetworkMapService.type)) - val bobNode = mockNet.createNode(nodeFactory = MockNetwork.DefaultFactory, legalName = BOB.name, entropyRoot = entropy, advertisedServices = ServiceInfo(NetworkMapService.type)) + val aliceNode = mockNet.createNode(nodeFactory = MockNetwork.DefaultFactory, legalName = ALICE.name, entropyRoot = entropy) + val bobNode = mockNet.createNode(nodeFactory = MockNetwork.DefaultFactory, legalName = BOB.name, entropyRoot = entropy) assertEquals(aliceNode.info.chooseIdentity(), bobNode.info.chooseIdentity()) mockNet.runNetwork() diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index a08230d9ff..0d832f164a 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -31,7 +31,6 @@ import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.internal.StartedNode -import net.corda.node.services.network.NetworkMapService import net.corda.node.services.persistence.checkpoints import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.internal.ServiceInfo @@ -79,7 +78,7 @@ class FlowFrameworkTests { fun start() { setCordappPackages("net.corda.finance.contracts", "net.corda.testing.contracts") mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) - node1 = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) + node1 = mockNet.createNode() node2 = mockNet.createNode(networkMapAddress = node1.network.myAddress) mockNet.runNetwork() diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt index 7e4781ba30..eac332f7f6 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt @@ -14,10 +14,8 @@ import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds import net.corda.node.internal.StartedNode import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.network.NetworkMapService import net.corda.testing.* import net.corda.testing.contracts.DummyContract -import net.corda.testing.getDefaultNotary import net.corda.testing.node.MockNetwork import org.assertj.core.api.Assertions.assertThat import org.junit.After @@ -40,7 +38,7 @@ class NotaryServiceTests { mockNet = MockNetwork() notaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, - advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type))) + advertisedServices = *arrayOf(ServiceInfo(SimpleNotaryService.type))) clientNode = mockNet.createNode(notaryNode.network.myAddress) mockNet.runNetwork() // Clear network map registration messages notaryNode.internals.ensureRegistered() diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt index 52a2165960..4de1dfba75 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt @@ -10,16 +10,13 @@ import net.corda.core.flows.NotaryException import net.corda.core.flows.NotaryFlow import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.getOrThrow import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode -import net.corda.nodeapi.internal.ServiceInfo import net.corda.node.services.issueInvalidState -import net.corda.node.services.network.NetworkMapService +import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.contracts.DummyContract -import net.corda.testing.dummyCommand -import net.corda.testing.getDefaultNotary import net.corda.testing.node.MockNetwork import org.assertj.core.api.Assertions.assertThat import org.junit.After @@ -41,7 +38,7 @@ class ValidatingNotaryServiceTests { mockNet = MockNetwork() notaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, - advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type)) + advertisedServices = *arrayOf(ServiceInfo(ValidatingNotaryService.type)) ) clientNode = mockNet.createNode(notaryNode.network.myAddress) mockNet.runNetwork() // Clear network map registration messages diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt index 6ba3f19e18..faec432e7b 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt @@ -2,19 +2,18 @@ package net.corda.netmap.simulation import net.corda.core.flows.FlowLogic import net.corda.core.identity.CordaX500Name -import net.corda.finance.utils.CityDatabase -import net.corda.finance.utils.WorldMapLocation import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.utilities.ProgressTracker +import net.corda.finance.utils.CityDatabase +import net.corda.finance.utils.WorldMapLocation import net.corda.irs.api.NodeInterestRates import net.corda.node.internal.StartedNode -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.nodeapi.internal.ServiceType import net.corda.node.services.config.NodeConfiguration -import net.corda.node.services.network.NetworkMapService import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.transactions.SimpleNotaryService +import net.corda.nodeapi.internal.ServiceInfo +import net.corda.nodeapi.internal.ServiceType import net.corda.testing.DUMMY_MAP import net.corda.testing.DUMMY_NOTARY import net.corda.testing.DUMMY_REGULATOR @@ -92,7 +91,6 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, advertisedServices: Set, id: Int, overrideServices: Map?, entropyRoot: BigInteger): SimulatedNode { - require(advertisedServices.containsType(NetworkMapService.type)) val cfg = testNodeConfiguration( baseDirectory = config.baseDirectory, myLegalName = DUMMY_MAP.name) @@ -153,7 +151,7 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, val mockNet = MockNetwork(networkSendManuallyPumped, runAsync) // This one must come first. - val networkMap = mockNet.createNode(nodeFactory = NetworkMapNodeFactory, advertisedServices = ServiceInfo(NetworkMapService.type)) + val networkMap = mockNet.createNode(nodeFactory = NetworkMapNodeFactory) val notary = mockNet.createNode(networkMap.network.myAddress, nodeFactory = NotaryNodeFactory, advertisedServices = ServiceInfo(SimpleNotaryService.type)) val regulators = listOf(mockNet.createUnstartedNode(networkMap.network.myAddress, nodeFactory = RegulatorFactory)) val ratesOracle = mockNet.createUnstartedNode(networkMap.network.myAddress, nodeFactory = RatesOracleFactory) diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt index 33d682f2b4..15c8fb4c77 100644 --- a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt @@ -9,7 +9,6 @@ import net.corda.core.internal.read import net.corda.core.messaging.startFlow import net.corda.core.serialization.CordaSerializable import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.User import net.corda.nodeapi.internal.ServiceInfo @@ -297,7 +296,7 @@ class FlowStackSnapshotTest { val notaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, overrideServices = mapOf(notaryService to DUMMY_NOTARY_KEY), - advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), notaryService)) + advertisedServices = *arrayOf(notaryService)) val node = mockNet.createPartyNode(notaryNode.network.myAddress) node.internals.registerInitiatedFlow(DummyFlow::class.java) node.services.startFlow(FlowStackSnapshotSerializationTestingFlow()).resultFuture.get() diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index 64645569a4..15fd43b655 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -830,9 +830,7 @@ class DriverDSL( "rpcAddress" to rpcAddress.toString(), "rpcUsers" to defaultRpcUserList, "p2pAddress" to dedicatedNetworkMapAddress.toString(), - "useTestClock" to useTestClock, - "extraAdvertisedServiceIds" to listOf(ServiceInfo(NetworkMapService.type).toString()) - ) + "useTestClock" to useTestClock) ) return startNodeInternal(config, webAddress, startInProcess, maximumHeapSize) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index 446c4dc09d..ea351fe8f1 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -400,7 +400,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, overrideServices: Map? = null, serviceName: CordaX500Name? = null): StartedNode { return createNode(networkMapAddress, legalName = legalName, overrideServices = overrideServices, - advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type, serviceName))) + advertisedServices = *arrayOf(ServiceInfo(ValidatingNotaryService.type, serviceName))) } // Convenience method for Java diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt index ce703de814..2a126e5d21 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt @@ -8,17 +8,16 @@ import net.corda.core.internal.div import net.corda.core.utilities.getOrThrow import net.corda.node.internal.Node import net.corda.node.internal.StartedNode -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.nodeapi.internal.ServiceType import net.corda.node.services.config.ConfigHelper import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.config.configOf import net.corda.node.services.config.plus -import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.nodeapi.User import net.corda.nodeapi.config.parseAs +import net.corda.nodeapi.internal.ServiceInfo +import net.corda.nodeapi.internal.ServiceType import net.corda.testing.DUMMY_MAP import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.driver.addressMustNotBeBoundFuture @@ -91,7 +90,7 @@ abstract class NodeBasedTest : TestDependencyInjectionBase() { rpcUsers: List = emptyList(), configOverrides: Map = emptyMap()): StartedNode { check(_networkMapNode == null || _networkMapNode!!.info.legalIdentitiesAndCerts.first().name == legalName) - return startNodeInternal(legalName, platformVersion, advertisedServices + ServiceInfo(NetworkMapService.type), rpcUsers, configOverrides).apply { + return startNodeInternal(legalName, platformVersion, advertisedServices, rpcUsers, configOverrides).apply { _networkMapNode = this } } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt index ef4e59d91a..4d8876b2ed 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt @@ -3,8 +3,6 @@ package net.corda.demobench.model import com.typesafe.config.Config import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.NetworkHostAndPort -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.nodeapi.internal.ServiceType import tornadofx.* import java.io.IOException import java.nio.file.Files @@ -54,7 +52,7 @@ class InstallFactory : Controller() { } private fun Config.parseExtraServices(path: String): MutableList { - val services = serviceController.services.values.toSortedSet() + ServiceInfo(ServiceType.networkMap).toString() + val services = serviceController.services.values.toSortedSet() return this.getStringList(path) .filter { !it.isNullOrEmpty() } .map { svc -> 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 f6479308c2..367f04b847 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 @@ -3,8 +3,6 @@ package net.corda.demobench.model import net.corda.core.identity.CordaX500Name import net.corda.demobench.plugin.PluginController import net.corda.demobench.pty.R3Pty -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.nodeapi.internal.ServiceType import tornadofx.* import java.io.IOException import java.lang.management.ManagementFactory @@ -100,7 +98,6 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { if (hasNetworkMap()) { config.networkMap = networkMapConfig } else { - config.extraServices.add(ServiceInfo(ServiceType.networkMap).toString()) networkMapConfig = config log.info("Network map provided by: ${config.legalName}") } From c87e1045eb98cba66aaf0d882823e124042e2cc3 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Tue, 3 Oct 2017 11:47:53 +0100 Subject: [PATCH 073/185] CORDA-540: Perform schema verification (#1672) --- .../amqp/DeserializationInput.kt | 2 +- .../serialization/ListsSerializationTest.kt | 24 +++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt index af7fcb3f81..648f11eeae 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt @@ -60,7 +60,7 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) { deserializeAndReturnEnvelope(bytes, T::class.java) @Throws(NotSerializableException::class) - private fun getEnvelope(bytes: ByteSequence): Envelope { + internal fun getEnvelope(bytes: ByteSequence): Envelope { // Check that the lead bytes match expected header val headerSize = AmqpHeaderV1_0.size if (bytes.take(headerSize) != AmqpHeaderV1_0) { diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt index 176f7468a0..c392c8819e 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt @@ -4,8 +4,12 @@ import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.util.DefaultClassResolver import net.corda.core.serialization.* import net.corda.node.services.statemachine.SessionData +import net.corda.nodeapi.internal.serialization.amqp.DeserializationInput +import net.corda.nodeapi.internal.serialization.amqp.Envelope +import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.amqpSpecific +import net.corda.testing.kryoSpecific import org.assertj.core.api.Assertions import org.junit.Assert.* import org.junit.Test @@ -17,6 +21,13 @@ import java.util.* class ListsSerializationTest : TestDependencyInjectionBase() { private companion object { val javaEmptyListClass = Collections.emptyList().javaClass + + fun verifyEnvelope(serBytes: SerializedBytes, envVerBody: (Envelope) -> Unit) = + amqpSpecific("AMQP specific envelope verification") { + val context = SerializationFactory.defaultFactory.defaultContext + val envelope = DeserializationInput(SerializerFactory(context.whitelist, context.deserializationClassLoader)).getEnvelope(serBytes) + envVerBody(envelope) + } } @Test @@ -43,7 +54,7 @@ class ListsSerializationTest : TestDependencyInjectionBase() { } @Test - fun `check empty list serialises as Java emptyList`() { + fun `check empty list serialises as Java emptyList`() = kryoSpecific("Kryo specific test"){ val nameID = 0 val serializedForm = emptyList().serialize() val output = ByteArrayOutputStream().apply { @@ -83,14 +94,19 @@ class ListsSerializationTest : TestDependencyInjectionBase() { payload.add(Child(1)) payload.add(Child(2)) val container = CovariantContainer(payload) - assertEqualAfterRoundTripSerialization(container) - } + fun verifyEnvelopeBody(envelope: Envelope) { + envelope.schema.types.single { typeNotation -> typeNotation.name == java.util.List::class.java.name + "" } + } + + assertEqualAfterRoundTripSerialization(container, {bytes -> verifyEnvelope(bytes, ::verifyEnvelopeBody)}) + } } -internal inline fun assertEqualAfterRoundTripSerialization(obj: T) { +internal inline fun assertEqualAfterRoundTripSerialization(obj: T, noinline streamValidation: ((SerializedBytes) -> Unit)? = null) { val serializedForm: SerializedBytes = obj.serialize() + streamValidation?.invoke(serializedForm) val deserializedInstance = serializedForm.deserialize() assertEquals(obj, deserializedInstance) From f03699d1a3e3e4f3a4be821f25851589ad5459a5 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 3 Oct 2017 11:57:12 +0100 Subject: [PATCH 074/185] Docs: more package descriptions and take non-stabilised APIs out of the docs build. (#1774) --- docs/build.gradle | 7 ++++-- docs/packages.md | 63 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 53 insertions(+), 17 deletions(-) diff --git a/docs/build.gradle b/docs/build.gradle index 6112f63647..542cc7380f 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -9,7 +9,9 @@ dokka { moduleName = 'corda' outputDirectory = file("${rootProject.rootDir}/docs/build/html/api/kotlin") processConfigurations = ['compile'] - sourceDirs = files('../core/src/main/kotlin', '../client/jfx/src/main/kotlin', '../client/mock/src/main/kotlin', '../client/rpc/src/main/kotlin', '../node-api/src/main/kotlin', '../finance/src/main/kotlin', '../client/jackson/src/main/kotlin', '../testing/node-driver/src/main/kotlin', '../testing/test-utils/src/main/kotlin') + // TODO: Re-add '../testing/node-driver/src/main/kotlin', '../testing/test-utils/src/main/kotlin' when they're API stable + // TODO: Add '../client/jfx/src/main/kotlin' and '../client/mock/src/main/kotlin' if we decide to make them into public API + sourceDirs = files('../core/src/main/kotlin', '../client/rpc/src/main/kotlin', '../finance/src/main/kotlin', '../client/jackson/src/main/kotlin') includes = ['packages.md'] jdkVersion = 8 @@ -29,7 +31,8 @@ task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) { outputFormat = "javadoc" outputDirectory = file("${rootProject.rootDir}/docs/build/html/api/javadoc") processConfigurations = ['compile'] - sourceDirs = files('../core/src/main/kotlin', '../client/jfx/src/main/kotlin', '../client/mock/src/main/kotlin', '../client/rpc/src/main/kotlin', '../node-api/src/main/kotlin', '../finance/src/main/kotlin', '../client/jackson/src/main/kotlin', '../testing/node-driver/src/main/kotlin', '../testing/test-utils/src/main/kotlin') + // TODO: Make this a copy of the list above programmatically. + sourceDirs = files('../core/src/main/kotlin', '../client/rpc/src/main/kotlin', '../finance/src/main/kotlin', '../client/jackson/src/main/kotlin') includes = ['packages.md'] jdkVersion = 8 diff --git a/docs/packages.md b/docs/packages.md index 9186dea33a..7331383ccc 100644 --- a/docs/packages.md +++ b/docs/packages.md @@ -3,19 +3,6 @@ Utilities and serialisers for working with JSON representations of basic types. This adds Jackson support for the java.time API, some core types, and Kotlin data classes. -# Package net.corda.client.jfx.model - -Data models for binding data feeds from Corda nodes into a JavaFX user interface, by presenting the data as [javafx.beans.Observable] -types. - -# Package net.corda.client.jfx.utils - -Utility classes (i.e. data classes) used by the Corda JavaFX client. - -# Package net.corda.client.mock - -Tools used by the client to produce mock data for testing purposes. - # Package net.corda.client.rpc RPC client interface to Corda, for use both by user-facing client and integration with external systems. @@ -99,12 +86,58 @@ actual states rather than state references). Corda utility classes, providing a broad range of functionality to help implement both Corda nodes and CorDapps. + # Package net.corda.finance -The finance module is a CorDapp containing sample cash and obligation contracts, as well as providing several -useful data types such as [Amount]. +Some simple testing utilities like pre-defined top-level values for common currencies. Mostly useful for +writing unit tests in Kotlin. + +WARNING: NOT API STABLE. # Package net.corda.finance.utils A collection of utilities for summing financial states, for example, summing obligations to get total debts. +WARNING: NOT API STABLE. + +# Package net.corda.finance.contracts + +Various types for common financial concepts like day roll conventions, fixes, etc. + +WARNING: NOT API STABLE. + +# net.corda.finance.contracts.asset + +Cash states, obligations and commodities. + +WARNING: NOT API STABLE. + +# net.corda.finance.contracts.asset.cash.selection + +Provisional support for pluggable cash selectors, needed for different database backends. + +WARNING: NOT API STABLE. + +# net.corda.finance.contracts.math + +Splines and interpolation. + +WARNING: NOT API STABLE. + +# net.corda.finance.flows + +Cash payments and issuances. Two party "delivery vs payment" atomic asset swaps. + +WARNING: NOT API STABLE. + +# net.corda.finance.plugin + +JSON/Jackson plugin for business calendars. + +WARNING: NOT API STABLE. + +# net.corda.finance.schemas + +JPA (Java Persistence Architecture) schemas for the financial state types. + +WARNING: NOT API STABLE. \ No newline at end of file From c570f8c6eb9a63da17894338b97385f78c9d9f6b Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Tue, 3 Oct 2017 12:10:52 +0100 Subject: [PATCH 075/185] Fail the API check for lines starting with a single '-'. (#1787) --- .ci/check-api-changes.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) mode change 100644 => 100755 .ci/check-api-changes.sh diff --git a/.ci/check-api-changes.sh b/.ci/check-api-changes.sh old mode 100644 new mode 100755 index a4273b9429..dd7d8efd60 --- a/.ci/check-api-changes.sh +++ b/.ci/check-api-changes.sh @@ -11,7 +11,7 @@ fi diffContents=`diff -u $apiCurrent ../build/api/api-corda-*.txt` echo "Diff contents: " echo "$diffContents" -removals=`echo "$diffContents" | grep "^-" | wc -l` +removals=`echo "$diffContents" | grep "^-\s" | wc -l` echo "Number of API removals/changes: "$removals echo "Exiting with exit code" $removals -exit $removals \ No newline at end of file +exit $removals From 55af4d211faf7d42c05cd4b64ae6d2e8ba2a387f Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Tue, 3 Oct 2017 12:51:55 +0100 Subject: [PATCH 076/185] Derive working directory for API check from location of script. (#1789) --- .ci/check-api-changes.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.ci/check-api-changes.sh b/.ci/check-api-changes.sh index dd7d8efd60..e2bd6afddc 100755 --- a/.ci/check-api-changes.sh +++ b/.ci/check-api-changes.sh @@ -2,13 +2,15 @@ echo "Starting API Diff" -apiCurrent=./api-current.txt +APIHOME=$(dirname $0) + +apiCurrent=$APIHOME/api-current.txt if [ ! -f $apiCurrent ]; then - echo "Missing $apiCurrent file - cannot check API diff. Please rebase or add it to this release or ensure working dir is .ci/" + echo "Missing $apiCurrent file - cannot check API diff. Please rebase or add it to this release" exit -1 fi -diffContents=`diff -u $apiCurrent ../build/api/api-corda-*.txt` +diffContents=`diff -u $apiCurrent $APIHOME/../build/api/api-corda-*.txt` echo "Diff contents: " echo "$diffContents" removals=`echo "$diffContents" | grep "^-\s" | wc -l` From ba9e2f01bb4305c4490d9ca76a13f70362e00a50 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Tue, 3 Oct 2017 14:28:10 +0100 Subject: [PATCH 077/185] Improve API page wording (#1788) --- docs/source/api-index.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/source/api-index.rst b/docs/source/api-index.rst index 76e1689b89..d229c040ad 100644 --- a/docs/source/api-index.rst +++ b/docs/source/api-index.rst @@ -52,11 +52,12 @@ developers using them until we are able to graduate them into the public API: We hope to graduate the node-driver, test-utils and confidential-identities modules in the next feature release after 1.0. The bulk of the Corda API is found in the core module. Other modules should be assumed to be fully internal. -The web server module will be removed in future: you should build web frontends for CorDapps using standard frameworks +The web server module will be removed in future: you should build web front-ends for CorDapps using standard frameworks like Spring Boot, J2EE, Play, etc. -Code that falls into the following package namespaces are for internal use only and not public. In a future release the -node will not load any CorDapp which uses them. +Code that falls into the following packages namespaces are for internal use only and not for public use: * Any package in the ``net.corda`` namespace which contains ``.internal`` -* ``net.corda.node`` \ No newline at end of file +* ``net.corda.node`` + +In the future releases the node upon starting up will reject any CorDapps which uses classes from these packages. \ No newline at end of file From 4fa63d07ddf6c6768efc0876de7bdc2ca9d8e2d6 Mon Sep 17 00:00:00 2001 From: josecoll Date: Tue, 3 Oct 2017 14:29:47 +0100 Subject: [PATCH 078/185] Update build label to 1.1-SNAPSHOT (#1598) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 96cd49c445..47fc8ef7e6 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { file("$projectDir/constants.properties").withInputStream { constants.load(it) } // Our version: bump this on release. - ext.corda_release_version = "0.16-SNAPSHOT" + ext.corda_release_version = "1.1-SNAPSHOT" // Increment this on any release that changes public APIs anywhere in the Corda platform // TODO This is going to be difficult until we have a clear separation throughout the code of what is public and what is internal ext.corda_platform_version = 1 From e6e3c29d748382bf8dcd8246ef806eea8b285d6f Mon Sep 17 00:00:00 2001 From: Clinton Date: Tue, 3 Oct 2017 14:31:36 +0100 Subject: [PATCH 079/185] Fixed a few instances of waitUntilNetworkReady() to fix flaky tests. (#1785) --- .../net/corda/node/services/AttachmentLoadingTests.kt | 2 +- .../kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt | 4 ++-- .../main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt | 2 +- .../main/kotlin/net/corda/explorer/ExplorerSimulation.kt | 7 +++---- 4 files changed, 7 insertions(+), 8 deletions(-) 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 ecbe731158..27c9a3d08a 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 @@ -129,7 +129,7 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() { startNode(providedName = bankBName, rpcUsers = listOf(adminUser)), startNode(providedName = notaryName, rpcUsers = listOf(adminUser), advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) ).transpose().getOrThrow() // Wait for all nodes to start up. - nodes.forEach { it.rpc.waitUntilNetworkReady() } + nodes.forEach { it.rpc.waitUntilNetworkReady().getOrThrow() } return nodes } } diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt index 33a36ec75f..157b2c0fd6 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt @@ -33,8 +33,8 @@ class BankOfCordaRPCClientTest { // Big Corporation RPC Client val bigCorpClient = nodeBigCorporation.rpcClientToNode() val bigCorpProxy = bigCorpClient.start("bigCorpCFO", "password2").proxy - bocProxy.waitUntilNetworkReady() - bigCorpProxy.waitUntilNetworkReady() + bocProxy.waitUntilNetworkReady().getOrThrow() + bigCorpProxy.waitUntilNetworkReady().getOrThrow() // Register for Bank of Corda Vault updates val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt index 455c090973..a50e99b727 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt @@ -36,7 +36,7 @@ class BankOfCordaClientApi(val hostAndPort: NetworkHostAndPort) { // TODO: privileged security controls required client.start("bankUser", "test").use { connection -> val rpc = connection.proxy - rpc.waitUntilNetworkReady() + rpc.waitUntilNetworkReady().getOrThrow() // Resolve parties via RPC val issueToParty = rpc.wellKnownPartyFromX500Name(params.issueToPartyName) diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt index 11026a819c..706d5e3649 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -132,10 +132,9 @@ class ExplorerSimulation(val options: OptionSet) { issuerNodeGBP.nodeInfo.legalIdentities.first() to issuerRPCGBP, issuerNodeUSD.nodeInfo.legalIdentities.first() to issuerRPCUSD)) - aliceRPC.waitUntilNetworkReady() - bobRPC.waitUntilNetworkReady() - issuerRPCGBP.waitUntilNetworkReady() - issuerRPCUSD.waitUntilNetworkReady() + listOf(aliceRPC, bobRPC, issuerRPCGBP, issuerRPCUSD).map { + it.waitUntilNetworkReady().getOrThrow() + } } private fun startSimulation(eventGenerator: EventGenerator, maxIterations: Int) { From e2bb14da8ebb094a8663f651c98c75c67d837972 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Tue, 3 Oct 2017 15:07:34 +0100 Subject: [PATCH 080/185] CORDA-540: AMQP specific fixes in "node-api" project (#1765) --- .../net/corda/core/crypto/CompositeKeyTests.kt | 2 +- .../internal/serialization/SerializationScheme.kt | 4 ++-- .../nodeapi/internal/AttachmentsClassLoaderTests.kt | 3 +-- .../internal/serialization/ListsSerializationTest.kt | 6 +++--- .../internal/serialization/MapsSerializationTest.kt | 6 +++--- .../internal/serialization/SetsSerializationTest.kt | 3 ++- .../main/kotlin/net/corda/testing/CoreTestUtils.kt | 12 +++++++----- 7 files changed, 19 insertions(+), 17 deletions(-) diff --git a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt index 80124d722d..0c65317b44 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt @@ -216,7 +216,7 @@ class CompositeKeyTests : TestDependencyInjectionBase() { } @Test() - fun `composite key validation with graph cycle detection`() = kryoSpecific("Cycle exists in the object graph which is not currently supported in AMQP mode") { + fun `composite key validation with graph cycle detection`() = kryoSpecific("Cycle exists in the object graph which is not currently supported in AMQP mode") { val key1 = CompositeKey.Builder().addKeys(alicePublicKey, bobPublicKey).build() as CompositeKey val key2 = CompositeKey.Builder().addKeys(alicePublicKey, key1).build() as CompositeKey val key3 = CompositeKey.Builder().addKeys(alicePublicKey, key2).build() as CompositeKey diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt index 648d6e5817..ca6c4c5292 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt @@ -27,7 +27,7 @@ import java.util.concurrent.ExecutionException val attachmentsClassLoaderEnabledPropertyName = "attachments.class.loader.enabled" -object NotSupportedSeralizationScheme : SerializationScheme { +object NotSupportedSerializationScheme : SerializationScheme { private fun doThrow(): Nothing = throw UnsupportedOperationException("Serialization scheme not supported.") override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean = doThrow() @@ -107,7 +107,7 @@ open class SerializationFactoryImpl : SerializationFactory() { registeredSchemes .filter { scheme -> scheme.canDeserializeVersion(it.first, it.second) } .forEach { return@computeIfAbsent it } - NotSupportedSeralizationScheme + NotSupportedSerializationScheme } } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt index 9d756a8812..e51a974936 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt @@ -5,7 +5,6 @@ import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.internal.declaredField -import net.corda.core.internal.toLedgerTransaction import net.corda.core.internal.toWireTransaction import net.corda.core.node.ServiceHub import net.corda.core.node.services.AttachmentStorage @@ -302,7 +301,7 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() { @Test fun `test deserialize of WireTransaction where contract cannot be found`() { - kryoSpecific("Kryo verifies/loads attachments on deserialization, whereas AMQP currently does not") { + kryoSpecific("Kryo verifies/loads attachments on deserialization, whereas AMQP currently does not") { ClassLoaderForTests().use { child -> val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, child) val contract = contractClass.newInstance() as DummyContractBackdoor diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt index c392c8819e..f8d281392b 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt @@ -23,7 +23,7 @@ class ListsSerializationTest : TestDependencyInjectionBase() { val javaEmptyListClass = Collections.emptyList().javaClass fun verifyEnvelope(serBytes: SerializedBytes, envVerBody: (Envelope) -> Unit) = - amqpSpecific("AMQP specific envelope verification") { + amqpSpecific("AMQP specific envelope verification") { val context = SerializationFactory.defaultFactory.defaultContext val envelope = DeserializationInput(SerializerFactory(context.whitelist, context.deserializationClassLoader)).getEnvelope(serBytes) envVerBody(envelope) @@ -54,7 +54,7 @@ class ListsSerializationTest : TestDependencyInjectionBase() { } @Test - fun `check empty list serialises as Java emptyList`() = kryoSpecific("Kryo specific test"){ + fun `check empty list serialises as Java emptyList`() = kryoSpecific("Kryo specific test"){ val nameID = 0 val serializedForm = emptyList().serialize() val output = ByteArrayOutputStream().apply { @@ -71,7 +71,7 @@ class ListsSerializationTest : TestDependencyInjectionBase() { data class WrongPayloadType(val payload: ArrayList) @Test - fun `check throws for forbidden declared type`() = amqpSpecific("Such exceptions are not expected in Kryo mode.") { + fun `check throws for forbidden declared type`() = amqpSpecific("Such exceptions are not expected in Kryo mode.") { val payload = ArrayList() payload.add(1) payload.add(2) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/MapsSerializationTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/MapsSerializationTest.kt index 6f97a39892..9788885420 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/MapsSerializationTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/MapsSerializationTest.kt @@ -22,7 +22,7 @@ class MapsSerializationTest : TestDependencyInjectionBase() { } @Test - fun `check EmptyMap serialization`() = amqpSpecific("kotlin.collections.EmptyMap is not enabled for Kryo serialization") { + fun `check EmptyMap serialization`() = amqpSpecific("kotlin.collections.EmptyMap is not enabled for Kryo serialization") { assertEqualAfterRoundTripSerialization(emptyMap()) } @@ -41,7 +41,7 @@ class MapsSerializationTest : TestDependencyInjectionBase() { data class WrongPayloadType(val payload: HashMap) @Test - fun `check throws for forbidden declared type`() = amqpSpecific("Such exceptions are not expected in Kryo mode.") { + fun `check throws for forbidden declared type`() = amqpSpecific("Such exceptions are not expected in Kryo mode.") { val payload = HashMap(smallMap) val wrongPayloadType = WrongPayloadType(payload) Assertions.assertThatThrownBy { wrongPayloadType.serialize() } @@ -64,7 +64,7 @@ class MapsSerializationTest : TestDependencyInjectionBase() { } @Test - fun `check empty map serialises as Java emptyMap`() = kryoSpecific("Specifically checks Kryo serialization") { + fun `check empty map serialises as Java emptyMap`() = kryoSpecific("Specifically checks Kryo serialization") { val nameID = 0 val serializedForm = emptyMap().serialize() val output = ByteArrayOutputStream().apply { diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SetsSerializationTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SetsSerializationTest.kt index c1c5ea05dd..4a652a7521 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SetsSerializationTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SetsSerializationTest.kt @@ -5,6 +5,7 @@ import com.esotericsoftware.kryo.util.DefaultClassResolver import net.corda.core.serialization.serialize import net.corda.node.services.statemachine.SessionData import net.corda.testing.TestDependencyInjectionBase +import net.corda.testing.kryoSpecific import org.junit.Assert.* import org.junit.Test import java.io.ByteArrayOutputStream @@ -39,7 +40,7 @@ class SetsSerializationTest : TestDependencyInjectionBase() { } @Test - fun `check empty set serialises as Java emptySet`() { + fun `check empty set serialises as Java emptySet`() = kryoSpecific("Checks Kryo header properties") { val nameID = 0 val serializedForm = emptySet().serialize() val output = ByteArrayOutputStream().apply { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index bedda6486b..5bec1a4aeb 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -146,13 +146,15 @@ fun getTestPartyAndCertificate(name: CordaX500Name, publicKey: PublicKey, trustR return getTestPartyAndCertificate(Party(name, publicKey), trustRoot) } -inline fun kryoSpecific(reason: String, function: () -> Unit) = if(!AMQP_ENABLED) { +@Suppress("unused") +inline fun T.kryoSpecific(reason: String, function: () -> Unit) = if(!AMQP_ENABLED) { function() } else { loggerFor().info("Ignoring Kryo specific test, reason: $reason" ) } -inline fun amqpSpecific(reason: String, function: () -> Unit) = if(AMQP_ENABLED) { +@Suppress("unused") +inline fun T.amqpSpecific(reason: String, function: () -> Unit) = if(AMQP_ENABLED) { function() } else { loggerFor().info("Ignoring AMQP specific test, reason: $reason" ) @@ -169,15 +171,15 @@ fun ServiceHub.getDefaultNotary(): Party = networkMapCache.notaryIdentities.firs /** * Set the package to scan for cordapps - this overrides the default behaviour of scanning the cordapps directory - * @param packageName A package name that you wish to scan for cordapps + * @param packageNames A package name that you wish to scan for cordapps */ fun setCordappPackages(vararg packageNames: String) { CordappLoader.testPackages = packageNames.toList() } /** - * Unsets the default overriding behaviour of [setCordappPackage] + * Unsets the default overriding behaviour of [setCordappPackages] */ fun unsetCordappPackages() { CordappLoader.testPackages = emptyList() -} +} \ No newline at end of file From 97731bcaafabeca2536d868b5940d749d8c4a06b Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Tue, 3 Oct 2017 15:33:20 +0100 Subject: [PATCH 081/185] Add README for our automated API check. (#1793) * Add README for our automated API check. * Mention that the Corda Release Manager is expected to update the API file. --- .ci/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .ci/README.md diff --git a/.ci/README.md b/.ci/README.md new file mode 100644 index 0000000000..62b9669242 --- /dev/null +++ b/.ci/README.md @@ -0,0 +1,11 @@ +# !! DO NOT MODIFY THE API FILE IN THIS DIRECTORY !! + +The `api-current.txt` file contains a summary of Corda's current public APIs, +as generated by the `api-scanner` Gradle plugin. (See [here](../gradle-plugins/api-scanner/README.md) for a detailed description of this plugin.) It will be regenerated and the copy in this repository updated by the Release Manager with +each new Corda release. It will not be modified otherwise except under special circumstances that will require extra approval. + +Deleting or changing the existing Corda APIs listed in `api-current.txt` may +break developers' CorDapps in the next Corda release! Please remember that we +have committed to API Stability for CorDapps. + +# !! DO NOT MODIFY THE API FILE IN THIS DIRECTORY !! From eb0e2535ed20a8c2f31159be9deb0eb027148382 Mon Sep 17 00:00:00 2001 From: Alberto Arri <30873160+al-r3@users.noreply.github.com> Date: Tue, 3 Oct 2017 15:43:50 +0100 Subject: [PATCH 082/185] [CORDA-442] Make cordformation serialize NodeInfos to disk during deployment. (#1546) Initial PR for https://r3-cev.atlassian.net/projects/CORDA/issues/CORDA-442 Allow for cordformation not to specify which node is the network map. When that happens Cordformation will start each node and make it serialize its NodeInfo to disk. This make 'depolyNodes' slower. On my machine for the traderDemo it's ~25s PersistentNetworkMapCache will load files from disk at startup. Additionally nodeinfos are loaded in the networkMapCache only if they're newer than the currently known version. --- constants.properties | 2 +- docs/source/changelog.rst | 6 ++ .../java/net/corda/cordform/CordformNode.java | 5 ++ .../groovy/net/corda/plugins/Cordform.groovy | 65 +++++++++++--- .../main/kotlin/net/corda/node/ArgsParser.kt | 9 +- .../net/corda/node/internal/AbstractNode.kt | 25 +++++- .../kotlin/net/corda/node/internal/Node.kt | 5 ++ .../net/corda/node/internal/NodeStartup.kt | 20 +++-- .../services/network/NodeInfoSerializer.kt | 85 +++++++++++++++++++ .../network/PersistentNetworkMapCache.kt | 21 +++-- .../kotlin/net/corda/node/ArgsParserTest.kt | 9 +- .../network/NodeInfoSerializerTest.kt | 67 +++++++++++++++ 12 files changed, 286 insertions(+), 33 deletions(-) create mode 100644 node/src/main/kotlin/net/corda/node/services/network/NodeInfoSerializer.kt create mode 100644 node/src/test/kotlin/net/corda/node/services/network/NodeInfoSerializerTest.kt diff --git a/constants.properties b/constants.properties index a9ce732db8..286d4dbc1c 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=1.0.1 +gradlePluginsVersion=1.1.0 kotlinVersion=1.1.50 guavaVersion=21.0 bouncycastleVersion=1.57 diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 696507c047..d02e2d8f62 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -7,6 +7,12 @@ from the previous milestone release. UNRELEASED ---------- +* ``Cordform`` and node identity generation + * Cordform may not specify a value for ``NetworkMap``, when that happens, during the task execution the following happens: + 1. Each node is started and its signed serialized NodeInfo is written to disk in the node base directory. + 2. Every serialized ``NodeInfo`` above is copied in every other node "additional-node-info" folder under the NodeInfo folder. + * Nodes read all the nodes stored in ``additional-node-info`` when the ``NetworkMapService`` starts up. + * ``Cordapp`` now has a name field for identifying CorDapps and all CorDapp names are printed to console at startup. * Enums now respsect the whitelist applied to the Serializer factory serializing / deserializing them. If the enum isn't diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java index 9175bead2f..d219c52281 100644 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java +++ b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java @@ -9,6 +9,11 @@ import java.util.List; import java.util.Map; public class CordformNode implements NodeDefinition { + /** + * Path relative to the running node where the serialized NodeInfos are stored. + */ + public static final String NODE_INFO_DIRECTORY = "additional-node-infos"; + protected static final String DEFAULT_HOST = "localhost"; /** diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordform.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordform.groovy index 98668d85af..7bfd9e0f0d 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordform.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordform.groovy @@ -3,6 +3,7 @@ package net.corda.plugins import static org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME import net.corda.cordform.CordformContext import net.corda.cordform.CordformDefinition +import net.corda.cordform.CordformNode import org.apache.tools.ant.filters.FixCrLfFilter import org.bouncycastle.asn1.x500.X500Name import org.gradle.api.DefaultTask @@ -61,8 +62,8 @@ class Cordform extends DefaultTask { * @return A node instance. */ private Node getNodeByName(String name) { - for(Node node : nodes) { - if(node.name == name) { + for (Node node : nodes) { + if (node.name == name) { return node } } @@ -109,10 +110,14 @@ class Cordform extends DefaultTask { */ @TaskAction void build() { - String networkMapNodeName + String networkMapNodeName = initializeConfigurationAndGetNetworkMapNodeName() + installRunScript() + finalizeConfiguration(networkMapNodeName) + } + + private initializeConfigurationAndGetNetworkMapNodeName() { if (null != definitionClass) { def cd = loadCordformDefinition() - networkMapNodeName = cd.networkMapNodeName.toString() cd.nodeConfigurers.each { nc -> node { Node it -> nc.accept it @@ -124,21 +129,55 @@ class Cordform extends DefaultTask { project.projectDir.toPath().resolve(getNodeByName(nodeName).nodeDir.toPath()) } } + return cd.networkMapNodeName.toString() } else { - networkMapNodeName = this.networkMapNodeName nodes.each { it.rootDir directory } + return this.networkMapNodeName } - installRunScript() - def networkMapNode = getNodeByName(networkMapNodeName) - if (networkMapNode == null) - throw new IllegalStateException("The networkMap property refers to a node that isn't configured ($networkMapNodeName)") - nodes.each { - if(it != networkMapNode) { - it.networkMapAddress(networkMapNode.getP2PAddress(), networkMapNodeName) + } + + private finalizeConfiguration(String networkMapNodeName) { + Node networkMapNode = getNodeByName(networkMapNodeName) + if (networkMapNode == null) { + nodes.each { + it.build() + } + generateNodeInfos() + logger.info("Starting without networkMapNode, this an experimental feature") + } else { + nodes.each { + if (it != networkMapNode) { + it.networkMapAddress(networkMapNode.getP2PAddress(), networkMapNodeName) + } + it.build() + } + } + } + + Path fullNodePath(Node node) { + return project.projectDir.toPath().resolve(node.nodeDir.toPath()) + } + + private generateNodeInfos() { + nodes.each { Node node -> + def process = new ProcessBuilder("java", "-jar", Node.NODEJAR_NAME, "--just-generate-node-info") + .directory(fullNodePath(node).toFile()) + .redirectErrorStream(true) + .start() + .waitFor() + } + for (source in nodes) { + for (destination in nodes) { + if (source.nodeDir != destination.nodeDir) { + project.copy { + from fullNodePath(source).toString() + include 'nodeInfo-*' + into fullNodePath(destination).resolve(Node.NODE_INFO_DIRECTORY).toString() + } + } } - it.build() } } } diff --git a/node/src/main/kotlin/net/corda/node/ArgsParser.kt b/node/src/main/kotlin/net/corda/node/ArgsParser.kt index 1527beec44..db175ea2b7 100644 --- a/node/src/main/kotlin/net/corda/node/ArgsParser.kt +++ b/node/src/main/kotlin/net/corda/node/ArgsParser.kt @@ -34,6 +34,8 @@ class ArgsParser { 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 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 helpArg = optionParser.accepts("help").forHelp() fun parse(vararg args: String): CmdLineOptions { @@ -50,7 +52,9 @@ class ArgsParser { val isVersion = optionSet.has(isVersionArg) val noLocalShell = optionSet.has(noLocalShellArg) val sshdServer = optionSet.has(sshdServerArg) - return CmdLineOptions(baseDirectory, configFile, help, loggingLevel, logToConsole, isRegistration, isVersion, noLocalShell, sshdServer) + val justGenerateNodeInfo = optionSet.has(justGenerateNodeInfoArg) + return CmdLineOptions(baseDirectory, configFile, help, loggingLevel, logToConsole, isRegistration, isVersion, + noLocalShell, sshdServer, justGenerateNodeInfo) } fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink) @@ -64,7 +68,8 @@ data class CmdLineOptions(val baseDirectory: Path, val isRegistration: Boolean, val isVersion: Boolean, val noLocalShell: Boolean, - val sshdServer: Boolean) { + val sshdServer: Boolean, + val justGenerateNodeInfo : Boolean) { fun loadConfig() = ConfigHelper .loadConfig(baseDirectory, configFile) .parseAs() 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 3e9ef56ae4..55f72dc451 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -169,20 +169,36 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, return CordaRPCOpsImpl(services, smm, database) } - open fun start(): StartedNode { - require(started == null) { "Node has already been started" } + private fun saveOwnNodeInfo() { + NodeInfoSerializer().saveToFile(configuration.baseDirectory, info, services.keyManagementService) + } + + private fun initCertificate() { if (configuration.devMode) { log.warn("Corda node is running in dev mode.") configuration.configureWithDevSSLCertificate() } validateKeystore() + } + open fun generateNodeInfo() { + check(started == null) { "Node has already been started" } + initCertificate() + log.info("Generating nodeInfo ...") + initialiseDatabasePersistence { + makeServices() + saveOwnNodeInfo() + } + } + + open fun start(): StartedNode { + check(started == null) { "Node has already been started" } + initCertificate() log.info("Node starting up ...") - // Do all of this in a database transaction so anything that might need a connection has one. val startedImpl = initialiseDatabasePersistence { val tokenizableServices = makeServices() - + saveOwnNodeInfo() smm = StateMachineManager(services, checkpointStorage, serverThread, @@ -391,6 +407,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, services.transactionVerifierService, services.validatedTransactions, services.contractUpgradeService, services, cordappProvider, this) makeNetworkServices(tokenizableServices) + return tokenizableServices } 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 689ef67e20..7a183e2894 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -310,6 +310,11 @@ open class Node(override val configuration: FullNodeConfiguration, private val _startupComplete = openFuture() val startupComplete: CordaFuture get() = _startupComplete + override fun generateNodeInfo() { + initialiseSerialization() + super.generateNodeInfo() + } + override fun start(): StartedNode { if (initialiseSerialization) { initialiseSerialization() 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 51cc7b454b..6413daa3ee 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -92,18 +92,24 @@ open class NodeStartup(val args: Array) { open protected fun startNode(conf: FullNodeConfiguration, versionInfo: VersionInfo, startTime: Long, cmdlineOptions: CmdLineOptions) { val advertisedServices = conf.calculateServices() - val node = createNode(conf, versionInfo, advertisedServices).start() - printPluginsAndServices(node.internals) - node.internals.nodeReadyFuture.thenMatch({ + val node = createNode(conf, versionInfo, advertisedServices) + if (cmdlineOptions.justGenerateNodeInfo) { + // Perform the minimum required start-up logic to be able to write a nodeInfo to disk + node.generateNodeInfo() + return + } + val startedNode = node.start() + printPluginsAndServices(startedNode.internals) + startedNode.internals.nodeReadyFuture.thenMatch({ val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0 - val name = node.info.legalIdentitiesAndCerts.first().name.organisation + val name = startedNode.info.legalIdentitiesAndCerts.first().name.organisation Node.printBasicNodeInfo("Node for \"$name\" started up and registered in $elapsed sec") // Don't start the shell if there's no console attached. val runShell = !cmdlineOptions.noLocalShell && System.console() != null - node.internals.startupComplete.then { + startedNode.internals.startupComplete.then { try { - InteractiveShell.startShell(cmdlineOptions.baseDirectory, runShell, cmdlineOptions.sshdServer, node) + InteractiveShell.startShell(cmdlineOptions.baseDirectory, runShell, cmdlineOptions.sshdServer, startedNode) } catch(e: Throwable) { logger.error("Shell failed to start", e) } @@ -112,7 +118,7 @@ open class NodeStartup(val args: Array) { { th -> logger.error("Unexpected exception during registration", th) }) - node.internals.run() + startedNode.internals.run() } open protected fun logStartupInfo(versionInfo: VersionInfo, cmdlineOptions: CmdLineOptions, conf: FullNodeConfiguration) { diff --git a/node/src/main/kotlin/net/corda/node/services/network/NodeInfoSerializer.kt b/node/src/main/kotlin/net/corda/node/services/network/NodeInfoSerializer.kt new file mode 100644 index 0000000000..925f393b7a --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/network/NodeInfoSerializer.kt @@ -0,0 +1,85 @@ +package net.corda.node.services.network + +import net.corda.cordform.CordformNode +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.SignedData +import net.corda.core.internal.createDirectories +import net.corda.core.internal.div +import net.corda.core.internal.isDirectory +import net.corda.core.node.NodeInfo +import net.corda.core.node.services.KeyManagementService +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize +import net.corda.core.utilities.ByteSequence +import net.corda.core.utilities.loggerFor +import java.io.File +import java.nio.file.Files +import java.nio.file.Path + +/** + * Class containing the logic to serialize and de-serialize a [NodeInfo] to disk and reading it back. + */ +class NodeInfoSerializer { + + companion object { + val logger = loggerFor() + } + + /** + * Saves the given [NodeInfo] to a path. + * The node is 'encoded' as a SignedData, signed with the owning key of its first identity. + * The name of the written file will be "nodeInfo-" followed by the hash of the content. The hash in the filename + * is used so that one can freely copy these files without fearing to overwrite another one. + * + * @param path the path where to write the file, if non-existent it will be created. + * @param nodeInfo the NodeInfo to serialize. + * @param keyManager a KeyManagementService used to sign the NodeInfo data. + */ + fun saveToFile(path: Path, nodeInfo: NodeInfo, keyManager: KeyManagementService) { + try { + path.createDirectories() + val serializedBytes = nodeInfo.serialize() + val regSig = keyManager.sign(serializedBytes.bytes, nodeInfo.legalIdentities.first().owningKey) + val signedData = SignedData(serializedBytes, regSig) + val file = (path / ("nodeInfo-" + SecureHash.sha256(serializedBytes.bytes).toString())).toFile() + file.writeBytes(signedData.serialize().bytes) + } catch (e : Exception) { + logger.warn("Couldn't write node info to file: $e") + } + } + + /** + * Loads all the files contained in a given path and returns the deserialized [NodeInfo]s. + * Signatures are checked before returning a value. + * + * @param nodePath the node base path. NodeInfo files are searched for in nodePath/[NODE_INFO_FOLDER] + * @return a list of [NodeInfo]s + */ + fun loadFromDirectory(nodePath: Path): List { + val result = mutableListOf() + val nodeInfoDirectory = nodePath / CordformNode.NODE_INFO_DIRECTORY + if (!nodeInfoDirectory.isDirectory()) { + logger.info("$nodeInfoDirectory isn't a Directory, not loading NodeInfo from files") + return result + } + for (path in Files.list(nodeInfoDirectory)) { + val file = path.toFile() + if (file.isFile) { + try { + logger.info("Reading NodeInfo from file: $file") + val nodeInfo = loadFromFile(file) + result.add(nodeInfo) + } catch (e: Exception) { + logger.error("Exception parsing NodeInfo from file. $file" , e) + } + } + } + logger.info("Succesfully read ${result.size} NodeInfo files.") + return result + } + + private fun loadFromFile(file: File): NodeInfo { + val signedData = ByteSequence.of(file.readBytes()).deserialize>() + return signedData.verified() + } +} \ No newline at end of file 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 400db60ef3..f2f5e8396c 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,11 +1,11 @@ package net.corda.node.services.network import net.corda.core.concurrent.CordaFuture +import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.VisibleForTesting -import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.internal.concurrent.map import net.corda.core.internal.concurrent.openFuture import net.corda.core.messaging.DataFeed @@ -88,9 +88,17 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) } init { + loadFromFiles() serviceHub.database.transaction { loadFromDB() } } + private fun loadFromFiles() { + logger.info("Loading network map from files..") + for (node in NodeInfoSerializer().loadFromDirectory(serviceHub.configuration.baseDirectory)) { + addNode(node) + } + } + override fun getPartyInfo(party: Party): PartyInfo? { val nodes = serviceHub.database.transaction { queryByIdentityKey(party.owningKey) } if (nodes.size == 1 && nodes[0].isLegalIdentity(party)) { @@ -159,6 +167,12 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) override fun addNode(node: NodeInfo) { logger.info("Adding node with info: $node") synchronized(_changed) { + registeredNodes[node.legalIdentities.first().owningKey]?.let { + if (it.serial > node.serial) { + logger.info("Discarding older nodeInfo for ${node.legalIdentities.first().name}") + return + } + } val previousNode = registeredNodes.put(node.legalIdentities.first().owningKey, node) // TODO hack... we left the first one as special one if (previousNode == null) { logger.info("No previous node found") @@ -225,8 +239,6 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) } private fun processRegistration(reg: NodeRegistration) { - // TODO: Implement filtering by sequence number, so we only accept changes that are - // more recent than the latest change we've processed. when (reg.type) { AddOrRemove.ADD -> addNode(reg.node) AddOrRemove.REMOVE -> removeNode(reg.node) @@ -263,8 +275,7 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) logger.info("Loaded node info: $nodeInfo") val publicKey = parsePublicKeyBase58(nodeInfo.legalIdentitiesAndCerts.single { it.isMain }.owningKey) val node = nodeInfo.toNodeInfo() - registeredNodes.put(publicKey, node) - changePublisher.onNext(MapChange.Added(node)) // Redeploy bridges after reading from DB on startup. + addNode(node) _loadDBSuccess = true // This is used in AbstractNode to indicate that node is ready. } catch (e: Exception) { logger.warn("Exception parsing network map from the database.", e) diff --git a/node/src/test/kotlin/net/corda/node/ArgsParserTest.kt b/node/src/test/kotlin/net/corda/node/ArgsParserTest.kt index 94b09c11c5..94d798c4e2 100644 --- a/node/src/test/kotlin/net/corda/node/ArgsParserTest.kt +++ b/node/src/test/kotlin/net/corda/node/ArgsParserTest.kt @@ -23,7 +23,8 @@ class ArgsParserTest { isRegistration = false, isVersion = false, noLocalShell = false, - sshdServer = false)) + sshdServer = false, + justGenerateNodeInfo = false)) } @Test @@ -117,4 +118,10 @@ class ArgsParserTest { 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() + } } diff --git a/node/src/test/kotlin/net/corda/node/services/network/NodeInfoSerializerTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NodeInfoSerializerTest.kt new file mode 100644 index 0000000000..6fe8ea6a28 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/network/NodeInfoSerializerTest.kt @@ -0,0 +1,67 @@ +package net.corda.node.services.network + +import net.corda.cordform.CordformNode +import net.corda.core.internal.div +import net.corda.core.node.NodeInfo +import net.corda.core.node.services.KeyManagementService +import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.testing.* +import net.corda.testing.node.MockKeyManagementService +import net.corda.testing.node.NodeBasedTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.nio.charset.Charset +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.contentOf + +class NodeInfoSerializerTest : NodeBasedTest() { + + @Rule @JvmField var folder = TemporaryFolder() + + lateinit var keyManagementService: KeyManagementService + + // Object under test + val nodeInfoSerializer = NodeInfoSerializer() + + companion object { + val nodeInfoFileRegex = Regex("nodeInfo\\-.*") + val nodeInfo = NodeInfo(listOf(), listOf(getTestPartyAndCertificate(ALICE)), 0, 0) + } + + @Before + fun start() { + val identityService = InMemoryIdentityService(trustRoot = DEV_TRUST_ROOT) + keyManagementService = MockKeyManagementService(identityService, ALICE_KEY) + } + + @Test + fun `save a NodeInfo`() { + nodeInfoSerializer.saveToFile(folder.root.toPath(), nodeInfo, keyManagementService) + + assertEquals(1, folder.root.list().size) + val fileName = folder.root.list()[0] + assertTrue(fileName.matches(nodeInfoFileRegex)) + val file = (folder.root.path / fileName).toFile() + // Just check that something is written, another tests verifies that the written value can be read back. + assertThat(contentOf(file)).isNotEmpty() + } + + @Test + fun `load an empty Directory`() { + assertEquals(0, nodeInfoSerializer.loadFromDirectory(folder.root.toPath()).size) + } + + @Test + fun `load a non empty Directory`() { + val nodeInfoFolder = folder.newFolder(CordformNode.NODE_INFO_DIRECTORY) + nodeInfoSerializer.saveToFile(nodeInfoFolder.toPath(), nodeInfo, keyManagementService) + val nodeInfos = nodeInfoSerializer.loadFromDirectory(folder.root.toPath()) + + assertEquals(1, nodeInfos.size) + assertEquals(nodeInfo, nodeInfos.first()) + } +} From ab7507db69d8b841dde4bc3840f3fc578f5ca54c Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Tue, 3 Oct 2017 16:30:17 +0100 Subject: [PATCH 083/185] CORDA-599 Remove unnecessary laziness (#1517) --- .../net/corda/node/internal/AbstractNode.kt | 22 +++++++++---------- .../kotlin/net/corda/node/internal/Node.kt | 6 ++--- .../persistence/HibernateConfiguration.kt | 5 ++--- .../corda/node/utilities/CordaPersistence.kt | 12 +++++----- .../persistence/DBTransactionStorageTests.kt | 6 +---- .../persistence/HibernateConfigurationTest.kt | 3 +-- .../services/schema/HibernateObserverTests.kt | 3 +-- .../kotlin/net/corda/testing/node/MockNode.kt | 3 ++- .../net/corda/testing/node/MockServices.kt | 5 ++--- .../net/corda/testing/node/SimpleNode.kt | 2 +- 10 files changed, 30 insertions(+), 37 deletions(-) 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 55f72dc451..3472db31b4 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -185,8 +185,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, check(started == null) { "Node has already been started" } initCertificate() log.info("Generating nodeInfo ...") - initialiseDatabasePersistence { - makeServices() + val schemaService = NodeSchemaService() + initialiseDatabasePersistence(schemaService) { + makeServices(schemaService) saveOwnNodeInfo() } } @@ -195,9 +196,10 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, check(started == null) { "Node has already been started" } initCertificate() log.info("Node starting up ...") + val schemaService = NodeSchemaService() // Do all of this in a database transaction so anything that might need a connection has one. - val startedImpl = initialiseDatabasePersistence { - val tokenizableServices = makeServices() + val startedImpl = initialiseDatabasePersistence(schemaService) { + val tokenizableServices = makeServices(schemaService) saveOwnNodeInfo() smm = StateMachineManager(services, checkpointStorage, @@ -391,10 +393,10 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, * Builds node internal, advertised, and plugin services. * Returns a list of tokenizable services to be added to the serialisation context. */ - private fun makeServices(): MutableList { + private fun makeServices(schemaService: SchemaService): MutableList { checkpointStorage = DBCheckpointStorage() cordappProvider = CordappProviderImpl(cordappLoader) - _services = ServiceHubInternalImpl() + _services = ServiceHubInternalImpl(schemaService) attachments = NodeAttachmentService(services.monitoringService.metrics) cordappProvider.start(attachments) legalIdentity = obtainIdentity() @@ -482,10 +484,10 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, // Specific class so that MockNode can catch it. class DatabaseConfigurationException(msg: String) : CordaException(msg) - protected open fun initialiseDatabasePersistence(insideTransaction: () -> T): T { + protected open fun initialiseDatabasePersistence(schemaService: SchemaService, insideTransaction: () -> T): T { val props = configuration.dataSourceProperties if (props.isNotEmpty()) { - this.database = configureDatabase(props, configuration.database, { _services.schemaService }, createIdentityService = { _services.identityService }) + this.database = configureDatabase(props, configuration.database, schemaService, { _services.identityService }) // Now log the vendor string as this will also cause a connection to be tested eagerly. database.transaction { log.info("Connected to ${database.dataSource.connection.metaData.databaseProductName} database.") @@ -689,15 +691,13 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, protected open fun generateKeyPair() = cryptoGenerateKeyPair() - private inner class ServiceHubInternalImpl : ServiceHubInternal, SingletonSerializeAsToken() { - + private inner class ServiceHubInternalImpl(override val schemaService: SchemaService) : ServiceHubInternal, SingletonSerializeAsToken() { override val rpcFlows = ArrayList>>() override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage() override val auditService = DummyAuditService() override val monitoringService = MonitoringService(MetricRegistry()) override val validatedTransactions = makeTransactionStorage() override val transactionVerifierService by lazy { makeTransactionVerifierService() } - override val schemaService by lazy { NodeSchemaService() } override val networkMapCache by lazy { PersistentNetworkMapCache(this) } override val vaultService by lazy { NodeVaultService(this, database.hibernateConfig) } override val contractUpgradeService by lazy { ContractUpgradeServiceImpl() } 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 7a183e2894..31cb1cb27f 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -15,11 +15,11 @@ import net.corda.core.node.ServiceHub import net.corda.core.serialization.SerializationDefaults import net.corda.core.utilities.* import net.corda.node.VersionInfo -import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.node.serialization.KryoServerSerializationScheme import net.corda.node.serialization.NodeClock import net.corda.node.services.RPCUserService import net.corda.node.services.RPCUserServiceImpl +import net.corda.node.services.api.SchemaService import net.corda.nodeapi.internal.ServiceInfo import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.messaging.ArtemisMessagingServer @@ -287,7 +287,7 @@ open class Node(override val configuration: FullNodeConfiguration, * This is not using the H2 "automatic mixed mode" directly but leans on many of the underpinnings. For more details * on H2 URLs and configuration see: http://www.h2database.com/html/features.html#database_url */ - override fun initialiseDatabasePersistence(insideTransaction: () -> T): T { + override fun initialiseDatabasePersistence(schemaService: SchemaService, insideTransaction: () -> T): T { val databaseUrl = configuration.dataSourceProperties.getProperty("dataSource.url") val h2Prefix = "jdbc:h2:file:" if (databaseUrl != null && databaseUrl.startsWith(h2Prefix)) { @@ -304,7 +304,7 @@ open class Node(override val configuration: FullNodeConfiguration, printBasicNodeInfo("Database connection url is", "jdbc:h2:$url/node") } } - return super.initialiseDatabasePersistence(insideTransaction) + return super.initialiseDatabasePersistence(schemaService, insideTransaction) } private val _startupComplete = openFuture() diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/HibernateConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/persistence/HibernateConfiguration.kt index cbec0c793c..73d8f77dff 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/HibernateConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/HibernateConfiguration.kt @@ -24,7 +24,7 @@ import java.sql.Connection import java.util.* import java.util.concurrent.ConcurrentHashMap -class HibernateConfiguration(createSchemaService: () -> SchemaService, private val databaseProperties: Properties, private val createIdentityScervice: () -> IdentityService) { +class HibernateConfiguration(val schemaService: SchemaService, private val databaseProperties: Properties, private val createIdentityService: () -> IdentityService) { companion object { val logger = loggerFor() } @@ -33,7 +33,6 @@ class HibernateConfiguration(createSchemaService: () -> SchemaService, private v private val sessionFactories = ConcurrentHashMap, SessionFactory>() private val transactionIsolationLevel = parserTransactionIsolationLevel(databaseProperties.getProperty("transactionIsolationLevel") ?:"") - var schemaService = createSchemaService() init { logger.info("Init HibernateConfiguration for schemas: ${schemaService.schemaOptions.keys}") @@ -86,7 +85,7 @@ class HibernateConfiguration(createSchemaService: () -> SchemaService, private v } }) // register custom converters - applyAttributeConverter(AbstractPartyToX500NameAsStringConverter(createIdentityScervice)) + applyAttributeConverter(AbstractPartyToX500NameAsStringConverter(createIdentityService)) // Register a tweaked version of `org.hibernate.type.MaterializedBlobType` that truncates logged messages. // to avoid OOM when large blobs might get logged. applyBasicType(CordaMaterializedBlobType, CordaMaterializedBlobType.name) diff --git a/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt b/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt index de78ac1071..c1d6d8d0b7 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt @@ -23,13 +23,13 @@ import java.util.concurrent.CopyOnWriteArrayList const val NODE_DATABASE_PREFIX = "node_" //HikariDataSource implements Closeable which allows CordaPersistence to be Closeable -class CordaPersistence(var dataSource: HikariDataSource, private var createSchemaService: () -> SchemaService, +class CordaPersistence(var dataSource: HikariDataSource, private val schemaService: SchemaService, private val createIdentityService: ()-> IdentityService, databaseProperties: Properties): Closeable { var transactionIsolationLevel = parserTransactionIsolationLevel(databaseProperties.getProperty("transactionIsolationLevel")) val hibernateConfig: HibernateConfiguration by lazy { transaction { - HibernateConfiguration(createSchemaService, databaseProperties, createIdentityService) + HibernateConfiguration(schemaService, databaseProperties, createIdentityService) } } @@ -40,8 +40,8 @@ class CordaPersistence(var dataSource: HikariDataSource, private var createSchem } companion object { - fun connect(dataSource: HikariDataSource, createSchemaService: () -> SchemaService, createIdentityService: () -> IdentityService, databaseProperties: Properties): CordaPersistence { - return CordaPersistence(dataSource, createSchemaService, createIdentityService, databaseProperties).apply { + fun connect(dataSource: HikariDataSource, schemaService: SchemaService, createIdentityService: () -> IdentityService, databaseProperties: Properties): CordaPersistence { + return CordaPersistence(dataSource, schemaService, createIdentityService, databaseProperties).apply { DatabaseTransactionManager(this) } } @@ -107,10 +107,10 @@ class CordaPersistence(var dataSource: HikariDataSource, private var createSchem } } -fun configureDatabase(dataSourceProperties: Properties, databaseProperties: Properties?, createSchemaService: () -> SchemaService = { NodeSchemaService() }, createIdentityService: () -> IdentityService): CordaPersistence { +fun configureDatabase(dataSourceProperties: Properties, databaseProperties: Properties?, schemaService: SchemaService = NodeSchemaService(), createIdentityService: () -> IdentityService): CordaPersistence { val config = HikariConfig(dataSourceProperties) val dataSource = HikariDataSource(config) - val persistence = CordaPersistence.connect(dataSource, createSchemaService, createIdentityService, databaseProperties ?: Properties()) + val persistence = CordaPersistence.connect(dataSource, schemaService, createIdentityService, databaseProperties ?: Properties()) // Check not in read-only mode. persistence.transaction { 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 4ea017461d..e13460aea9 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 @@ -42,11 +42,7 @@ class DBTransactionStorageTests : TestDependencyInjectionBase() { fun setUp() { LogHelper.setLevel(PersistentUniquenessProvider::class) val dataSourceProps = makeTestDataSourceProperties() - - val createSchemaService = { NodeSchemaService() } - - database = configureDatabase(dataSourceProps, makeTestDatabaseProperties(), createSchemaService, ::makeTestIdentityService) - + database = configureDatabase(dataSourceProps, makeTestDatabaseProperties(), NodeSchemaService(), ::makeTestIdentityService) database.transaction { services = object : MockServices(BOB_KEY) { diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt index a5ac113dc6..79c4650c61 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt @@ -77,8 +77,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY, BOB_KEY, BOC_KEY) val dataSourceProps = makeTestDataSourceProperties() val defaultDatabaseProperties = makeTestDatabaseProperties() - val createSchemaService = { NodeSchemaService() } - database = configureDatabase(dataSourceProps, defaultDatabaseProperties, createSchemaService, ::makeTestIdentityService) + database = configureDatabase(dataSourceProps, defaultDatabaseProperties, NodeSchemaService(), ::makeTestIdentityService) database.transaction { hibernateConfig = database.hibernateConfig services = object : MockServices(BOB_KEY, BOC_KEY, DUMMY_NOTARY_KEY) { diff --git a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt index 7472b66b02..44fc3cb47b 100644 --- a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt @@ -70,8 +70,7 @@ class HibernateObserverTests { return parent } } - val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), { schemaService }, createIdentityService = ::makeTestIdentityService) - + val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), schemaService, createIdentityService = ::makeTestIdentityService) @Suppress("UNUSED_VARIABLE") val observer = HibernateObserver(rawUpdatesPublisher, database.hibernateConfig) database.transaction { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index ea351fe8f1..92a40391b5 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -26,6 +26,7 @@ import net.corda.core.utilities.loggerFor import net.corda.finance.utils.WorldMapLocation import net.corda.node.internal.AbstractNode import net.corda.node.internal.StartedNode +import net.corda.node.services.api.SchemaService import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.E2ETestKeyManagementService @@ -248,7 +249,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, @Suppress("unused") val place: WorldMapLocation get() = findMyLocation()!! private var dbCloser: (() -> Any?)? = null - override fun initialiseDatabasePersistence(insideTransaction: () -> T) = super.initialiseDatabasePersistence { + override fun initialiseDatabasePersistence(schemaService: SchemaService, insideTransaction: () -> T) = super.initialiseDatabasePersistence(schemaService) { dbCloser = database::close insideTransaction() } 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 7338a6c68f..367dcad228 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 @@ -100,9 +100,8 @@ open class MockServices(cordappPackages: List = emptyList(), vararg val cordappPackages: List = emptyList()): Pair { val dataSourceProps = makeTestDataSourceProperties() val databaseProperties = makeTestDatabaseProperties() - val createSchemaService = { NodeSchemaService(customSchemas) } val identityServiceRef: IdentityService by lazy { createIdentityService() } - val database = configureDatabase(dataSourceProps, databaseProperties, createSchemaService, { identityServiceRef }) + val database = configureDatabase(dataSourceProps, databaseProperties, NodeSchemaService(customSchemas), { identityServiceRef }) val mockService = database.transaction { object : MockServices(cordappPackages, *(keys.toTypedArray())) { override val identityService: IdentityService = database.transaction { identityServiceRef } @@ -158,7 +157,7 @@ open class MockServices(cordappPackages: List = emptyList(), vararg val lateinit var hibernatePersister: HibernateObserver - fun makeVaultService(hibernateConfig: HibernateConfiguration = HibernateConfiguration( { NodeSchemaService() }, makeTestDatabaseProperties(), { identityService })): VaultService { + fun makeVaultService(hibernateConfig: HibernateConfiguration = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties(), { identityService })): VaultService { val vaultService = NodeVaultService(this, hibernateConfig) hibernatePersister = HibernateObserver(vaultService.rawUpdates, hibernateConfig) return vaultService diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/SimpleNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/SimpleNode.kt index 726b0c5202..cfb1e3a29d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/SimpleNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/SimpleNode.kt @@ -37,7 +37,7 @@ class SimpleNode(val config: NodeConfiguration, val address: NetworkHostAndPort val monitoringService = MonitoringService(MetricRegistry()) val identity: KeyPair = generateKeyPair() val identityService: IdentityService = InMemoryIdentityService(trustRoot = trustRoot) - val database: CordaPersistence = configureDatabase(config.dataSourceProperties, config.database, { NodeSchemaService() }, { InMemoryIdentityService(trustRoot = trustRoot) }) + val database: CordaPersistence = configureDatabase(config.dataSourceProperties, config.database, NodeSchemaService(), { InMemoryIdentityService(trustRoot = trustRoot) }) val keyService: KeyManagementService = E2ETestKeyManagementService(identityService, setOf(identity)) val executor = ServiceAffinityExecutor(config.myLegalName.organisation, 1) // TODO: We should have a dummy service hub rather than change behaviour in tests From dd3d8ba626523e379b8888177ca19f2e377d0c42 Mon Sep 17 00:00:00 2001 From: josecoll Date: Tue, 3 Oct 2017 17:32:11 +0100 Subject: [PATCH 084/185] Merge release-v1 onto master (mostly documentation changes) (#1797) * Updated corda release version to 1.0.0.RC2 (#1641) * Fixed Simm Valuation Demo Test and enable serializabe java 8 lambdas. (#1655) * [CORDA-624] Node Explorer on Issuing cash throws MissingContractAttachements exception (#1656) (cherry picked from commit 27fea4d) * BIGINT fix for H2 coin selection. (#1658) * BIGINT fix for H2 coin selection. * Review feedback * CORDA-637 Node Explorer shows Network Map Service in Cash Issue dropdown (#1665) * [CORDA-637] Node Explorer shows Network Map Service in Cash Issue dropdown * add TODO to remove the hack * Declare this internal message string as "const". (#1676) * Merge "A variety of small fixes" into the 1.0 release branch (#1673) * Minor: improve javadocs in NodeInfo * Minor: use package descriptions in Kotlin build of api docs too, not just javadocs. * RPC: make RPCConnection non-internal, as it's a core API. Move docs around so they're on public API not internal API. * Add an IntelliJ scope that covers the currently supported Corda API. This is useful when used in combination with the "Highlight public declarations with missing KDoc" inspection. * Ironic: upgrade the version of the Gradle plugin that checks for upgraded versions of things. It had broken due being incompatible with the new versions of Gradle itself. * Docs: flesh out javadocs on ServiceHub * Docs: add @suppress to a few things that were polluting the Dokka docs. * Docs: mention RPC access in NodeInfo javadoc * IRS Fixes to bring UI closer to declared financial types (#1662) * Made problematic CordaRPCClient c'tor private (with internal bridge methods) and added correct c'tors for public use. (#1653) initialiseSerialization param has also been removed. * Fixing flow snapshot feature (#1685) * Fix validating notary flow to handle notary change transactions properly. (#1687) Add a notary change test for checking longer chains involving both regular and notary change transactions. * Unification of VaultQuery And VaultService APIs (into single VaultService interface) to simplify node bootstrapping and usability. (#1677) (#1688) * Identity documentation (#1620) * Sketch initial identity docs * Restructure confidential identity docs to better fit structure * Split confidential identities into API and concepts * Further expansion on basic identity conceptS * Merge Party type into api-identity.rst * Address feedback on written content * Rework inline code with literalinclude * Start addressing feedback from Richard * Clarify use of "counterparty" * Address comments on key concepts * Correct back to US english * Clarify distribution/publishing of identities * Update changelog around confidential identities * CORDA-642 Notary demo documentation fixes (#1682) * Notary demo documentation fixes. * One of the tables is prefixed. * CORDA-641: A temporary fix for contract upgrade transactions (#1700) * A temporary fix for contract upgrade transactions: during LedgerTransaction verification run the right logic based on whether it contains the UpgradeCommand. * Move ContractUpgradeFlowTest away from createSomeNodes() * Remove assembleBareTx as it's not used * Update corda version tag to 1.0.0-RC3 (#1705) * Hide SerializationContext from public API on TransactionBuilder (#1707) * Hide SerializationContext from public API on TransactionBuilder (cherry picked from commit 6ff7b7e) * Hide SerializationContext from public API on TransactionBuilder (cherry picked from commit 6ff7b7e) * Address feedback on confidential identities docs (#1701) * Address minor comments on confidential identities docs * Expand on implementation details of confidential identities * Cleanup * Clarify details of the data blob in the swap identites flow * Add that certificate path is not made public for confidential identities * FlowSession docs (#1693) * FlowSession docs (#1660) * FlowSession docs * PR comments * Milder example flow name * Fixes bugs with contract constraints (#1696) * Added schedulable flows to cordapp scanning Fixed a bug where the core flows are included in every cordapp. Added a test to prove the scheduled flows are loaded correctly. Added scheduled flow support to cordapp. Renabled broken test. Fixed test to prove cordapps aren't retreived from network. Review fixes. Fixed a test issue caused by gradle having slightly different paths to IntelliJ * Fixed test for real this time. * Consistent use of CordaException and CordaRuntimeException (#1710) * Custom exceptions in corda, should either derive from an appropriate closely related java exception, or CordaException, or CordaRuntimeException. They should not inherit just from Exception, or RuntimeException. Handle PR comments Add nicer constructors to CordaException and CordaRuntimeException * Fix ambiguous defaulted constructor * Add @suppress (#1725) * Git-ignore Node Explorer config. (#1709) * add message warning windows users they might need to manually kill explorer demo nodes started by gradle (#1717) (#1726) * Misc documentation fixes (#1694) (cherry picked from commit 592896f) * Document -parameters compiler arg for Java CorDapps. (#1712) * Correct non-anonymous two party trade flow (#1731) * Parameterize TwoPartyTradeFlowTests to confirm deanonymised functionality works. * Correct handling of counterparty using well known identity in TWoPartyTradeFlow * CORDA-594 - SIMM Demo doc update (#1723) (#1735) * CORDA-594 - SIMM Demo doc update For V1 write a series of JSON / curl commands a user can follow to run the demo * Review Comments * Updated the rationale behind as to why SIMM was introduced. * typo * Cordapps now have a name field. (#1664) Corrected cordapp name generation. Added changelog entry. * Small API fixes against M16 (#1737) * Move CompositeSignaturesWithKeys into net.corda.core.crypto package. (cherry picked from commit 8f29562) * Rename and move CordaPluginRegistry to reflect its real purpose now. Simplify serialization code a bit. (cherry picked from commit e2ecd3a) * Docs: docsite improvements * Remove discussion of webserver from 'writing a cordapp' page. * Fixup some flow docs. * Add a couple more package descriptions. (cherry picked from commit 2aedc43) * Review comments (cherry picked from commit ba1d007) * Review comments - always apply default whitelist and no longer load it via ServiceLoader (cherry picked from commit 7d4d7bb) * Added wording about renaming services resource file * Update corda version tag to 1.0.0-RC4 (#1734) * Update corda version tag to 1.0.0-RC3 * Update corda version tag to 1.0.0-RC4 * Update build.gradle * V1 tests and fixes for the ContractConstraints work (#1739) * V1 tests and fixes for the ContractConstraints work * More fixes. * Added a contract constraints section to the key concepts doc. (#1704) Documentation for contract constraints. Added to index. Review fixes round 1. More review fixes. Review fixes. Explained package contents. review fixes. Addressed RGB's final review comments. Updated source code type to 'java' * Fixes dead links. (#1749) * Update gradle plugins version to 1.0.0 (#1753) * Update Readme (#1756) * Update Readme Minor tweaks to Readme -- consistent capitalisation and more descriptive list of features (also reordered to put the important things first) * Copied master readme. * Update Readme Minor tweaks to Readme -- consistent capitalisation and more descriptive list of features (also reordered to put the important things first) * Fixes .rst formatting. (#1751) * Updates tutorials. (#1649) * Updates tutorials. * Addresses review comments. * Tutorial refresh for v1.0 and moving of code into separate files. (#1758) * Moves code sections in tutorials to code files. * Removes wallet references. * Updates repo layout doc. * Removes remaining cordapp-tutorial references, replaced with cordapp-example. * Fixes broken link. * Misc docs fixes. * Refreshes the ServiceHub and rpc ops api pages. * Updates the cheat sheet. * Updates cookbooks. * Refreshes the running-a-notary tutorial. * Updates flow-testing tutorial * Updates tear-offs tutorial. * Refreshes integration-testing tutorial. * Updates to contract tutorial and accompanying code to bring inline with V1 release. * Refreshes contract-upgrade tutorial. * Fixed broken code sample in "writing a contract" and updated contracts dsl. * Added contract ref to java code. Fixed broken rst markup. * Updates transaction-building tutorial. * Updates the client-rpc and flow-state-machines tutorials. * Updates the oracles tutorial. * Amended country in X500 names from "UK" to "GB" * Update FlowCookbook.kt * Amended cheatsheet. Minor update on contract upgrades tutoraial. * Added `extraCordappPackagesToScan` to node driver. * Changes to match new function signature. * Update to reflect change in location of cash contract name. * CORDA-670: Correct scanned packages in network visualiser (#1763) * Add CorDapp dependency of IRS to network visualiser * Set CorDapp directories * Checking out the latest milestone will no longer be required. (#1761) * Updated documentation indices (#1754) * Update documentation indices. * Reference a moveable tag for V1 docs. Remove redundant warning text. * Reverted proposed usage of new docs release tag * Minor: print a deprecation warning when the web server starts. (#1767) * Release and upgrade notes for V1.0 (#1736) * Release and upgrade notes for V1.0 * Update changelog.rst * Update changelog.rst * Formatting. * Incorporating review feedback from KB and MN. * "guarantee" instead of "promise" * Updated with final review comments from KB and RGB. * Updated upgrade notes to describe migration from removed CordaPluginRegistry. * Minor clarification. * Minor updates following final RGB feedback. * Kat's further pedantic feedback * Minor changes following feedback from KB. * Incorporating review feedback from MH. * killed 'patent-pending' * Made the visualiser into a regular JVM module - not a CorDapp. (#1771) * Docs: more package descriptions and take non-stabilised APIs out of the docs build. (#1775) * Update corda version tag to 1.0.0 * Updated release notes to fix minor typos (#1779) Fixed bold type on simplified annotation driven scanning bullet and added bold type to module name bullets * Fixed drop down.. probably. (#1780) * fixed formatting for release notes. (#1782) * Improve API page wording (#1784) * Removed "unreleased" sections from the release notes and change log. * Merge remote-tracking branch 'origin/release-V1' into colljos-merge-v1-docs # Conflicts: # build.gradle # client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt # client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt # client/rpc/src/main/kotlin/net/corda/client/rpc/PermissionException.kt # constants.properties # core/src/main/kotlin/net/corda/core/flows/FlowSession.kt # core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt # core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt # docs/source/api-flows.rst # docs/source/api-index.rst # docs/source/changelog.rst # docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java # docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt # docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/contract/TutorialContract.kt # docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt # docs/source/hello-world-state.rst # docs/source/key-concepts-contract-constraints.rst # docs/source/serialization.rst # docs/source/tut-two-party-flow.rst # docs/source/tutorial-tear-offs.rst # node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt # node-api/src/test/java/net/corda/nodeapi/internal/serialization/ForbiddenLambdaSerializationTests.java # node-api/src/test/java/net/corda/nodeapi/internal/serialization/LambdaCheckpointSerializationTest.java # node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt # node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt # node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt # node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt # node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt # samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt # samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt # testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt # testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt # testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt # webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt --- .gitignore | 3 + build.gradle | 2 +- .../client/jfx/model/NetworkIdentityModel.kt | 5 +- .../client/jfx/model/NodeMonitorModel.kt | 4 +- .../kotlin/net/corda/core/flows/FlowLogic.kt | 4 + .../net/corda/core/flows/FlowSession.kt | 4 +- docs/source/_static/versions | 1 + docs/source/api-core-types.rst | 14 - docs/source/api-flows.rst | 93 ++++- docs/source/api-identity.rst | 129 +++++++ docs/source/api-index.rst | 1 + docs/source/changelog.rst | 80 ++-- docs/source/deploying-a-node.rst | 10 +- .../java/net/corda/docs/FlowCookbookJava.java | 3 +- .../kotlin/net/corda/docs/FlowCookbook.kt | 3 +- .../net/corda/docs/LaunchSpaceshipFlow.kt | 99 +++++ docs/source/index.rst | 3 - .../key-concepts-contract-constraints.rst | 4 +- docs/source/key-concepts-identity.rst | 78 ++++ docs/source/key-concepts.rst | 1 + docs/source/release-notes.rst | 105 +++++- docs/source/release-process-index.rst | 2 + docs/source/resources/signatureMetadata.png | Bin 0 -> 18666 bytes docs/source/resources/subTreesPrivacy.png | Bin 0 -> 105835 bytes docs/source/running-the-demos.rst | 13 +- docs/source/serialization.rst | 8 +- docs/source/shell.rst | 4 + docs/source/troubleshooting.rst | 5 + docs/source/tut-two-party-flow.rst | 24 +- docs/source/tutorial-tear-offs.rst | 9 +- docs/source/upgrade-notes.rst | 349 ++++++++++++++++++ .../corda/finance/flows/TwoPartyDealFlow.kt | 2 + .../corda/finance/flows/TwoPartyTradeFlow.kt | 6 +- .../node/services/AttachmentLoadingTests.kt | 9 +- .../node/messaging/TwoPartyTradeFlowTests.kt | 15 +- samples/network-visualiser/build.gradle | 10 +- .../net/corda/netmap/simulation/Simulation.kt | 2 + .../corda/testing/contracts/DummyContract.kt | 4 +- 38 files changed, 980 insertions(+), 128 deletions(-) create mode 100644 docs/source/api-identity.rst create mode 100644 docs/source/example-code/src/main/kotlin/net/corda/docs/LaunchSpaceshipFlow.kt create mode 100644 docs/source/key-concepts-identity.rst create mode 100644 docs/source/resources/signatureMetadata.png create mode 100644 docs/source/resources/subTreesPrivacy.png create mode 100644 docs/source/upgrade-notes.rst diff --git a/.gitignore b/.gitignore index 67dc614169..ae3bd7f4de 100644 --- a/.gitignore +++ b/.gitignore @@ -88,6 +88,9 @@ docs/virtualenv/ # bft-smart **/config/currentView +# Node Explorer +/tools/explorer/conf/CordaExplorer.properties + # vim *.swp *.swn diff --git a/build.gradle b/build.gradle index 47fc8ef7e6..acee113b33 100644 --- a/build.gradle +++ b/build.gradle @@ -40,7 +40,7 @@ buildscript { ext.jopt_simple_version = '5.0.2' ext.jansi_version = '1.14' ext.hibernate_version = '5.2.6.Final' - ext.h2_version = '1.4.194' + ext.h2_version = '1.4.194' // Update docs if renamed or removed. ext.rxjava_version = '1.2.4' ext.dokka_version = '0.9.14' ext.eddsa_version = '0.2.0' 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 15cf636622..9ff83660aa 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 @@ -42,7 +42,10 @@ class NetworkIdentityModel { }.map { it?.party }.filterNotNull() val notaryNodes: ObservableList = notaries.map { rpcProxy.value?.nodeInfoFromParty(it) }.filterNotNull() - val parties: ObservableList = networkIdentities.filtered { it.legalIdentities.all { it !in notaries } } + val parties: ObservableList = networkIdentities + .filtered { it.legalIdentities.all { it !in notaries } } + // TODO: REMOVE THIS HACK WHEN NETWORK MAP REDESIGN WORK IS COMPLETED. + .filtered { it.legalIdentities.all { it.name.organisation != "Network Map Service" } } val myIdentity = rpcProxy.map { it?.nodeInfo()?.legalIdentitiesAndCerts?.first()?.party } fun partyFromPublicKey(publicKey: PublicKey): ObservableValue = identityCache[publicKey] diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt index 9bece5edcc..9a4da742b0 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt @@ -57,8 +57,8 @@ class NodeMonitorModel { */ fun register(nodeHostAndPort: NetworkHostAndPort, username: String, password: String) { val client = CordaRPCClient( - hostAndPort = nodeHostAndPort, - configuration = CordaRPCClientConfiguration.DEFAULT.copy( + nodeHostAndPort, + CordaRPCClientConfiguration.DEFAULT.copy( connectionMaxRetryInterval = 10.seconds ) ) diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt index 9145ae4baa..fd2c144d9e 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt @@ -55,6 +55,10 @@ abstract class FlowLogic { */ val serviceHub: ServiceHub get() = stateMachine.serviceHub + /** + * Creates a communication session with [party]. Subsequently you may send/receive using this session object. Note + * that this function does not communicate in itself, the counter-flow will be kicked off by the first send/receive. + */ @Suspendable fun initiateFlow(party: Party): FlowSession = stateMachine.initiateFlow(party, flowUsedForSessions) diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt b/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt index 7effaa9f52..07e4b49d33 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt @@ -6,8 +6,8 @@ import net.corda.core.utilities.UntrustworthyData /** * - * A [FlowSession] is a handle on a communication sequence between two flows. It is used to send and receive messages - * between flows. + * A [FlowSession] is a handle on a communication sequence between two paired flows, possibly running on separate nodes. + * It is used to send and receive messages between the flows as well as to query information about the counter-flow. * * There are two ways of obtaining such a session: * diff --git a/docs/source/_static/versions b/docs/source/_static/versions index 4110e2dfd6..1152a8e46a 100644 --- a/docs/source/_static/versions +++ b/docs/source/_static/versions @@ -8,5 +8,6 @@ "https://docs.corda.net/releases/release-M12.1": "M12.1", "https://docs.corda.net/releases/release-M13.0": "M13.0", "https://docs.corda.net/releases/release-M14.0": "M14.0", + "https://docs.corda.net/releases/release-V1.0": "V1.0", "https://docs.corda.net/head/": "Master" } diff --git a/docs/source/api-core-types.rst b/docs/source/api-core-types.rst index e69c5d858d..e7f3c898ee 100644 --- a/docs/source/api-core-types.rst +++ b/docs/source/api-core-types.rst @@ -20,20 +20,6 @@ Any object that needs to be identified by its hash should implement the ``NamedB ``SecureHash`` is a sealed class that only defines a single subclass, ``SecureHash.SHA256``. There are utility methods to create and parse ``SecureHash.SHA256`` objects. -Party ------ -Identities on the network are represented by ``AbstractParty``. There are two types of ``AbstractParty``: - -* ``Party``, identified by a ``PublicKey`` and a ``CordaX500Name`` - -* ``AnonymousParty``, identified by a ``PublicKey`` - -For example, in a transaction sent to your node as part of a chain of custody it is important you can convince yourself -of the transaction's validity, but equally important that you don't learn anything about who was involved in that -transaction. In these cases ``AnonymousParty`` should be used. In contrast, for internal processing where extended -details of a party are required, the ``Party`` class should be used. The identity service provides functionality for -resolving anonymous parties to full parties. - CompositeKey ------------ Corda supports scenarios where more than one signature is required to authorise a state object transition. For example: diff --git a/docs/source/api-flows.rst b/docs/source/api-flows.rst index 99cddf74b2..931aa493df 100644 --- a/docs/source/api-flows.rst +++ b/docs/source/api-flows.rst @@ -377,10 +377,11 @@ as it likes, and each party can invoke a different response flow: :end-before: DOCEND 6 :dedent: 12 -.. warning:: If you initiate several counter flows from the same ``@InitiatingFlow`` flow then on the receiving side you - must be prepared to be initiated by any of the corresponding ``initiateFlow()`` calls! A good way of handling this - ambiguity is to send as a first message a "role" message to the initiated flow, indicating which part of the - initiating flow the rest of the counter-flow should conform to. +.. warning:: If you initiate several flows from the same ``@InitiatingFlow`` flow then on the receiving side you must be + prepared to be initiated by any of the corresponding ``initiateFlow()`` calls! A good way of handling this ambiguity + is to send as a first message a "role" message to the initiated flow, indicating which part of the initiating flow + the rest of the counter-flow should conform to. For example send an enum, and on the other side start with a switch + statement. SendAndReceive ~~~~~~~~~~~~~~ @@ -426,20 +427,52 @@ Our side of the flow must mirror these calls. We could do this as follows: :end-before: DOCEND 8 :dedent: 12 -Porting from the old Party-based API -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Why sessions? +^^^^^^^^^^^^^ Before ``FlowSession`` s were introduced the send/receive API looked a bit different. They were functions on ``FlowLogic`` and took the address ``Party`` as argument. The platform internally maintained a mapping from ``Party`` to session, hiding sessions from the user completely. -However we realised that this could introduce subtle bugs and security issues where sends meant for different sessions -may end up in the same session if the target ``Party`` happens to be the same. +Although this is a convenient API it introduces subtle issues where a message that was originally meant for a specific +session may end up in another. -Therefore the session concept is now exposed through ``FlowSession`` which disambiguates which communication sequence a -message is intended for. +Consider the following contrived example using the old ``Party`` based API: -In the old API the first ``send`` or ``receive`` to a ``Party`` was the one kicking off the counterflow. This is now +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/LaunchSpaceshipFlow.kt + :language: kotlin + :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. + +However the above can go horribly wrong when the ``launcher`` happens to be the same party ``getSecretary`` returns. In +this case the boolean meant for the secretary will be received by the launcher! + +This indicates that ``Party`` is not a good identifier for the communication sequence, and indeed the ``Party`` based +API may introduce ways for an attacker to fish for information and even trigger unintended control flow like in the +above case. + +Hence we introduced ``FlowSession``, which identifies the communication sequence. With ``FlowSession`` s the above set +of flows would look like this: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/LaunchSpaceshipFlow.kt + :language: kotlin + :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 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In the old API the first ``send`` or ``receive`` to a ``Party`` was the one kicking off the counter-flow. This is now explicit in the ``initiateFlow`` function call. To port existing code: .. container:: codeset @@ -460,24 +493,28 @@ explicit in the ``initiateFlow`` function call. To port existing code: Subflows -------- -Subflows are pieces of reusable flows that may be run by calling ``FlowLogic.subFlow``. +Subflows are pieces of reusable flows that may be run by calling ``FlowLogic.subFlow``. There are two broad categories +of subflows, inlined and initiating ones. The main difference lies in the counter-flow's starting method, initiating +ones initiate counter-flows automatically, while inlined ones expect some parent counter-flow to run the inlined +counter-part. Inlined subflows ^^^^^^^^^^^^^^^^ -Inlined subflows inherit their calling flow's type when initiating a new session with a counterparty. For example say +Inlined subflows inherit their calling flow's type when initiating a new session with a counterparty. For example, say we have flow A calling an inlined subflow B, which in turn initiates a session with a party. The FlowLogic type used to -determine which counterflow should be kicked off will be A, not B. Note that this means that the other side of this -session must therefore be called explicitly in the kicked off flow as well. +determine which counter-flow should be kicked off will be A, not B. Note that this means that the other side of this +inlined flow must therefore be implemented explicitly in the kicked off flow as well. This may be done by calling a +matching inlined counter-flow, or by implementing the other side explicitly in the kicked off parent flow. An example of such a flow is ``CollectSignaturesFlow``. It has a counter-flow ``SignTransactionFlow`` that isn't -annotated with ``InitiatedBy``. This is because both of these flows are inlined, the kick-off relationship will be +annotated with ``InitiatedBy``. This is because both of these flows are inlined; the kick-off relationship will be defined by the parent flows calling ``CollectSignaturesFlow`` and ``SignTransactionFlow``. In the code inlined subflows appear as regular ``FlowLogic`` instances, `without` either of the ``@InitiatingFlow`` or ``@InitiatedBy`` annotation. -.. note:: Inlined flows aren't versioned, they inherit their parent flow's version. +.. note:: Inlined flows aren't versioned; they inherit their parent flow's version. Initiating subflows ^^^^^^^^^^^^^^^^^^^ @@ -493,7 +530,7 @@ Core initiating subflows ^^^^^^^^^^^^^^^^^^^^^^^^ Corda-provided initiating subflows are a little different to standard ones as they are versioned together with the -platform, and their initiated counterflows are registered explicitly, so there is no need for the ``InitiatedBy`` +platform, and their initiated counter-flows are registered explicitly, so there is no need for the ``InitiatedBy`` annotation. An example is the ``FinalityFlow``/``FinalityHandler`` flow pair. @@ -504,8 +541,10 @@ Built-in subflows Corda provides a number of built-in flows that should be used for handling common tasks. The most important are: * ``CollectSignaturesFlow`` (inlined), which should be used to collect a transaction's required signatures -* ``FinalityFlow`` (initiating), which should be used to notarise and record a transaction -* ``SendTransactionFlow`` (inlined), which should be used to send a signed transaction if it needed to be resolved on the other side. +* ``FinalityFlow`` (initiating), which should be used to notarise and record a transaction as well as to broadcast it to + all relevant parties +* ``SendTransactionFlow`` (inlined), which should be used to send a signed transaction if it needed to be resolved on + the other side. * ``ReceiveTransactionFlow`` (inlined), which should be used receive a signed transaction * ``ContractUpgradeFlow`` (initiating), which should be used to change a state's contract * ``NotaryChangeFlow`` (initiating), which should be used to change a state's notary @@ -641,6 +680,20 @@ We can also send and receive a ``StateAndRef`` dependency chain and automaticall :end-before: DOCEND 14 :dedent: 12 +Why inlined subflows? +^^^^^^^^^^^^^^^^^^^^^ + +Inlined subflows provide a way to share commonly used flow code `while forcing users to create a parent flow`. Take for +example ``CollectSignaturesFlow``. Say we made it an initiating flow that automatically kicks off +``SignTransactionFlow`` that signs the transaction. This would mean malicious nodes can just send any old transaction to +us using ``CollectSignaturesFlow`` and we would automatically sign it! + +By making this pair of flows inlined we provide control to the user over whether to sign the transaction or not by +forcing them to nest it in their own parent flows. + +In general if you're writing a subflow the decision of whether you should make it initiating should depend on whether +the counter-flow needs broader context to achieve its goal. + FlowException ------------- Suppose a node throws an exception while running a flow. Any counterparty flows waiting for a message from the node diff --git a/docs/source/api-identity.rst b/docs/source/api-identity.rst new file mode 100644 index 0000000000..270e31bafd --- /dev/null +++ b/docs/source/api-identity.rst @@ -0,0 +1,129 @@ +API: Identity +============= + +.. note:: Before reading this page, you should be familiar with the key concepts of :doc:`key-concepts-identity`. + +.. contents:: + +Party +----- +Identities on the network are represented by ``AbstractParty``. There are two types of ``AbstractParty``: + +* ``Party``, identified by a ``PublicKey`` and a ``CordaX500Name`` + +* ``AnonymousParty``, identified by a ``PublicKey`` + +For example, in a transaction sent to your node as part of a chain of custody it is important you can convince yourself +of the transaction's validity, but equally important that you don't learn anything about who was involved in that +transaction. In these cases ``AnonymousParty`` should be used by flows constructing when transaction states and commands. +In contrast, for internal processing where extended details of a party are required, the ``Party`` class should be used +instead. The identity service provides functionality for flows to resolve anonymous parties to full parties, dependent +on the anonymous party's identity having been registered with the node earlier (typically this is handled by +``SwapIdentitiesFlow`` or ``IdentitySyncFlow``, discussed below). + +Party names are held within the ``CordaX500Name`` data class, which enforces the structure of names within Corda, as +well as ensuring a consistent rendering of the names in plain text. + +The support for both Party and AnonymousParty classes in Corda enables sophisticated selective disclosure of identity +information. For example, it is possible to construct a Transaction using an AnonymousParty, so nobody can learn of your +involvement by inspection of the transaction, yet prove to specific counterparts that this AnonymousParty actually is +owned by your well known identity. This disclosure is achieved through the use of the PartyAndCertificate data class +which can be propagated to those who need to know, and contains the Party's X.509 certificate path to provide proof of +ownership by a well known identity. + +The PartyAndCertificate class is also used in the network map service to represent well known identities, in which +scenario the certificate path proves its issuance by the Doorman service. + + +Confidential Identities +----------------------- + +Confidential identities are key pairs where the corresponding X.509 certificate (and path) are not made public, so that parties who +are not involved in the transaction cannot identify its participants. They are owned by a well known identity, which +must sign the X.509 certificate. Before constructing a new transaction the involved parties must generate and send new +confidential identities to each other, a process which is managed using ``SwapIdentitiesFlow`` (discussed below). The +public keys of these confidential identities are then used when generating output states and commands for the transaction. + +Where using outputs from a previous transaction in a new transaction, counterparties may need to know who the involved +parties are. One example is in ``TwoPartyTradeFlow`` which delegates to ``CollectSignaturesFlow`` to gather certificates +from both parties. ``CollectSignaturesFlow`` requires that a confidential identity of the initiating node has signed +the transaction, and verifying this requires the receiving node has a copy of the confidential identity for the input +state. ``IdentitySyncFlow`` can be used to synchronize the confidential identities we have the certificate paths for, in +a single transaction, to another node. + +.. note:: ``CollectSignaturesFlow`` requires that the initiating node has signed the transaction, and as such all nodes + providing signatures must recognise the signing key used by the initiating node as being either its well known identity + or a confidential identity they have the certificate for. + +Swap identities flow +~~~~~~~~~~~~~~~~~~~~ + +``SwapIdentitiesFlow`` takes the party to swap identities with in its constructor (the counterparty), and is typically run as a subflow of +another flow. It returns a mapping from well known identities of the calling flow and our counterparty to the new +confidential identities; in future this will be extended to handle swapping identities with multiple parties. +You can see an example of it being used in ``TwoPartyDealFlow.kt``: + +.. container:: codeset + + .. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt + :language: kotlin + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + +The swap identities flow goes through the following key steps: + +1. Generate a nonce value to form a challenge to the other nodes. +2. Send nonce value to all counterparties, and receive their nonce values. +3. Generate a new confidential identity from our well known identity. +4. Create a data blob containing the new confidential identity (public key, name and X.509 certificate path), + and the hash of the nonce values. +5. Sign the resulting data blob with the confidential identity's private key. +6. Send the confidential identity and data blob signature to all counterparties, while receiving theirs. +7. Verify the signatures to ensure that identities were generated by the involved set of parties. +8. Verify the confidential identities are owned by the expected well known identities. +9. Store the confidential identities and return them to the calling flow. + +This ensures not only that the confidential identity X.509 certificates are signed by the correct well known identities, +but also that the confidential identity private key is held by the counterparty, and that a party cannot claim ownership +another party's confidential identities belong to its well known identity. + +Identity synchronization flow +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When constructing a transaction whose input states reference confidential identities, it is common for other signing +entities (counterparties) to require to know which well known identities those confidential identities map to. The +``IdentitySyncFlow`` handles this process, and you can see an example of its use in ``TwoPartyTradeFlow.kt``: + +.. container:: codeset + + .. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt + :language: kotlin + :start-after: DOCSTART 6 + :end-before: DOCEND 6 + +The identity synchronization flow goes through the following key steps: + +1. Extract participant identities from all input and output states and remove any well known identities. Required signers + on commands are currently ignored as they are presumed to be included in the participants on states, or to be well + known identities of services (such as an oracle service). +2. For each counterparty node, send a list of the public keys of the confidential identities, and receive back a list + of those the counterparty needs the certificate path for. +3. Verify the requested list of identities contains only confidential identities in the offered list, and abort otherwise. +4. Send the requested confidential identities as ``PartyAndCertificate`` instances to the counterparty. + +.. note:: ``IdentitySyncFlow`` works on a push basis. The initiating node can only send confidential identities it has + the X.509 certificates for, and the remote nodes can only request confidential identities being offered (are + referenced in the transaction passed to the initiating flow). There is no standard flow for nodes to collect + confidential identities before assembling a transaction, and this is left for individual flows to manage if required. + +``IdentitySyncFlow`` will serve all confidential identities in the provided transaction, irrespective of well known +identity. This is important for more complex transaction cases with 3+ parties, for example: + +* Alice is building the transaction, and provides some input state *x* owned by a confidential identity of Alice +* Bob provides some input state *y* owned by a confidential identity of Bob +* Charlie provides some input state *z* owned by a confidential identity of Charlie + +Alice may know all of the confidential identities ahead of time, but Bob not know about Charlie's and vice-versa. +The assembled transaction therefore has three input states *x*, *y* and *z*, for which only Alice possesses certificates +for all confidential identities. ``IdentitySyncFlow`` must send not just Alice's confidential identity but also any other +identities in the transaction to the Bob and Charlie. diff --git a/docs/source/api-index.rst b/docs/source/api-index.rst index d229c040ad..c3486e4cea 100644 --- a/docs/source/api-index.rst +++ b/docs/source/api-index.rst @@ -12,6 +12,7 @@ This section describes the APIs that are available for the development of CorDap api-vault-query api-transactions api-flows + api-identity api-service-hub api-rpc api-core-types diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index d02e2d8f62..0012e64902 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -19,6 +19,8 @@ UNRELEASED either annotated with the @CordaSerializable annotation or explicitly whitelisted then a NotSerializableException is thrown. +.. _changelog_v1: + Release 1.0 ----------- @@ -27,36 +29,45 @@ Release 1.0 * Java 8 lambdas now work property with Kryo during check-pointing. +* Java 8 serializable lambdas now work property with Kryo during check-pointing. + * String constants have been marked as ``const`` type in Kotlin, eliminating cases where functions of the form ``get()`` were created for the Java API. These can now be referenced by their name directly. * ``FlowLogic`` communication has been extensively rewritten to use functions on ``FlowSession`` as the base for communication between nodes. + * Calls to ``send()``, ``receive()`` and ``sendAndReceive()`` on FlowLogic should be replaced with calls to the function of the same name on ``FlowSession``. Note that the replacement functions do not take in a destination parameter, as this is defined in the session. * Initiated flows now take in a ``FlowSession`` instead of ``Party`` in their constructor. If you need to access the counterparty identity, it is in the ``counterparty`` property of the flow session. + * Added X509EdDSAEngine to intercept and rewrite EdDSA public keys wrapped in X509Key instances. This corrects an issue with verifying certificate paths loaded from a Java Keystore where they contain EdDSA keys. -* generateSpend() now creates a new confidential identity for the change address rather than using the identity of the - input state owner. +* Confidential identities are now complete: + + * The identity negotiation flow is now called ``SwapIdentitiesFlow``, renamed from ``TransactionKeyFlow``. + * generateSpend() now creates a new confidential identity for the change address rather than using the identity of the + input state owner. + * Please see the documentation :doc:`key-concepts-identity` and :doc:`api-identity` for more details. * Remove the legacy web front end from the SIMM demo. * ``NodeInfo`` and ``NetworkMapCache`` changes: + * Removed ``NodeInfo::legalIdentity`` in preparation for handling of multiple identities. We left list of ``NodeInfo::legalIdentitiesAndCerts``, - the first identity still plays a special role of main node identity. + the first identity still plays a special role of main node identity. * We no longer support advertising services in network map. Removed ``NodeInfo::advertisedServices``, ``serviceIdentities`` - and ``notaryIdentity``. + and ``notaryIdentity``. * Removed service methods from ``NetworkMapCache``: ``partyNodes``, ``networkMapNodes``, ``notaryNodes``, ``regulatorNodes``, - ``getNodesWithService``, ``getPeersWithService``, ``getRecommended``, ``getNodesByAdvertisedServiceIdentityKey``, ``getAnyNotary``, - ``notaryNode``, ``getAnyServiceOfType``. To get all known ``NodeInfo``s call ``allNodes``. + ``getNodesWithService``, ``getPeersWithService``, ``getRecommended``, ``getNodesByAdvertisedServiceIdentityKey``, ``getAnyNotary``, + ``notaryNode``, ``getAnyServiceOfType``. To get all known ``NodeInfo``'s call ``allNodes``. * In preparation for ``NetworkMapService`` redesign and distributing notaries through ``NetworkParameters`` we added - ``NetworkMapCache::notaryIdentities`` list to enable to lookup for notary parties known to the network. Related ``CordaRPCOps::notaryIdentities`` - was introduced. Other special nodes parties like Oracles or Regulators need to be specified directly in CorDapp or flow. + ``NetworkMapCache::notaryIdentities`` list to enable to lookup for notary parties known to the network. Related ``CordaRPCOps::notaryIdentities`` + was introduced. Other special nodes parties like Oracles or Regulators need to be specified directly in CorDapp or flow. * Moved ``ServiceType`` and ``ServiceInfo`` to ``net.corda.nodeapi`` package as services are only required on node startup. * Adding enum support to the class carpenter @@ -71,7 +82,7 @@ Release 1.0 * About half of the code in test-utils has been moved to a new module ``node-driver``, and the test scope modules are now located in a ``testing`` directory. -* CordaPluginRegistry has been renamed to SerializationWhitelist and moved to the net.corda.core.serialization +* ``CordaPluginRegistry`` has been renamed to ``SerializationWhitelist`` and moved to the ``net.corda.core.serialization`` package. The API for whitelisting types that can't be annotated was slightly simplified. This class used to contain many things, but as we switched to annotations and classpath scanning over time it hollowed out until this was the only functionality left. You also need to rename your services resource file to the new class name. @@ -93,10 +104,14 @@ Release 1.0 * Vault Query fix: filter by multiple issuer names in ``FungibleAssetQueryCriteria`` * Following deprecated methods have been removed: + * In ``DataFeed`` + * ``first`` and ``current``, replaced by ``snapshot`` * ``second`` and ``future``, replaced by ``updates`` + * In ``CordaRPCOps`` + * ``stateMachinesAndUpdates``, replaced by ``stateMachinesFeed`` * ``verifiedTransactions``, replaced by ``verifiedTransactionsFeed`` * ``stateMachineRecordedTransactionMapping``, replaced by ``stateMachineRecordedTransactionMappingFeed`` @@ -106,13 +121,12 @@ Release 1.0 ``ResolveTransactionsFlow`` has been made internal. Instead merge the receipt of the ``SignedTransaction`` and the subsequent sub-flow call to ``ResolveTransactionsFlow`` with a single call to ``ReceiveTransactionFlow``. The flow running on the counterparty must use ``SendTransactionFlow`` at the correct place. There is also ``ReceiveStateAndRefFlow`` and ``SendStateAndRefFlow`` for - dealing with ``StateAndRef``s. + dealing with ``StateAndRef``'s. * Vault query soft locking enhancements and deprecations + * removed original ``VaultService`` ``softLockedStates` query mechanism. - * introduced improved ``SoftLockingCondition`` filterable attribute in ``VaultQueryCriteria`` to enable specification - of different soft locking retrieval behaviours (exclusive of soft locked states, soft locked states only, specified - by set of lock ids) + * introduced improved ``SoftLockingCondition`` filterable attribute in ``VaultQueryCriteria`` to enable specification of different soft locking retrieval behaviours (exclusive of soft locked states, soft locked states only, specified by set of lock ids) * Trader demo now issues cash and commercial paper directly from the bank node, rather than the seller node self-issuing commercial paper but labelling it as if issued by the bank. @@ -122,7 +136,7 @@ Release 1.0 If you specifically need well known identities, use the network map, which is the authoritative source of current well known identities. -* Currency-related API in ``net.corda.core.contracts.ContractsDSL`` has moved to ```net.corda.finance.CurrencyUtils`. +* Currency-related API in ``net.corda.core.contracts.ContractsDSL`` has moved to ```net.corda.finance.CurrencyUtils``. * Remove `IssuerFlow` as it allowed nodes to request arbitrary amounts of cash to be issued from any remote node. Use `CashIssueFlow` instead. @@ -174,15 +188,16 @@ Release 1.0 * The unused ``MetaData`` and ``SignatureType`` in ``crypto`` package have been removed. -* The ``class TransactionSignature(bytes: ByteArray, val by: PublicKey, val signatureMetadata: SignatureMetadata): DigitalSignature(bytes)`` - class is now utilised Vs the old ``DigitalSignature.WithKey`` for Corda transaction signatures. Practically, it takes - the ``signatureMetadata`` as an extra input, in order to support signing both the transaction and the extra metadata. +* The ``class TransactionSignature(bytes: ByteArray, val by: PublicKey, val signatureMetadata:`` + ``SignatureMetadata): DigitalSignature(bytes)`` class is now utilised Vs the old ``DigitalSignature.WithKey`` for + Corda transaction signatures. Practically, it takes the ``signatureMetadata`` as an extra input, in order to support + signing both the transaction and the extra metadata. * To reflect changes in the signing process, the ``Crypto`` object is now equipped with the: ``fun doSign(keyPair: KeyPair, signableData: SignableData): TransactionSignature`` and ``fun doVerify(txId: SecureHash, transactionSignature: TransactionSignature): Boolean`` functions. -* ``SerializationCustomization.addToWhitelist()` now accepts multiple classes via varargs. +* ``SerializationCustomization.addToWhitelist()`` now accepts multiple classes via varargs. * Two functions to easily sign a ``FilteredTransaction`` have been added to ``ServiceHub``: ``createSignature(filteredTransaction: FilteredTransaction, publicKey: PublicKey)`` and @@ -212,8 +227,8 @@ Release 1.0 * All of the ``serializedHash`` and ``computeNonce`` functions have been removed from ``MerkleTransaction``. The ``serializedHash(x: T)`` and ``computeNonce`` were moved to ``CryptoUtils``. -* Two overloaded methods ``componentHash(opaqueBytes: OpaqueBytes, privacySalt: PrivacySalt, componentGroupIndex: Int, - internalIndex: Int): SecureHash`` and ``componentHash(nonce: SecureHash, opaqueBytes: OpaqueBytes): SecureHash`` have +* Two overloaded methods ``componentHash(opaqueBytes: OpaqueBytes, privacySalt: PrivacySalt,`` + ``componentGroupIndex: Int, internalIndex: Int): SecureHash`` and ``componentHash(nonce: SecureHash, opaqueBytes: OpaqueBytes): SecureHash`` have been added to ``CryptoUtils``. Similarly to ``computeNonce``, they internally use SHA256d for nonce and leaf hash computations. @@ -224,24 +239,25 @@ Release 1.0 ``FilteredTransaction`` now extend ``TraversableTransaction``. * Two classes, ``ComponentGroup(open val groupIndex: Int, open val components: List)`` and - ``FilteredComponentGroup(override val groupIndex: Int, override val components: List, - val nonces: List, val partialMerkleTree: PartialMerkleTree): ComponentGroup(groupIndex, components)`` - have been added, which are properties of the ``WireTransaction`` and ``FilteredTransaction``, respectively. + ``FilteredComponentGroup(override val groupIndex: Int, override val components:`` + ``List, val nonces: List, val partialMerkleTree:`` + ``PartialMerkleTree): ComponentGroup(groupIndex, components)`` have been added, which are properties + of the ``WireTransaction`` and ``FilteredTransaction``, respectively. * ``checkAllComponentsVisible(componentGroupEnum: ComponentGroupEnum)`` is added to ``FilteredTransaction``, a new function to check if all components are visible in a specific component-group. * To allow for backwards compatibility, ``WireTransaction`` and ``FilteredTransaction`` have new fields and constructors: ``WireTransaction(componentGroups: List, privacySalt: PrivacySalt = PrivacySalt())``, - ``FilteredTransaction private constructor(id: SecureHash,filteredComponentGroups: List, - groupHashes: List``. ``FilteredTransaction`` is still built via - ``buildFilteredTransaction(wtx: WireTransaction, filtering: Predicate). + ``FilteredTransaction private constructor(id: SecureHash,filteredComponentGroups:`` + ``List, groupHashes: List``. ``FilteredTransaction`` is still built via + ``buildFilteredTransaction(wtx: WireTransaction, filtering: Predicate)``. * ``FilteredLeaves`` class have been removed and as a result we can directly call the components from ``FilteredTransaction``, such as ``ftx.inputs`` Vs the old ``ftx.filteredLeaves.inputs``. * A new ``ComponentGroupEnum`` is added with the following enum items: ``INPUTS_GROUP``, ``OUTPUTS_GROUP``, - ``COMMANDS_GROUP``, ``ATTACHMENTS_GROUP``, ``NOTARY_GROUP``, ``TIMEWINDOW_GROUP``. + ``COMMANDS_GROUP``, ``ATTACHMENTS_GROUP``, ``NOTARY_GROUP``, ``TIMEWINDOW_GROUP``. * ``ContractUpgradeFlow.Initiator`` has been renamed to ``ContractUpgradeFlow.Initiate`` @@ -250,6 +266,8 @@ Release 1.0 * Current implementation of SSL in ``CordaRPCClient`` has been removed until we have a better solution which doesn't rely on the node's keystore. +.. _changelog_m14: + Milestone 14 ------------ @@ -343,6 +361,8 @@ Milestone 14 * Added JPA ``AbstractPartyConverter`` to ensure identity schema attributes are persisted securely according to type (well known party, resolvable anonymous party, completely anonymous party). +.. _changelog_m13: + Milestone 13 ------------ @@ -422,8 +442,10 @@ support for more currencies to the DemoBench and Explorer tools. * Upgraded BouncyCastle to v1.57. * Upgraded Requery to v1.3.1. -Milestone 12 ------------- +.. _changelog_m12: + +Milestone 12 (First Public Beta) +-------------------------------- * Quite a few changes have been made to the flow API which should make things simpler when writing CorDapps: diff --git a/docs/source/deploying-a-node.rst b/docs/source/deploying-a-node.rst index 0517d61c4d..562df5952f 100644 --- a/docs/source/deploying-a-node.rst +++ b/docs/source/deploying-a-node.rst @@ -61,6 +61,14 @@ one node as running the network map service, by putting their name in the ``netw .. warning:: When adding nodes, make sure that there are no port clashes! +If your CorDapp is written in Java, you should also add the following Gradle snippet so that you can pass named arguments to your flows via the Corda shell: + +.. sourcecode:: groovy + + tasks.withType(JavaCompile) { + options.compilerArgs << "-parameters" + } + Any CorDapps defined in the project's source folders are also automatically registered with all the nodes defined in ``deployNodes``, even if the CorDapps are not listed in each node's ``cordapps`` entry. @@ -86,4 +94,4 @@ run all the nodes at once. Each node in the ``nodes`` folder has the following s .. note:: Outside of development environments, do not store your node directories in the build folder. -If you make any changes to your ``deployNodes`` task, you will need to re-run the task to see the changes take effect. \ No newline at end of file +If you make any changes to your ``deployNodes`` task, you will need to re-run the task to see the changes take effect. 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/FlowCookbookJava.java index c9d708540d..1946a2501d 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/FlowCookbookJava.java @@ -267,8 +267,7 @@ public class FlowCookbookJava { // We then need to pair our output state with a contract. // DOCSTART 47 - String contractName = "net.corda.testing.contracts.DummyContract"; - StateAndContract ourOutput = new StateAndContract(ourOutputState, contractName); + StateAndContract ourOutput = new StateAndContract(ourOutputState, DummyContract.PROGRAM_ID); // DOCEND 47 // Commands pair a ``CommandData`` instance with a list of 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/FlowCookbook.kt index 8aa248f709..1fbee1e1be 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/FlowCookbook.kt @@ -255,8 +255,7 @@ class InitiatorFlow(val arg1: Boolean, val arg2: Int, private val counterparty: // We then need to pair our output state with a contract. // DOCSTART 47 - val contractName: String = "net.corda.testing.contracts.DummyContract" - val ourOutput: StateAndContract = StateAndContract(ourOutputState, contractName) + val ourOutput: StateAndContract = StateAndContract(ourOutputState, DummyContract.PROGRAM_ID) // DOCEND 47 // Commands pair a ``CommandData`` instance with a list of 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/LaunchSpaceshipFlow.kt new file mode 100644 index 0000000000..e6826fa213 --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/LaunchSpaceshipFlow.kt @@ -0,0 +1,99 @@ +package net.corda.docs + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.identity.Party +import net.corda.core.utilities.unwrap + +// DOCSTART LaunchSpaceshipFlow +@InitiatingFlow +class LaunchSpaceshipFlow : FlowLogic() { + @Suspendable + override fun call() { + val shouldLaunchSpaceship = receive(getPresident()).unwrap { it } + if (shouldLaunchSpaceship) { + launchSpaceship() + } + } + + fun launchSpaceship() { + } + + fun getPresident(): Party { + TODO() + } +} + +@InitiatedBy(LaunchSpaceshipFlow::class) +@InitiatingFlow +class PresidentSpaceshipFlow(val launcher: Party) : FlowLogic() { + @Suspendable + override fun call() { + val needCoffee = true + send(getSecretary(), needCoffee) + val shouldLaunchSpaceship = false + send(launcher, shouldLaunchSpaceship) + } + + fun getSecretary(): Party { + TODO() + } +} + +@InitiatedBy(PresidentSpaceshipFlow::class) +class SecretaryFlow(val president: Party) : FlowLogic() { + @Suspendable + override fun call() { + // ignore + } +} +// DOCEND LaunchSpaceshipFlow + +// DOCSTART LaunchSpaceshipFlowCorrect +@InitiatingFlow +class LaunchSpaceshipFlowCorrect : FlowLogic() { + @Suspendable + override fun call() { + val presidentSession = initiateFlow(getPresident()) + val shouldLaunchSpaceship = presidentSession.receive().unwrap { it } + if (shouldLaunchSpaceship) { + launchSpaceship() + } + } + + fun launchSpaceship() { + } + + fun getPresident(): Party { + TODO() + } +} + +@InitiatedBy(LaunchSpaceshipFlowCorrect::class) +@InitiatingFlow +class PresidentSpaceshipFlowCorrect(val launcherSession: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + val needCoffee = true + val secretarySession = initiateFlow(getSecretary()) + secretarySession.send(needCoffee) + val shouldLaunchSpaceship = false + launcherSession.send(shouldLaunchSpaceship) + } + + fun getSecretary(): Party { + TODO() + } +} + +@InitiatedBy(PresidentSpaceshipFlowCorrect::class) +class SecretaryFlowCorrect(val presidentSession: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + // ignore + } +} +// DOCEND LaunchSpaceshipFlowCorrect diff --git a/docs/source/index.rst b/docs/source/index.rst index fab9e1ec77..75f9733f4e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,9 +1,6 @@ Welcome to Corda ! ================== -.. warningX:: This build of the docs is from the "|version|" branch, not a milestone release. It may not reflect the - current state of the code. `Read the docs for milestone release M12.1 `_. - `Corda `_ is a blockchain-inspired open source distributed ledger platform. If you’d like a quick introduction to distributed ledgers and how Corda is different, then watch this short video: diff --git a/docs/source/key-concepts-contract-constraints.rst b/docs/source/key-concepts-contract-constraints.rst index 6c3e536264..39840924fd 100644 --- a/docs/source/key-concepts-contract-constraints.rst +++ b/docs/source/key-concepts-contract-constraints.rst @@ -15,7 +15,7 @@ A typical constraint is the hash of the CorDapp JAR that contains the contract a include constraints that require specific signers of the JAR, or both the signer and the hash. Constraints can be specified when constructing a transaction; if unspecified, an automatic constraint is used. -``TransactionState``s have a ``constraint`` field that represents that state's attachment constraint. When a party +A ``TransactionState`` has a ``constraint`` field that represents that state's attachment constraint. When a party constructs a ``TransactionState`` without specifying the constraint parameter a default value (``AutomaticHashConstraint``) is used. This default will be automatically resolved to a specific ``HashAttachmentConstraint`` that contains the hash of the attachment which contains the contract of that @@ -80,7 +80,7 @@ attachment JAR. This allows for trusting of attachments from trusted entities. Limitations ----------- -``AttachmentConstraint``s are verified by running the ``AttachmentConstraint.isSatisfiedBy`` method. When this is called +An ``AttachmentConstraint`` is verified by running the ``AttachmentConstraint.isSatisfiedBy`` method. When this is called it is provided only the relevant attachment by the transaction that is verifying it. Testing diff --git a/docs/source/key-concepts-identity.rst b/docs/source/key-concepts-identity.rst new file mode 100644 index 0000000000..7f1f9bbcc2 --- /dev/null +++ b/docs/source/key-concepts-identity.rst @@ -0,0 +1,78 @@ +Identity +======== + +.. topic:: Summary + + * *Identities in Corda can represent legal identities or service identities* + * *Identities are attested to by X.509 certificate signed by the Doorman or a well known identity* + * *Well known identities are published in the network map* + * *Confidential identities are only shared on a need to know basis* + +Identities in Corda can represent: + +* Legal identity of an organisation +* Service identity of a network service + +Legal identities are used for parties in a transaction, such as the owner of a cash state. Service identities are used +for those providing transaction-related services, such as notary, or oracle. Service identities are distinct to legal +identities so that distributed services can exist on nodes owned by different organisations. Such distributed service +identities are based on ``CompositeKeys``, which describe the valid sets of signers for a signature from the service. +See :doc:`api-core-types` for more technical detail on composite keys. + +Identities are either well known or confidential, depending on whether their X.509 certificate (and corresponding +certificate path to a trusted root certificate) is published: + +* Well known identities are the generally identifiable public key of a legal entity or service, which makes them + ill-suited to transactions where confidentiality of participants is required. This certificate is published in the + network map service for anyone to access. +* Confidential identities are only published to those who are involved in transactions with the identity. The public + key may be exposed to third parties (for example to the notary service), but distribution of the name and X.509 + certificate is limited. + +Although there are several elements to the Corda transaction privacy model, including ensuring that transactions are +only shared with those who need to see them, and planned use of Intel SGX, it is important to provide defense in depth against +privacy breaches. Confidential identities are used to ensure that even if a third party gets access to an unencrypted +transaction, they cannot identify the participants without additional information. + +Name +---- + +Identity names are X.500 distinguished names with Corda-specific constraints applied. In order to be compatible with +other implementations (particularly TLS implementations), we constrain the allowed X.500 attribute types to a subset of +the minimum supported set for X.509 certificates (specified in RFC 3280), plus the locality attribute: + +* organization (O) +* state (ST) +* locality (L) +* country (C) +* organizational-unit (OU) +* common name (CN) - used only for service identities + +The organisation, locality and country attributes are required, while state, organisational-unit and common name are +optional. Attributes cannot be be present more than once in the name. The "country" code is strictly restricted to valid +ISO 3166-1 two letter codes. + +Certificates +------------ + +Nodes must be able to verify the identity of the owner of a public key, which is achieved using X.509 certificates. +When first run a node generates a key pair and submits a certificate signing request to the network Doorman service +(see :doc:`permissioning`). +The Doorman service applies appropriate identity checks then issues a certificate to the node, which is used as the +node certificate authority (CA). From this initial CA certificate the node automatically creates and signs two further +certificates, a TLS certificate and a signing certificate for the node's well known identity. Finally the node +builds a node info record containing its address and well known identity, and registers it with the network map service. + +From the signing certificate the organisation can create both well known and confidential identities. Use-cases for +well known identities include clusters of nodes representing a single identity for redundancy purposes, or creating +identities for organisational units. + +It is up to organisations to decide which identities they wish to publish in the network map service, making them +well known, and which they wish to keep as confidential identities for privacy reasons (typically to avoid exposing +business sensitive details of transactions). In some cases nodes may also use private network map services in addition +to the main network map service, for operational reasons. Identities registered with such network maps must be +considered well known, and it is never appropriate to store confidential identities in a central directory without +controls applied at the record level to ensure only those who require access to an identity can retrieve its +certificate. + +.. TODO: Revisit once design & use cases of private maps is further fleshed out \ No newline at end of file diff --git a/docs/source/key-concepts.rst b/docs/source/key-concepts.rst index e83d04d093..a252e1a853 100644 --- a/docs/source/key-concepts.rst +++ b/docs/source/key-concepts.rst @@ -13,6 +13,7 @@ This section should be read in order: key-concepts-ecosystem key-concepts-ledger + key-concepts-identity key-concepts-states key-concepts-contracts key-concepts-contract-constraints diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 4da3939152..845933db05 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -5,17 +5,112 @@ Here are release notes for each snapshot release from M9 onwards. Unreleased ---------- + Release 1.0 ----------- +Corda 1.0 is finally here! -* Flow communications API has been redesigned around session based communication. +This critical step in the Corda journey enables the developer community, clients, and partners to build on Corda with confidence. +Corda 1.0 is the first released version to provide API stability for Corda application (CorDapp) developers. +Corda applications will continue to work against this API with each subsequent release of Corda. The public API for Corda +will only evolve to include new features. -* Merged handling of well known and confidential identities in the identity service. +As of Corda 1.0, the following modules export public APIs for which we guarantee to maintain backwards compatibility, +unless an incompatible change is required for security reasons: -* Remove `IssuerFlow` as it allowed nodes to request arbitrary amounts of cash to be issued from any remote node. + * **core**: + Contains the bulk of the APIs to be used for building CorDapps: contracts, transactions, flows, identity, node services, + cryptographic libraries, and general utility functions. -* Remove the legacy web front end from the SIMM demo. This was a very early sample, and does not reflect the quality of - current Corda code. It may be replaced with a new front end based on a more recent version of AngularJS in a later release. + * **client-rpc**: + An RPC client interface to Corda, for use by both UI facing clients and integration with external systems. + + * **client-jackson**: + Utilities and serialisers for working with JSON representations of basic types. + +Our extensive testing frameworks will continue to evolve alongside future Corda APIs. As part of our commitment to ease of use and modularity +we have introduced a new test node driver module to encapsulate all test functionality in support of building standalone node integration +tests using our DSL driver. + +Please read :doc:`api-index` for complete details. + +.. note:: it may be necessary to recompile applications against future versions of the API until we begin offering + `ABI (Application Binary Interface) `_ stability as well. + We plan to do this soon after this release of Corda. + +Significant changes implemented in reaching Corda API stability include: + +* **Flow framework**: + The Flow framework communications API has been redesigned around session based communication with the introduction of a new + ``FlowSession`` to encapsulate the counterparty information associated with a flow. + All shipped Corda flows have been upgraded to use the new `FlowSession`. Please read :doc:`api-flows` for complete details. + +* **Complete API cleanup**: + Across the board, all our public interfaces have been thoroughly revised and updated to ensure a productive and intuitive developer experience. + Methods and flow naming conventions have been aligned with their semantic use to ease the understanding of CorDapps. + In addition, we provide ever more powerful re-usable flows (such as `CollectSignaturesFlow`) to minimize the boiler-plate code developers need to write. + +* **Simplified annotation driven scanning**: + CorDapp configuration has been made simpler through the removal of explicit configuration items in favour of annotations + and classpath scanning. As an example, we have now completely removed the `CordaPluginRegistry` configuration. + Contract definitions are no longer required to explicitly define a legal contract reference hash. In their place an + optional `LegalProseReference` annotation to specify a URI is used. + +* **Java usability**: + All code has been updated to enable simple access to static API parameters. Developers no longer need to + call getter methods, and can reference static API variables directly. + +In addition to API stability this release encompasses a number of major functional improvements, including: + +* **Contract constraints**: + Provides a means with which to enforce a specific implementation of a State's verify method during transaction verification. + When loading an attachment via the attachment classloader, constraints of a transaction state are checked against the + list of attachment hashes provided, and the attachment is rejected if the constraints are not matched. + +* **Signature Metadata support**: + Signers now have the ability to add metadata to their digital signatures. Whereas previously a user could only sign the Merkle root of a + transaction, it is now possible for extra information to be attached to a signature, such as a platform version + and the signature-scheme used. + + .. image:: resources/signatureMetadata.png + +* **Backwards compatibility and improvements to core transaction data structures**: + A new Merkle tree model has been introduced that utilises sub-Merkle trees per component type. Components of the + same type, such as inputs or commands, are grouped together and form their own Merkle tree. Then, the roots of + each group are used as leaves in the top-level Merkle tree. This model enables backwards compatibility, in the + sense that if new component types are added in the future, old clients will still be able to compute the Merkle root + and relay transactions even if they cannot read (deserialise) the new component types. Due to the above, + `FilterTransaction` has been made simpler with a structure closer to `WireTransaction`. This has the effect of making the API + more user friendly and intuitive for both filtered and unfiltered transactions. + +* **Enhanced component privacy**: + Corda 1.0 is equipped with a scalable component visibility design based on the above sophisticated + sub-tree model and the introduction of nonces per component. Roughly, an initial base-nonce, the "privacy-salt", + is used to deterministically generate nonces based on the path of each component in the tree. Because each component + is accompanied by a nonce, we protect against brute force attacks, even against low-entropy components. In addition, + a new privacy feature is provided that allows non-validating notaries to ensure they see all inputs and if there was a + `TimeWindow` in the original transaction. Due to the above, a malicious user cannot selectively hide one or more + input states from the notary that would enable her to bypass the double-spending check. The aforementioned + functionality could also be applied to Oracles so as to ensure all of the commands are visible to them. + + .. image:: resources/subTreesPrivacy.png + +* **Full support for confidential identities**: + This includes rework and improvements to the identity service to handle both `well known` and `confidential` identities. + This work ships in an experimental module in Corda 1.0, called `confidential-identities`. API stabilisation of confidential + identities will occur as we make the integration of this privacy feature into applications even easier for developers. + +* **Re-designed network map service**: + The foundations for a completely redesigned network map service have been implemented to enable future increased network + scalability and redundancy, support for multiple notaries, and administration of network compatibility zones and business networks. + +Finally, please note that the 1.0 release has not yet been security audited. + +We have provided a comprehensive :doc:`upgrade-notes` to ease the transition of migrating CorDapps to Corda 1.0 + +Upgrading to this release is strongly recommended, and you will be safe in the knowledge that core APIs will no longer break. + +Thank you to all contributors for this release! Milestone 14 ------------ diff --git a/docs/source/release-process-index.rst b/docs/source/release-process-index.rst index f1c9ea2037..eb81e833f0 100644 --- a/docs/source/release-process-index.rst +++ b/docs/source/release-process-index.rst @@ -6,4 +6,6 @@ Release process release-notes changelog + upgrade-notes + codestyle testing \ No newline at end of file diff --git a/docs/source/resources/signatureMetadata.png b/docs/source/resources/signatureMetadata.png new file mode 100644 index 0000000000000000000000000000000000000000..61f0d11e17a25f376ebbefcb534630d92a334ac5 GIT binary patch literal 18666 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 literal 0 HcmV?d00001 diff --git a/docs/source/resources/subTreesPrivacy.png b/docs/source/resources/subTreesPrivacy.png new file mode 100644 index 0000000000000000000000000000000000000000..3a7dae6110f8d735ee0648caf33e94b0afd919c4 GIT binary patch literal 105835 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%0DT|or6oc1(R*d@m`)QEC>>Cfs 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 Aj{pDw literal 0 HcmV?d00001 diff --git a/docs/source/running-the-demos.rst b/docs/source/running-the-demos.rst index 32a99967c6..f9e78737d1 100644 --- a/docs/source/running-the-demos.rst +++ b/docs/source/running-the-demos.rst @@ -143,19 +143,18 @@ To run from the command line in Windows: To run the BFT SMaRt notary demo, use ``nodesBFT`` instead of ``nodesRaft`` in the path (you will see messages from notary nodes trying to communicate each other sometime with connection errors, that's normal). For a single notary node, use ``nodesSingle``. -Notary nodes store consumed states in a replicated commit log, which is backed by a H2 database on each node. +Distributed notary nodes store consumed states in a replicated commit log, which is backed by a H2 database on each node. You can ascertain that the commit log is synchronised across the cluster by accessing and comparing each of the nodes' backing stores by using the H2 web console: - Firstly, download `H2 web console `_ (download the "platform-independent zip"), - and start it using a script in the extracted folder: ``h2/bin/h2.sh`` (or ``h2\bin\h2`` for Windows) + and start it using a script in the extracted folder: ``sh h2/bin/h2.sh`` (or ``h2\bin\h2`` for Windows) - If you are uncertain as to which version of h2 to install or if you have connectivity issues, refer to ``build.gradle`` - located in the ``node`` directory and locate the compile step for ``com.h2database``. Use a client of the same - major version - even if still in beta. + located in the corda directory and locate ``h2_version``. Use a client of the same major version - even if still in beta. - The H2 web console should start up in a web browser tab. To connect we first need to obtain a JDBC connection string. - Each node outputs its connection string in the terminal window as it starts up. In a terminal window where a node is running, + Each node outputs its connection string in the terminal window as it starts up. In a terminal window where a **notary** node is running, look for the following string: ``Database connection url is : jdbc:h2:tcp://10.18.0.150:56736/node`` @@ -163,8 +162,8 @@ by using the H2 web console: You can use the string on the right to connect to the h2 database: just paste it into the `JDBC URL` field and click *Connect*. You will be presented with a web application that enumerates all the available tables and provides an interface for you to query them using SQL -- The committed states are stored in the ``NOTARY_COMMITTED_STATES`` table. Note that the raw data is not human-readable, - but we're only interested in the row count for this demo +- The committed states are stored in the ``NOTARY_COMMITTED_STATES`` table (for Raft) or ``NODE_BFT_SMART_NOTARY_COMMITTED_STATES`` (for BFT). + Note that in the Raft case the raw data is not human-readable, but we're only interested in the row count for this demo .. _bank-of-corda-demo: diff --git a/docs/source/serialization.rst b/docs/source/serialization.rst index ca86da886c..92d542dd71 100644 --- a/docs/source/serialization.rst +++ b/docs/source/serialization.rst @@ -22,8 +22,8 @@ Classes get onto the whitelist via one of three 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 returning the class as part of a plugin via the method ``customizeSerialization``. It's important to return - true from this method if you override it, otherwise the plugin will be excluded. See :doc:`writing-cordapps`. +#. By implementing the ``SerializationWhitelist`` interface and specifying a list of `whitelist` classes. + See :doc:`writing-cordapps`. #. 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. @@ -39,8 +39,8 @@ It's reproduced here as an example of both ways you can do this for a couple of them will automatically be whitelisted. This includes `Contract`, `ContractState` and `CommandData`. .. warning:: Java 8 Lambda expressions are not serializable except in flow checkpoints, and then not by default. The syntax to declare a serializable Lambda -expression that will work with Corda is ``Runnable r = (Runnable & Serializable) () -> System.out.println("Hello World");``, or -``Callable c = (Callable & Serializable) () -> "Hello World";``. + expression that will work with Corda is ``Runnable r = (Runnable & Serializable) () -> System.out.println("Hello World");``, or + ``Callable c = (Callable & Serializable) () -> "Hello World";``. .. warning:: We will be replacing the use of Kryo in the serialization framework and so additional changes here are likely. diff --git a/docs/source/shell.rst b/docs/source/shell.rst index 5d00d73890..cdddc4548e 100644 --- a/docs/source/shell.rst +++ b/docs/source/shell.rst @@ -99,6 +99,10 @@ Nested objects can be created using curly braces, as in ``{ a: 1, b: 2}``. This parser is defined for the type you need, for instance, if an API requires a ``Pair`` which could be represented as ``{ first: foo, second: 123 }``. +.. note:: If your CorDapp is written in Java, + named arguments won't work unless you compiled using the ``-parameters`` argument to javac. + See :doc:`deploying-a-node` for how to specify it via Gradle. + The same syntax is also used to specify the parameters for RPCs, accessed via the ``run`` command, like this: ``run registeredFlows`` diff --git a/docs/source/troubleshooting.rst b/docs/source/troubleshooting.rst index a07980119c..51e60133c4 100644 --- a/docs/source/troubleshooting.rst +++ b/docs/source/troubleshooting.rst @@ -23,6 +23,11 @@ Some of the unit tests, and our serialization framework in general, rely on the to Java reflection. Make sure you have specified the ``-parameters`` option to the Java compiler. We attempt to set this globally for gradle and IntelliJ, but it's possible this option is not present in your environment for some reason. +"No matching constructor found: - [arg0: int, arg1: Party]: missing parameter arg0" +*********************************************************************************** + +Your CorDapp is written in Java and you haven't specified the ``-parameters`` compiler argument. See :doc:`deploying-a-node` for how it can be done using Gradle. + IDEA issues ----------- diff --git a/docs/source/tut-two-party-flow.rst b/docs/source/tut-two-party-flow.rst index 84acdb6f7c..b19630eb32 100644 --- a/docs/source/tut-two-party-flow.rst +++ b/docs/source/tut-two-party-flow.rst @@ -125,10 +125,10 @@ In a new ``IOUFlowResponder.java`` file in Java, or within the ``App.kt`` file i ... @InitiatedBy(IOUFlow::class) - class IOUFlowResponder(val otherPartyFlow: FlowSession) : FlowLogic() { + class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic() { @Suspendable override fun call() { - val signTransactionFlow = object : SignTransactionFlow(otherPartyFlow, SignTransactionFlow.tracker()) { + val signTransactionFlow = object : SignTransactionFlow(otherPartySession, SignTransactionFlow.tracker()) { override fun checkTransaction(stx: SignedTransaction) = requireThat { val output = stx.tx.outputs.single().data "This must be an IOU transaction." using (output is IOUState) @@ -148,7 +148,11 @@ In a new ``IOUFlowResponder.java`` file in Java, or within the ``App.kt`` file i import co.paralleluniverse.fibers.Suspendable; import com.template.state.IOUState; import net.corda.core.contracts.ContractState; - import net.corda.core.flows.*; + import net.corda.core.flows.FlowException; + 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.core.transactions.SignedTransaction; import net.corda.core.utilities.ProgressTracker; @@ -156,18 +160,18 @@ In a new ``IOUFlowResponder.java`` file in Java, or within the ``App.kt`` file i @InitiatedBy(IOUFlow.class) public class IOUFlowResponder extends FlowLogic { - private final FlowSession otherPartyFlow; + private final FlowSession otherPartySession; - public IOUFlowResponder(FlowSession otherPartyFlow) { - this.otherPartyFlow = otherPartyFlow; + public IOUFlowResponder(FlowSession otherPartySession) { + this.otherPartySession = otherPartySession; } @Suspendable @Override public Void call() throws FlowException { - class signTxFlow extends SignTransactionFlow { - private signTxFlow(FlowSession otherPartyFlow, ProgressTracker progressTracker) { - super(otherPartyFlow, progressTracker); + class SignTxFlow extends SignTransactionFlow { + private signTxFlow(FlowSession otherPartySession, ProgressTracker progressTracker) { + super(otherPartySession, progressTracker); } @Override @@ -182,7 +186,7 @@ In a new ``IOUFlowResponder.java`` file in Java, or within the ``App.kt`` file i } } - subFlow(new signTxFlow(otherPartyFlow, SignTransactionFlow.Companion.tracker())); + subFlow(new SignTxFlow(otherPartySession, SignTransactionFlow.Companion.tracker())); return null; } diff --git a/docs/source/tutorial-tear-offs.rst b/docs/source/tutorial-tear-offs.rst index 80d8395112..fe665bf3bf 100644 --- a/docs/source/tutorial-tear-offs.rst +++ b/docs/source/tutorial-tear-offs.rst @@ -49,11 +49,10 @@ The following code snippet is taken from ``NodeInterestRates.kt`` and implements :end-before: DOCEND 1 :dedent: 8 -.. note:: The way the ``FilteredTransaction`` is constructed ensures that after signing of the root hash it's impossible - to add or remove components (leaves). However, it can happen that having transaction with multiple commands one party - reveals only subset of them to the Oracle. As signing is done now over the Merkle root hash, the service signs all - commands of given type, even though it didn't see all of them. In the case however where all of the commands should be - visible to an Oracle, one can type ``ftx.checkAllComponentsVisible(COMMANDS_GROUP)`` before invoking ``ftx.verify``. +.. note:: The way the ``FilteredTransaction`` is constructed ensures that after signing of the root hash it's impossible to add or remove + components (leaves). However, it can happen that having transaction with multiple commands one party reveals only subset of them to the Oracle. + As signing is done now over the Merkle root hash, the service signs all commands of given type, even though it didn't see + all of them. In the case however where all of the commands should be visible to an Oracle, one can type ``ftx.checkAllComponentsVisible(COMMANDS_GROUP)`` before invoking ``ftx.verify``. ``checkAllComponentsVisible`` is using a sophisticated underlying partial Merkle tree check to guarantee that all of the components of a particular group that existed in the original ``WireTransaction`` are included in the received ``FilteredTransaction``. \ No newline at end of file diff --git a/docs/source/upgrade-notes.rst b/docs/source/upgrade-notes.rst new file mode 100644 index 0000000000..3f6444d497 --- /dev/null +++ b/docs/source/upgrade-notes.rst @@ -0,0 +1,349 @@ +Upgrade notes +============= + +These notes provide helpful instructions to upgrade your Corda Applications (CorDapps) from previous versions, starting +from our first public Beta (:ref:`Milestone 12 `), to :ref:`V1.0 ` + +General +------- +Always remember to update the version identifiers in your project gradle file: + +.. sourcecode:: shell + + ext.corda_release_version = '1.0.0' + ext.corda_gradle_plugins_version = '1.0.0' + +It may be necessary to update the version of major dependencies: + +.. sourcecode:: shell + + ext.kotlin_version = '1.1.4' + ext.quasar_version = '0.7.9' + +Please consult the relevant release notes of the release in question. If not specified, you may assume the +versions you are currently using are still in force. + +We also strongly recommend cross referencing with the :doc:`changelog` to confirm changes. + +:ref:`Milestone 14 ` +------------ + +Build +^^^^^ + +* MockNetwork has moved. + + A new test driver module dependency needs to be including in your project: `corda-node-driver`. To continue using the + mock network for testing, add the following entry to your gradle build file: + +.. sourcecode:: shell + + testCompile "net.corda:corda-node-driver:$corda_release_version" + +.. note:: you may only need `testCompile "net.corda:corda-test-utils:$corda_release_version"` if not using the Driver DSL. + +Configuration +^^^^^^^^^^^^^ + +* ``CordaPluginRegistry`` has been removed. + The one remaining configuration item ``customizeSerialisation``, which defined a optional whitelist of types for use in + object serialization, has been replaced with the ``SerializationWhitelist`` interface which should be implemented to + define a list of equivalent whitelisted classes. + You will need to rename your services resource file to the new class name: + 'resources/META-INF/services/net.corda.core.node.CordaPluginRegistry' becomes 'resources/META-INF/services/net.corda.core.serialization.SerializationWhitelist' + An associated property on ``MockNode`` was renamed from ``testPluginRegistries`` to ``testSerializationWhitelists``. + In general, the ``@CordaSerializable`` annotation is the preferred method for whitelisting as described in :doc:`serialization` + +Missing imports +^^^^^^^^^^^^^^^ + +Use the automatic imports feature of IntelliJ to intelligently resolve the new imports. + +* Missing imports for contract types. + + CommercialPaper and Cash are now contained within the `finance` module, as are associated helpers functions. + For example: + ``import net.corda.contracts.ICommercialPaperState`` becomes ``import net.corda.finance.contracts.ICommercialPaperState`` + + ``import net.corda.contracts.asset.sumCashBy`` becomes ``import net.corda.finance.utils.sumCashBy`` + + ``import net.corda.core.contracts.DOLLARS`` becomes ``import net.corda.finance.DOLLARS`` + + ``import net.corda.core.contracts.issued by`` becomes ``import net.corda.finance.issued by`` + + ``import net.corda.contracts.asset.Cash`` becomes ``import net.corda.finance.contracts.asset.Cash`` + +* Missing imports for utility functions. + + Many common types and helper methods have been consolidated into `net.corda.core.utilities` package. + For example: + ``import net.corda.core.crypto.commonName`` becomes ``import net.corda.core.utilities.commonName`` + + ``import net.corda.core.crypto.toBase58String`` becomes ``import net.corda.core.utilities.toBase58String`` + + ``import net.corda.core.getOrThrow`` becomes ``import net.corda.core.utilities.getOrThrow`` + +* Missing flow imports. + + In general all reusable library flows are contained within the **core** API `net.corda.core.flows` package. + Financial domain library flows are contained within the **finance** module `net.corda.finance.flows` package. + Other flows that have moved include: + + ``import net.corda.core.flows.ResolveTransactionsFlow`` becomes ``import net.corda.core.internal.ResolveTransactionsFlow`` + +Core data structures +^^^^^^^^^^^^^^^^^^^^ + +* Missing Contract override. + + The contract interace attribute ``legalContractReference`` has been removed, and replaced by + the optional annotation ``@LegalProseReference(uri = "")`` + +* Unresolved reference. + + Calls to ``AuthenticatedObject`` are replaced by ``CommandWithParties`` + +* Overrides nothing: ``isRelevant`` in ``LinearState``. + + Removed the concept of relevancy from ``LinearState``. A ``ContractState``'s relevance to the vault is now resolved + internally; the vault will process any transaction from a flow which is not derived from transaction resolution verification. + The notion of relevancy is subject to further improvements to enable a developer to control what state the vault thinks + are relevant. + +* Calls to ``txBuilder.toLedgerTransaction()`` now requires a serviceHub parameter. + + Used by the new Contract Constraints functionality to validate and resolve attachments. + +Flow framework +^^^^^^^^^^^^^^ + +* Flow session deprecations + + ``FlowLogic`` communication has been upgraded to use functions on ``FlowSession`` as the base for communication + between nodes. + + * Calls to ``send()``, ``receive()`` and ``sendAndReceive()`` on FlowLogic should be replaced with calls + to the function of the same name on ``FlowSession``. Note that the replacement functions do not take in a destination + parameter, as this is defined in the session. + + * Initiated flows now take in a ``FlowSession`` instead of ``Party`` in their constructor. If you need to access the + counterparty identity, it is in the ``counterparty`` property of the flow session. + + See ``FlowSession`` for step by step instructions on porting existing flows to use the new mechanism. + +* ``FinalityFlow`` now returns a single ``SignedTransaction``, instead of a ``List`` + +* ``TransactionKeyFlow`` renamed to ``SwapIdentitiesFlow`` + + Note that ``SwapIdentitiesFlow`` must be imported from the *confidential-identities** package ''net.corda.confidential'' + +Node services (ServiceHub) +^^^^^^^^^^^^^^ + +* VaultQueryService: unresolved reference to `vaultQueryService`. + + Replace all references to ``.vaultQueryService`` with ``.vaultService``. + Previously there were two vault APIs. Now there is a single unified API with the same functions: ``VaultService``. + +* ``serviceHub.myInfo.legalIdentity`` no longer exists; use the ``ourIdentity`` property of the flow instead. + + ``FlowLogic.ourIdentity`` has been introduced as a shortcut for retrieving our identity in a flow + +* ``getAnyNotary`` is gone - use ``serviceHub.networkMapCache.notaryIdentities[0]`` instead + + Note: ongoing work to support multiple notary identities is still in progress. + +* ``ServiceHub.networkMapUpdates`` is replaced by ``ServiceHub.networkMapFeed`` + +* ``ServiceHub.partyFromX500Name`` is replaced by ``ServiceHub.wellKnownPartyFromX500Name`` + Note: A "well known" party is one that isn't anonymous and this change was motivated by the confidential identities work. + +RPC Client +^^^^^^^^^^ + +* Missing API methods on `CordaRPCOps` interface. + + * Calls to ``verifiedTransactionsFeed()`` and ``verifiedTransactions()`` have been replaced with: + ``internalVerifiedTransactionsSnapshot()`` and ``internalVerifiedTransactionsFeed()`` respectively + + This is in preparation for the planned integration of Intel SGX™, which will encrypt the transactions feed. + Apps that use this API will not work on encrypted ledgers: you should probably be using the vault query API instead. + + * Accessing the `networkMapCache` via ``services.nodeInfo().legalIdentities`` returns a list of identities. + The first element in the list is the Party object referring to a node's single identity. + + This is in preparation for allowing a node to host multiple separate identities in future. + +Testing +^^^^^^^ + +Please note that `Clauses` have been removed completely as of V1.0. +We will be revisiting this capability in a future release. + +* CorDapps must be explicitly registered in ``MockNetwork`` unit tests. + + This is done by calling ``setCordappPackages``, an extension helper function in the ``net.corda.testing`` package, + on the first line of your `@Before` method. This takes a variable number of `String` arguments which should be the + package names of the CorDapps containing the contract verification code you wish to load. + You should unset CorDapp packages in your `@After` method by using ``unsetCordappPackages()`` after `stopNodes()`. + +* CorDapps must be explicitly registered in ``DriverDSL`` and ``RPCDriverDSL`` integration tests. + + Similarly, you must also register package names of the CorDapps containing the contract verification code you wish to load + using the ``extraCordappPackagesToScan: List`` constructor parameter of the driver DSL. + +Finance +^^^^^^^ + +* `FungibleAsset` interface simplification. + + The ``FungibleAsset`` interface has been made simpler. The ``Commands`` grouping interface + that included the ``Move``, ``Issue`` and ``Exit`` interfaces have all been removed, while the ``move`` function has + been renamed to ``withNewOwnerAndAmount`` to be consistent with the ``withNewOwner`` function of the ``OwnableState``. + + The following errors may be reported: + + * override nothing (FungibleAsset): `move` + * not a subtype of overridden FungibleAsset: `withNewOwner` + * no longer need to override `override val contractHash: SecureHash? = null` + * need to override `override val contract: Class? = null` + + +Miscellaneous +^^^^^^^^^^^^^ + +* ``args[0].parseNetworkHostAndPort()`` becomes ``NetworkHostAndPort.parse(args[0])`` + +* There is no longer a ``NodeInfo.advertisedServices`` property. + + The concept of advertised services has been removed from Corda. This is because it was vaguely defined and real world + apps would not typically select random, unknown counterparties from the network map based on self-declared capabilities. + We will introduce a replacement for this functionality, business networks, in a future release. + +Gotchas +^^^^^^^ + +* Beware to use the correct identity when issuing cash: + + The 3rd parameter to ``CashIssueFlow`` should be the ** notary ** (not the ** node identity **) + + +:ref:`Milestone 13 ` +------------ + +Core data structures +^^^^^^^^^^^^^^^^^^^^ + +* `TransactionBuilder` changes. + + Use convenience class ``StateAndContract`` instead of ``TransactionBuilder.withItems()`` for passing + around a state and its contract. + +* Transaction building DSL changes: + + * now need to explicitly pass the ContractClassName into all inputs and outputs. + * `ContractClassName` refers to the class containing the “verifier” method. + +* Contract verify method signature change. + + ``override fun verify(tx: TransactionForContract)`` becomes ``override fun verify(tx: LedgerTransaction)`` + +* No longer need to override Contract ``contract()`` function. + +Node services (ServiceHub) +^^^^^^^^^^^^^ + +* ServiceHub API method changes. + + ``services.networkMapUpdates().justSnapshot`` becomes ``services.networkMapSnapshot()`` + +Configuration +^^^^^^^^^^^^^ + +* No longer need to define ``CordaPluginRegistry`` and configure ``requiredSchemas`` + + Custom contract schemas are automatically detected at startup time by class path scanning. + For testing purposes, use the ``SchemaService`` method to register new custom schemas: + eg. ``services.schemaService.registerCustomSchemas(setOf(YoSchemaV1))`` + +Identity +^^^^^^^^ + +* Party names are now ``CordaX500Name``, not ``X500Name`` + + ``CordaX500Name`` specifies a predefined set of mandatory (organisation, locality, country) + and optional fields (commonName, organisationUnit, state) with validation checking. + Use new builder CordaX500Name.build(X500Name(target)) or, preferably, explicitly define X500Name parameters using + ``CordaX500Name`` constructor. + +Testing +^^^^^^^ + +* MockNetwork Testing. + + Mock nodes in node tests are now of type ``StartedNode``, rather than ``MockNode`` + MockNetwork now returns a BasketOf(>) + Must call internals on StartedNode to get MockNode: + a = nodes.partyNodes[0].internals + b = nodes.partyNodes[1].internals + +* Host and Port change. + + Use string helper function ``parseNetworkHostAndPort()`` to parse a URL on startup. + eg. ``val hostAndPort = args[0].parseNetworkHostAndPort()`` + +* The node driver parameters for starting a node have been reordered, and the node’s name needs to be given as an + ``CordaX500Name``, instead of using ``getX509Name`` + + +:ref:`Milestone 12 ` (First Public Beta) +----------------------------------- + +Core data structures +^^^^^^^^^^^^^^^^^^^^ + +* Transaction building + + You no longer need to specify the type of a ``TransactionBuilder`` as ``TransactionType.General`` + ``TransactionType.General.Builder(notary)`` becomes ``TransactionBuilder(notary)`` + +Build +^^^^^ + +* Gradle dependency reference changes. + + Module name has changed to include `corda` in the artifacts jar name: + +.. sourcecode:: shell + + compile "net.corda:core:$corda_release_version" -> compile "net.corda:corda-core:$corda_release_version" + compile "net.corda:finance:$corda_release_version" -> compile "net.corda:corda-finance:$corda_release_version" + compile "net.corda:jackson:$corda_release_version" -> compile "net.corda:corda-jackson:$corda_release_version" + compile "net.corda:node:$corda_release_version" -> compile "net.corda:corda-node:$corda_release_version" + compile "net.corda:rpc:$corda_release_version" -> compile "net.corda:corda-rpc:$corda_release_version" + +Node services (ServiceHub) +^^^^^^^^^^^^^ + +* ServiceHub API changes. + + ``services.networkMapUpdates()`` becomes ``services.networkMapFeed()`` + ``services.getCashBalances()`` becomes a helper method within the **finance** module contracts package: ``net.corda.finance.contracts.getCashBalances`` + +Finance +^^^^^^^ + +* Financial asset contracts (Cash, CommercialPaper, Obligations) are now a standalone CorDapp within the **finance** module. + + Need to import from respective package within `finance` module: + eg. ``net.corda.finance.contracts.asset.Cash`` + + Likewise, need to import associated asset flows from respective package within `finance` module: + eg. ``net.corda.finance.flows.CashIssueFlow`` + ``net.corda.finance.flows.CashIssueAndPaymentFlow`` + ``net.corda.finance.flows.CashExitFlow`` + +* Moved ``finance`` gradle project files into a ``net.corda.finance`` package namespace. + + This may require adjusting imports of Cash flow references and also of ``StartFlow`` permission in ``gradle.build`` files. + Associated flows (`Cash*Flow`, `TwoPartyTradeFlow`, `TwoPartyDealFlow`) must now be imported from this package. diff --git a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt index f87bfb2390..0525cead6a 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt @@ -50,12 +50,14 @@ object TwoPartyDealFlow { abstract val notaryParty: Party abstract val otherSideSession: FlowSession + // DOCSTART 2 @Suspendable override fun call(): SignedTransaction { progressTracker.currentStep = GENERATING_ID val txIdentities = subFlow(SwapIdentitiesFlow(otherSideSession.counterparty)) val anonymousMe = txIdentities[ourIdentity] ?: ourIdentity.anonymise() val anonymousCounterparty = txIdentities[otherSideSession.counterparty] ?: otherSideSession.counterparty.anonymise() + // DOCEND 2 progressTracker.currentStep = SENDING_PROPOSAL // Make the first message we'll send to kick off the flow. val hello = Handshake(payload, anonymousMe, anonymousCounterparty) diff --git a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt index 47da4a403a..d243792253 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt @@ -169,6 +169,7 @@ object TwoPartyTradeFlow { progressTracker.currentStep = SIGNING val (ptx, cashSigningPubKeys) = assembleSharedTX(assetForSale, tradeRequest, buyerAnonymousIdentity) + // DOCSTART 6 // Now sign the transaction with whatever keys we need to move the cash. val partSignedTx = serviceHub.signInitialTransaction(ptx, cashSigningPubKeys) @@ -180,6 +181,7 @@ object TwoPartyTradeFlow { progressTracker.currentStep = COLLECTING_SIGNATURES val sellerSignature = subFlow(CollectSignatureFlow(partSignedTx, sellerSession, sellerSession.counterparty.owningKey)) val twiceSignedTx = partSignedTx + sellerSignature + // DOCEND 6 // Notarise and record the transaction. progressTracker.currentStep = RECORDING @@ -202,8 +204,8 @@ object TwoPartyTradeFlow { // Register the identity we're about to send payment to. This shouldn't be the same as the asset owner // identity, so that anonymity is enforced. - val wellKnownPayToIdentity = serviceHub.identityService.verifyAndRegisterIdentity(it.payToIdentity) - require(wellKnownPayToIdentity?.party == sellerSession.counterparty) { "Well known identity to pay to must match counterparty identity" } + val wellKnownPayToIdentity = serviceHub.identityService.verifyAndRegisterIdentity(it.payToIdentity) ?: it.payToIdentity + require(wellKnownPayToIdentity.party == sellerSession.counterparty) { "Well known identity to pay to must match counterparty identity" } if (it.price > acceptablePrice) throw UnacceptablePriceException(it.price) 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 27c9a3d08a..fd803ee9ef 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 @@ -1,7 +1,5 @@ package net.corda.node.services -import net.corda.client.rpc.RPCException -import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.Contract import net.corda.core.contracts.PartyAndReference import net.corda.core.cordapp.CordappProvider @@ -18,7 +16,6 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.loggerFor -import net.corda.node.internal.StartedNode import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.node.services.transactions.SimpleNotaryService @@ -27,19 +24,15 @@ import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_NOTARY import net.corda.testing.TestDependencyInjectionBase -import net.corda.testing.driver.DriverDSL import net.corda.testing.driver.DriverDSLExposedInterface import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver import net.corda.testing.node.MockServices -import net.corda.testing.resetTestSerialization -import org.junit.Assert.* +import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import java.net.URLClassLoader import java.nio.file.Files -import java.sql.Driver -import kotlin.test.assertFails import kotlin.test.assertFailsWith class AttachmentLoadingTests : TestDependencyInjectionBase() { diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 30ec451570..4cdcbc42e4 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -51,6 +51,8 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized import rx.Observable import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream @@ -69,7 +71,15 @@ import kotlin.test.assertTrue * * We assume that Alice and Bob already found each other via some market, and have agreed the details already. */ -class TwoPartyTradeFlowTests { +@RunWith(Parameterized::class) +class TwoPartyTradeFlowTests(val anonymous: Boolean) { + companion object { + @JvmStatic + @Parameterized.Parameters + fun data(): Collection { + return listOf(true, false) + } + } private lateinit var mockNet: MockNetwork @Before @@ -542,8 +552,7 @@ class TwoPartyTradeFlowTests { private fun runBuyerAndSeller(notary: Party, sellerNode: StartedNode, buyerNode: StartedNode, - assetToSell: StateAndRef, - anonymous: Boolean = true): RunResult { + assetToSell: StateAndRef): RunResult { val buyerFlows: Observable> = buyerNode.internals.registerInitiatedFlow(BuyerAcceptor::class.java) val firstBuyerFiber = buyerFlows.toFuture().map { it.stateMachine } val seller = SellerInitiator(buyerNode.info.chooseIdentity(), notary, assetToSell, 1000.DOLLARS, anonymous) diff --git a/samples/network-visualiser/build.gradle b/samples/network-visualiser/build.gradle index 72ba1207c3..bad1295dbc 100644 --- a/samples/network-visualiser/build.gradle +++ b/samples/network-visualiser/build.gradle @@ -5,10 +5,10 @@ apply plugin: 'application' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'us.kirchmeier.capsule' -dependencies { - compile project(':samples:irs-demo') - compile project(':node-driver') +// Warning: The network visualiser is not a Cordapp so please do not use it as an example of how +// to build a cordapp +dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" testCompile "junit:junit:$junit_version" @@ -17,8 +17,10 @@ dependencies { compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') compile project(':core') compile project(':finance') + compile project(':node-driver') + compile project(':finance') + compile project(':samples:irs-demo') - // Cordapp dependencies // GraphStream: For visualisation compileOnly "co.paralleluniverse:capsule:$capsule_version" } diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt index faec432e7b..47483f7610 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt @@ -21,6 +21,7 @@ import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.MockNetwork import net.corda.testing.node.TestClock import net.corda.testing.node.setTo +import net.corda.testing.setCordappPackages import net.corda.testing.testNodeConfiguration import rx.Observable import rx.subjects.PublishSubject @@ -261,6 +262,7 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, val networkInitialisationFinished = allOf(*mockNet.nodes.map { it.nodeReadyFuture.toCompletableFuture() }.toTypedArray()) fun start(): Future { + setCordappPackages("net.corda.irs.contract", "net.corda.finance.contract") mockNet.startNodes() mockNet.registerIdentities() // Wait for all the nodes to have finished registering with the network map service. diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContract.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContract.kt index 855994138e..28341e2e92 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContract.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContract.kt @@ -5,11 +5,13 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder -import kotlin.reflect.jvm.jvmName // The dummy contract doesn't do anything useful. It exists for testing purposes, but has to be serializable data class DummyContract(val blank: Any? = null) : Contract { + + val PROGRAM_ID = "net.corda.testing.contracts.DummyContract" + interface State : ContractState { val magicNumber: Int } From 5b79def79161e68c71ec9da0470390cdfa2a0665 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Tue, 3 Oct 2017 10:52:57 +0100 Subject: [PATCH 085/185] Add CordaRPCOps.notaryPartyFromX500Name (#1786) --- .../main/kotlin/net/corda/core/messaging/CordaRPCOps.kt | 8 ++++++++ .../kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt | 2 ++ 2 files changed, 10 insertions(+) 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 60979b02cb..9fddfead5d 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -256,6 +256,14 @@ interface CordaRPCOps : RPCOps { /** Returns the [Party] with the X.500 principal as it's [Party.name]. */ fun wellKnownPartyFromX500Name(x500Name: CordaX500Name): Party? + /** + * Get a notary identity by name. + * + * @return the notary identity, or null if there is no notary by that name. Note that this will return null if there + * is a peer with that name but they are not a recognised notary service. + */ + fun notaryPartyFromX500Name(x500Name: CordaX500Name): Party? + /** * Returns a list of candidate matches for a given string, with optional fuzzy(ish) matching. Fuzzy matching may * get smarter with time e.g. to correct spelling errors, so you should not hard-code indexes into the results 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 c902e8964a..08045171c5 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -200,6 +200,8 @@ class CordaRPCOpsImpl( } } + override fun notaryPartyFromX500Name(x500Name: CordaX500Name): Party? = services.networkMapCache.getNotary(x500Name) + override fun partiesFromName(query: String, exactMatch: Boolean): Set { return database.transaction { services.identityService.partiesFromName(query, exactMatch) From f5784f718d3c2fb60a67d1218d8a8e1f5927bff0 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Tue, 3 Oct 2017 10:54:35 +0100 Subject: [PATCH 086/185] Remove database transaction from notaryPartyFromX500Name (#1786) Remove database transaction from notaryPartyFromX500Name as it doesn't touch the database --- .../main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 08045171c5..ee685466d3 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -176,11 +176,7 @@ class CordaRPCOpsImpl( override fun currentNodeTime(): Instant = Instant.now(services.clock) - override fun waitUntilNetworkReady(): CordaFuture { - return database.transaction { - services.networkMapCache.nodeReady - } - } + override fun waitUntilNetworkReady(): CordaFuture = services.networkMapCache.nodeReady override fun wellKnownPartyFromAnonymous(party: AbstractParty): Party? { return database.transaction { From 2ca140d7f78d190b4171f2ba8e9b87555ca9d3ba Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Wed, 4 Oct 2017 08:51:26 +0100 Subject: [PATCH 087/185] CORDA-540: Allow AMQP switch for storage context (#1799) --- .../persistence/HibernateConfigurationTest.kt | 7 ++++--- .../net/corda/testing/SerializationTestHelpers.kt | 15 +++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt index 79c4650c61..54c7f933c8 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt @@ -10,6 +10,7 @@ import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultService import net.corda.core.schemas.CommonSchemaV1 import net.corda.core.schemas.PersistentStateRef +import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.deserialize import net.corda.core.transactions.SignedTransaction import net.corda.finance.DOLLARS @@ -140,7 +141,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { // execute query val queryResults = entityManager.createQuery(criteriaQuery).resultList - val coins = queryResults.map { it.contractState.deserialize>().data }.sumCash() + val coins = queryResults.map { it.contractState.deserialize>(context = SerializationDefaults.STORAGE_CONTEXT).data }.sumCash() assertThat(coins.toDecimal() >= BigDecimal("50.00")) } @@ -661,7 +662,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { val queryResults = entityManager.createQuery(criteriaQuery).resultList queryResults.forEach { - val contractState = it.contractState.deserialize>() + val contractState = it.contractState.deserialize>(context = SerializationDefaults.STORAGE_CONTEXT) val cashState = contractState.data as Cash.State println("${it.stateRef} with owner: ${cashState.owner.owningKey.toBase58String()}") } @@ -745,7 +746,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { // execute query val queryResults = entityManager.createQuery(criteriaQuery).resultList queryResults.forEach { - val contractState = it.contractState.deserialize>() + val contractState = it.contractState.deserialize>(context = SerializationDefaults.STORAGE_CONTEXT) val cashState = contractState.data as Cash.State println("${it.stateRef} with owner ${cashState.owner.owningKey.toBase58String()} and participants ${cashState.participants.map { it.owningKey.toBase58String() }}") } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt index 73884f6cab..c3f5872cd7 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt @@ -68,19 +68,18 @@ fun initialiseTestSerialization() { registerScheme(AMQPServerSerializationScheme()) } - val AMQP_ENABLE_PROP_NAME = "net.corda.testing.amqp.enable" - // TODO: Remove these "if" conditions once we fully switched to AMQP - (SerializationDefaults.P2P_CONTEXT as TestSerializationContext).delegate = if (java.lang.Boolean.getBoolean(AMQP_ENABLE_PROP_NAME)) { - AMQP_P2P_CONTEXT - } else { - KRYO_P2P_CONTEXT - } + (SerializationDefaults.P2P_CONTEXT as TestSerializationContext).delegate = if (isAmqpEnabled()) AMQP_P2P_CONTEXT else KRYO_P2P_CONTEXT (SerializationDefaults.RPC_SERVER_CONTEXT as TestSerializationContext).delegate = KRYO_RPC_SERVER_CONTEXT (SerializationDefaults.RPC_CLIENT_CONTEXT as TestSerializationContext).delegate = KRYO_RPC_CLIENT_CONTEXT - (SerializationDefaults.STORAGE_CONTEXT as TestSerializationContext).delegate = KRYO_STORAGE_CONTEXT + (SerializationDefaults.STORAGE_CONTEXT as TestSerializationContext).delegate = if (isAmqpEnabled()) AMQP_STORAGE_CONTEXT else KRYO_STORAGE_CONTEXT (SerializationDefaults.CHECKPOINT_CONTEXT as TestSerializationContext).delegate = KRYO_CHECKPOINT_CONTEXT } +private const val AMQP_ENABLE_PROP_NAME = "net.corda.testing.amqp.enable" + +// TODO: Remove usages of this function when we fully switched to AMQP +private fun isAmqpEnabled(): Boolean = java.lang.Boolean.getBoolean(AMQP_ENABLE_PROP_NAME) + fun resetTestSerialization() { (SerializationDefaults.SERIALIZATION_FACTORY as TestSerializationFactory).delegate = null (SerializationDefaults.P2P_CONTEXT as TestSerializationContext).delegate = null From 46532ccbcb6a5084efb9c1ac12a1850c9c34cb5d Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Thu, 5 Oct 2017 09:14:00 +0100 Subject: [PATCH 088/185] Don't repackage well known key types (#1545) * Don't repackage well known key types when converting keys to a well known type * Remove custom key serializers * Remove duplicate serializer registration --- .../kotlin/net/corda/core/crypto/Crypto.kt | 21 ++++++- .../serialization/DefaultKryoCustomizer.kt | 9 ++- .../nodeapi/internal/serialization/Kryo.kt | 60 ------------------- 3 files changed, 23 insertions(+), 67 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index 605b95466c..b6df5e2bc9 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -941,7 +941,16 @@ object Crypto { * is inappropriate for a supported key factory to produce a private key. */ @JvmStatic - fun toSupportedPublicKey(key: PublicKey): PublicKey = decodePublicKey(key.encoded) + fun toSupportedPublicKey(key: PublicKey): PublicKey { + return when (key) { + is BCECPublicKey -> key + is BCRSAPublicKey -> key + is BCSphincs256PublicKey -> key + is EdDSAPublicKey -> key + is CompositeKey -> key + else -> decodePublicKey(key.encoded) + } + } /** * Convert a private key to a supported implementation. This can be used to convert a SUN's EC key to an BC key. @@ -952,5 +961,13 @@ object Crypto { * is inappropriate for a supported key factory to produce a private key. */ @JvmStatic - fun toSupportedPrivateKey(key: PrivateKey): PrivateKey = decodePrivateKey(key.encoded) + fun toSupportedPrivateKey(key: PrivateKey): PrivateKey { + return when (key) { + is BCECPrivateKey -> key + is BCRSAPrivateKey -> key + is BCSphincs256PrivateKey -> key + is EdDSAPrivateKey -> key + else -> decodePrivateKey(key.encoded) + } + } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt index d4a6f11ee2..04104533ee 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt @@ -88,10 +88,10 @@ object DefaultKryoCustomizer { register(BufferedInputStream::class.java, InputStreamSerializer) register(Class.forName("sun.net.www.protocol.jar.JarURLConnection\$JarURLInputStream"), InputStreamSerializer) noReferencesWithin() - register(ECPublicKeyImpl::class.java, ECPublicKeyImplSerializer) - register(EdDSAPublicKey::class.java, Ed25519PublicKeySerializer) - register(EdDSAPrivateKey::class.java, Ed25519PrivateKeySerializer) - register(CompositeKey::class.java, CompositeKeySerializer) // Using a custom serializer for compactness + register(ECPublicKeyImpl::class.java, PublicKeySerializer) + register(EdDSAPublicKey::class.java, PublicKeySerializer) + register(EdDSAPrivateKey::class.java, PrivateKeySerializer) + register(CompositeKey::class.java, PublicKeySerializer) // Using a custom serializer for compactness // Exceptions. We don't bother sending the stack traces as the client will fill in its own anyway. register(Array::class, read = { _, _ -> emptyArray() }, write = { _, _, _ -> }) // This ensures a NonEmptySetSerializer is constructed with an initial value. @@ -109,7 +109,6 @@ object DefaultKryoCustomizer { register(BCRSAPublicKey::class.java, PublicKeySerializer) register(BCSphincs256PrivateKey::class.java, PrivateKeySerializer) register(BCSphincs256PublicKey::class.java, PublicKeySerializer) - register(sun.security.ec.ECPublicKeyImpl::class.java, PublicKeySerializer) register(NotaryChangeWireTransaction::class.java, NotaryChangeWireTransactionSerializer) register(PartyAndCertificate::class.java, PartyAndCertificateSerializer) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt index 82234df40b..2905922460 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt @@ -285,66 +285,6 @@ object SignedTransactionSerializer : Serializer() { } } -/** For serialising an ed25519 private key */ -@ThreadSafe -object Ed25519PrivateKeySerializer : Serializer() { - override fun write(kryo: Kryo, output: Output, obj: EdDSAPrivateKey) { - check(obj.params == Crypto.EDDSA_ED25519_SHA512.algSpec) - output.writeBytesWithLength(obj.seed) - } - - override fun read(kryo: Kryo, input: Input, type: Class): EdDSAPrivateKey { - val seed = input.readBytesWithLength() - return EdDSAPrivateKey(EdDSAPrivateKeySpec(seed, Crypto.EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec)) - } -} - -/** For serialising an ed25519 public key */ -@ThreadSafe -object Ed25519PublicKeySerializer : Serializer() { - override fun write(kryo: Kryo, output: Output, obj: EdDSAPublicKey) { - check(obj.params == Crypto.EDDSA_ED25519_SHA512.algSpec) - output.writeBytesWithLength(obj.abyte) - } - - override fun read(kryo: Kryo, input: Input, type: Class): EdDSAPublicKey { - val A = input.readBytesWithLength() - return EdDSAPublicKey(EdDSAPublicKeySpec(A, Crypto.EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec)) - } -} - -/** For serialising an ed25519 public key */ -@ThreadSafe -object ECPublicKeyImplSerializer : Serializer() { - override fun write(kryo: Kryo, output: Output, obj: ECPublicKeyImpl) { - output.writeBytesWithLength(obj.encoded) - } - - override fun read(kryo: Kryo, input: Input, type: Class): ECPublicKeyImpl { - val A = input.readBytesWithLength() - val der = DerValue(A) - return ECPublicKeyImpl.parse(der) as ECPublicKeyImpl - } -} - -// TODO Implement standardized serialization of CompositeKeys. See JIRA issue: CORDA-249. -@ThreadSafe -object CompositeKeySerializer : Serializer() { - override fun write(kryo: Kryo, output: Output, obj: CompositeKey) { - output.writeInt(obj.threshold) - output.writeInt(obj.children.size) - obj.children.forEach { kryo.writeClassAndObject(output, it) } - } - - override fun read(kryo: Kryo, input: Input, type: Class): CompositeKey { - val threshold = input.readInt() - val children = readListOfLength(kryo, input, minLen = 2) - val builder = CompositeKey.Builder() - children.forEach { builder.addKey(it.node, it.weight) } - return builder.build(threshold) as CompositeKey - } -} - @ThreadSafe object PrivateKeySerializer : Serializer() { override fun write(kryo: Kryo, output: Output, obj: PrivateKey) { From 257756d8628c4f070ad3a1a7ddfa8997bd6f2f11 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Thu, 5 Oct 2017 10:33:35 +0100 Subject: [PATCH 089/185] Change IdentitySyncFlow to only offer own identities (#1740) Change IdentitySyncFlow to only offer confidential identities matching the identity the flow is run as. This avoids risks of nodes being convinced to include a state with a confidential identity in it, by a remote node, then feeding the well known identity to the node during identity sync. --- .../corda/confidential/IdentitySyncFlow.kt | 31 ++++++++------- .../confidential/IdentitySyncFlowTests.kt | 38 ++++++++++++++++++- 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt b/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt index 01f520afda..b454556bf3 100644 --- a/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt +++ b/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt @@ -12,14 +12,12 @@ import net.corda.core.utilities.unwrap object IdentitySyncFlow { /** - * Flow for ensuring that one or more counterparties to a transaction have the full certificate paths of confidential - * identities used in the transaction. This is intended for use as a subflow of another flow, typically between + * Flow for ensuring that our counterparties in a transaction have the full certificate paths for *our* confidential + * identities used in states present in the transaction. This is intended for use as a subflow of another flow, typically between * transaction assembly and signing. An example of where this is useful is where a recipient of a [Cash] state wants * to know that it is being paid by the correct party, and the owner of the state is a confidential identity of that * party. This flow would send a copy of the confidential identity path to the recipient, enabling them to verify that * identity. - * - * @return a mapping of well known identities to the confidential identities used in the transaction. */ // TODO: Can this be triggered automatically from [SendTransactionFlow] class Send(val otherSideSessions: Set, @@ -36,17 +34,10 @@ object IdentitySyncFlow { @Suspendable override fun call() { progressTracker.currentStep = SYNCING_IDENTITIES - val states: List = (tx.inputs.map { serviceHub.loadState(it) }.requireNoNulls().map { it.data } + tx.outputs.map { it.data }) - val identities: Set = states.flatMap { it.participants }.toSet() - // Filter participants down to the set of those not in the network map (are not well known) - val confidentialIdentities = identities - .filter { serviceHub.networkMapCache.getNodesByLegalIdentityKey(it.owningKey).isEmpty() } - .toList() - val identityCertificates: Map = identities - .map { Pair(it, serviceHub.identityService.certificateFromKey(it.owningKey)) }.toMap() + val identityCertificates: Map = extractOurConfidentialIdentities() otherSideSessions.forEach { otherSideSession -> - val requestedIdentities: List = otherSideSession.sendAndReceive>(confidentialIdentities).unwrap { req -> + val requestedIdentities: List = otherSideSession.sendAndReceive>(identityCertificates.keys.toList()).unwrap { req -> require(req.all { it in identityCertificates.keys }) { "${otherSideSession.counterparty} requested a confidential identity not part of transaction: ${tx.id}" } req } @@ -61,6 +52,20 @@ object IdentitySyncFlow { } } + private fun extractOurConfidentialIdentities(): Map { + val states: List = (tx.inputs.map { serviceHub.loadState(it) }.requireNoNulls().map { it.data } + tx.outputs.map { it.data }) + val identities: Set = states.flatMap(ContractState::participants).toSet() + // Filter participants down to the set of those not in the network map (are not well known) + val confidentialIdentities = identities + .filter { serviceHub.networkMapCache.getNodesByLegalIdentityKey(it.owningKey).isEmpty() } + .toList() + return confidentialIdentities + .map { Pair(it, serviceHub.identityService.certificateFromKey(it.owningKey)) } + // Filter down to confidential identities of our well known identity + // TODO: Consider if this too restrictive - we perhaps should be checking the name on the signing certificate in the certificate path instead + .filter { it.second?.name == ourIdentity.name } + .toMap() + } } /** diff --git a/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt b/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt index 1fe89e43ec..ac5863d818 100644 --- a/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt +++ b/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt @@ -13,12 +13,14 @@ import net.corda.core.utilities.unwrap import net.corda.finance.DOLLARS import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.CashIssueAndPaymentFlow +import net.corda.finance.flows.CashPaymentFlow import net.corda.testing.* import net.corda.testing.node.MockNetwork import org.junit.After import org.junit.Before import org.junit.Test import kotlin.test.assertEquals +import kotlin.test.assertNotNull import kotlin.test.assertNull class IdentitySyncFlowTests { @@ -45,12 +47,12 @@ class IdentitySyncFlowTests { val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) val alice: Party = aliceNode.services.myInfo.chooseIdentity() val bob: Party = bobNode.services.myInfo.chooseIdentity() + val notary = notaryNode.services.getDefaultNotary() bobNode.internals.registerInitiatedFlow(Receive::class.java) // Alice issues then pays some cash to a new confidential identity that Bob doesn't know about val anonymous = true val ref = OpaqueBytes.of(0x01) - val notary = aliceNode.services.getDefaultNotary() val issueFlow = aliceNode.services.startFlow(CashIssueAndPaymentFlow(1000.DOLLARS, ref, alice, anonymous, notary)) val issueTx = issueFlow.resultFuture.getOrThrow().stx val confidentialIdentity = issueTx.tx.outputs.map { it.data }.filterIsInstance().single().owner @@ -67,6 +69,40 @@ class IdentitySyncFlowTests { assertEquals(expected, actual) } + @Test + fun `don't offer other's identities confidential identities`() { + // Set up values we'll need + val notaryNode = mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) + val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) + val charlieNode = mockNet.createPartyNode(notaryNode.network.myAddress, CHARLIE.name) + val alice: Party = aliceNode.services.myInfo.chooseIdentity() + val bob: Party = bobNode.services.myInfo.chooseIdentity() + val charlie: Party = charlieNode.services.myInfo.chooseIdentity() + val notary = notaryNode.services.getDefaultNotary() + bobNode.internals.registerInitiatedFlow(Receive::class.java) + + // Charlie issues then pays some cash to a new confidential identity + val anonymous = true + val ref = OpaqueBytes.of(0x01) + val issueFlow = charlieNode.services.startFlow(CashIssueAndPaymentFlow(1000.DOLLARS, ref, charlie, anonymous, notary)) + val issueTx = issueFlow.resultFuture.getOrThrow().stx + val confidentialIdentity = issueTx.tx.outputs.map { it.data }.filterIsInstance().single().owner + val confidentialIdentCert = charlieNode.services.identityService.certificateFromKey(confidentialIdentity.owningKey)!! + + // Manually inject this identity into Alice's database so the node could leak it, but we prove won't + aliceNode.database.transaction { aliceNode.services.identityService.verifyAndRegisterIdentity(confidentialIdentCert) } + assertNotNull(aliceNode.database.transaction { aliceNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity) }) + + // Generate a payment from Charlie to Alice, including the confidential state + val payTx = charlieNode.services.startFlow(CashPaymentFlow(1000.DOLLARS, alice, anonymous)).resultFuture.getOrThrow().stx + + // Run the flow to sync up the identities, and confirm Charlie's confidential identity doesn't leak + assertNull(bobNode.database.transaction { bobNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity) }) + aliceNode.services.startFlow(Initiator(bob, payTx.tx)).resultFuture.getOrThrow() + assertNull(bobNode.database.transaction { bobNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity) }) + } + /** * Very lightweight wrapping flow to trigger the counterparty flow that receives the identities. */ From cf83328d5da82ee71bd6343db608e57b49f45c56 Mon Sep 17 00:00:00 2001 From: Alberto Arri <30873160+al-r3@users.noreply.github.com> Date: Thu, 5 Oct 2017 14:11:10 +0100 Subject: [PATCH 090/185] Add filesystem polling to nodes (#1623) Add the logic in node to poll for new serialized nodes to appear on disk. Newly discovered nodes are automatically added to the PersistentNetworkMapCache --- docs/source/changelog.rst | 3 +- .../net/corda/node/internal/AbstractNode.kt | 3 +- .../services/network/NodeInfoSerializer.kt | 85 ---------- .../node/services/network/NodeInfoWatcher.kt | 150 ++++++++++++++++++ .../network/PersistentNetworkMapCache.kt | 9 +- .../network/NodeInfoSerializerTest.kt | 67 -------- .../services/network/NodeInfoWatcherTest.kt | 113 +++++++++++++ 7 files changed, 271 insertions(+), 159 deletions(-) delete mode 100644 node/src/main/kotlin/net/corda/node/services/network/NodeInfoSerializer.kt create mode 100644 node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt delete mode 100644 node/src/test/kotlin/net/corda/node/services/network/NodeInfoSerializerTest.kt create mode 100644 node/src/test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 0012e64902..c996b4254b 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -11,7 +11,8 @@ UNRELEASED * Cordform may not specify a value for ``NetworkMap``, when that happens, during the task execution the following happens: 1. Each node is started and its signed serialized NodeInfo is written to disk in the node base directory. 2. Every serialized ``NodeInfo`` above is copied in every other node "additional-node-info" folder under the NodeInfo folder. - * Nodes read all the nodes stored in ``additional-node-info`` when the ``NetworkMapService`` starts up. + +* Nodes read and poll the filesystem for serialized ``NodeInfo`` in the ``additional-node-info`` directory. * ``Cordapp`` now has a name field for identifying CorDapps and all CorDapp names are printed to console at startup. 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 3472db31b4..d8146ff1fe 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -170,7 +170,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } private fun saveOwnNodeInfo() { - NodeInfoSerializer().saveToFile(configuration.baseDirectory, info, services.keyManagementService) + NodeInfoWatcher.saveToFile(configuration.baseDirectory, info, services.keyManagementService) } private fun initCertificate() { @@ -409,7 +409,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, services.transactionVerifierService, services.validatedTransactions, services.contractUpgradeService, services, cordappProvider, this) makeNetworkServices(tokenizableServices) - return tokenizableServices } diff --git a/node/src/main/kotlin/net/corda/node/services/network/NodeInfoSerializer.kt b/node/src/main/kotlin/net/corda/node/services/network/NodeInfoSerializer.kt deleted file mode 100644 index 925f393b7a..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/network/NodeInfoSerializer.kt +++ /dev/null @@ -1,85 +0,0 @@ -package net.corda.node.services.network - -import net.corda.cordform.CordformNode -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.SignedData -import net.corda.core.internal.createDirectories -import net.corda.core.internal.div -import net.corda.core.internal.isDirectory -import net.corda.core.node.NodeInfo -import net.corda.core.node.services.KeyManagementService -import net.corda.core.serialization.deserialize -import net.corda.core.serialization.serialize -import net.corda.core.utilities.ByteSequence -import net.corda.core.utilities.loggerFor -import java.io.File -import java.nio.file.Files -import java.nio.file.Path - -/** - * Class containing the logic to serialize and de-serialize a [NodeInfo] to disk and reading it back. - */ -class NodeInfoSerializer { - - companion object { - val logger = loggerFor() - } - - /** - * Saves the given [NodeInfo] to a path. - * The node is 'encoded' as a SignedData, signed with the owning key of its first identity. - * The name of the written file will be "nodeInfo-" followed by the hash of the content. The hash in the filename - * is used so that one can freely copy these files without fearing to overwrite another one. - * - * @param path the path where to write the file, if non-existent it will be created. - * @param nodeInfo the NodeInfo to serialize. - * @param keyManager a KeyManagementService used to sign the NodeInfo data. - */ - fun saveToFile(path: Path, nodeInfo: NodeInfo, keyManager: KeyManagementService) { - try { - path.createDirectories() - val serializedBytes = nodeInfo.serialize() - val regSig = keyManager.sign(serializedBytes.bytes, nodeInfo.legalIdentities.first().owningKey) - val signedData = SignedData(serializedBytes, regSig) - val file = (path / ("nodeInfo-" + SecureHash.sha256(serializedBytes.bytes).toString())).toFile() - file.writeBytes(signedData.serialize().bytes) - } catch (e : Exception) { - logger.warn("Couldn't write node info to file: $e") - } - } - - /** - * Loads all the files contained in a given path and returns the deserialized [NodeInfo]s. - * Signatures are checked before returning a value. - * - * @param nodePath the node base path. NodeInfo files are searched for in nodePath/[NODE_INFO_FOLDER] - * @return a list of [NodeInfo]s - */ - fun loadFromDirectory(nodePath: Path): List { - val result = mutableListOf() - val nodeInfoDirectory = nodePath / CordformNode.NODE_INFO_DIRECTORY - if (!nodeInfoDirectory.isDirectory()) { - logger.info("$nodeInfoDirectory isn't a Directory, not loading NodeInfo from files") - return result - } - for (path in Files.list(nodeInfoDirectory)) { - val file = path.toFile() - if (file.isFile) { - try { - logger.info("Reading NodeInfo from file: $file") - val nodeInfo = loadFromFile(file) - result.add(nodeInfo) - } catch (e: Exception) { - logger.error("Exception parsing NodeInfo from file. $file" , e) - } - } - } - logger.info("Succesfully read ${result.size} NodeInfo files.") - return result - } - - private fun loadFromFile(file: File): NodeInfo { - val signedData = ByteSequence.of(file.readBytes()).deserialize>() - return signedData.verified() - } -} \ No newline at end of file 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 new file mode 100644 index 0000000000..2d4db9e3fa --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt @@ -0,0 +1,150 @@ +package net.corda.node.services.network + +import net.corda.cordform.CordformNode +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.SignedData +import net.corda.core.internal.* +import net.corda.core.node.NodeInfo +import net.corda.core.node.services.KeyManagementService +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize +import net.corda.core.utilities.loggerFor +import rx.Observable +import rx.Scheduler +import rx.schedulers.Schedulers +import java.nio.file.Path +import java.nio.file.StandardWatchEventKinds +import java.nio.file.WatchEvent +import java.nio.file.WatchKey +import java.nio.file.WatchService +import java.util.concurrent.TimeUnit +import kotlin.streams.toList + +/** + * Class containing the logic to + * - Serialize and de-serialize a [NodeInfo] to disk and reading it back. + * - Poll a directory for new serialized [NodeInfo] + * + * @param path the base path of a node. + * @param scheduler a [Scheduler] for the rx [Observable] returned by [nodeInfoUpdates], this is mainly useful for + * testing. It defaults to the io scheduler which is the appropriate value for production uses. + */ +class NodeInfoWatcher(private val nodePath: Path, + private val scheduler: Scheduler = Schedulers.io()) { + + private val nodeInfoDirectory = nodePath / CordformNode.NODE_INFO_DIRECTORY + private val watchService : WatchService? by lazy { initWatch() } + + companion object { + private val logger = loggerFor() + + /** + * Saves the given [NodeInfo] to a path. + * The node is 'encoded' as a SignedData, signed with the owning key of its first identity. + * The name of the written file will be "nodeInfo-" followed by the hash of the content. The hash in the filename + * is used so that one can freely copy these files without fearing to overwrite another one. + * + * @param path the path where to write the file, if non-existent it will be created. + * @param nodeInfo the NodeInfo to serialize. + * @param keyManager a KeyManagementService used to sign the NodeInfo data. + */ + fun saveToFile(path: Path, nodeInfo: NodeInfo, keyManager: KeyManagementService) { + try { + path.createDirectories() + val serializedBytes = nodeInfo.serialize() + val regSig = keyManager.sign(serializedBytes.bytes, + nodeInfo.legalIdentities.first().owningKey) + val signedData = SignedData(serializedBytes, regSig) + val file = (path / ("nodeInfo-" + SecureHash.sha256(serializedBytes.bytes).toString())).toFile() + file.writeBytes(signedData.serialize().bytes) + } catch (e: Exception) { + logger.warn("Couldn't write node info to file", e) + } + } + } + + /** + * Read all the files contained in [nodePath] / [CordformNode.NODE_INFO_DIRECTORY] and keep watching + * the folder for further updates. + * + * @return an [Observable] returning [NodeInfo]s, there is no guarantee that the same value isn't returned more + * than once. + */ + fun nodeInfoUpdates(): Observable { + val pollForFiles = Observable.interval(5, TimeUnit.SECONDS, scheduler) + .flatMapIterable { pollWatch() } + val readCurrentFiles = Observable.from(loadFromDirectory()) + return readCurrentFiles.mergeWith(pollForFiles) + } + + /** + * Loads all the files contained in a given path and returns the deserialized [NodeInfo]s. + * Signatures are checked before returning a value. + * + * @return a list of [NodeInfo]s + */ + private fun loadFromDirectory(): List { + val nodeInfoDirectory = nodePath / CordformNode.NODE_INFO_DIRECTORY + if (!nodeInfoDirectory.isDirectory()) { + logger.info("$nodeInfoDirectory isn't a Directory, not loading NodeInfo from files") + return emptyList() + } + val result = nodeInfoDirectory.list { paths -> + paths.filter { it.isRegularFile() } + .map { processFile(it) } + .toList() + .filterNotNull() + } + logger.info("Successfully read ${result.size} NodeInfo files.") + return result + } + + // Polls the watchService for changes to nodeInfoDirectory, return all the newly read NodeInfos. + private fun pollWatch(): List { + if (watchService == null) { + return emptyList() + } + val watchKey: WatchKey = watchService?.poll() ?: return emptyList() + val files = mutableSetOf() + for (event in watchKey.pollEvents()) { + val kind = event.kind() + if (kind == StandardWatchEventKinds.OVERFLOW) continue + + val ev: WatchEvent = uncheckedCast(event) + val filename = ev.context() + val absolutePath = nodeInfoDirectory.resolve(filename) + if (absolutePath.isRegularFile()) { + files.add(absolutePath) + } + } + val valid = watchKey.reset() + if (!valid) { + logger.warn("Can't poll $nodeInfoDirectory anymore, it was probably deleted.") + } + return files.mapNotNull { processFile(it) } + } + + private fun processFile(file: Path) : NodeInfo? { + try { + logger.info("Reading NodeInfo from file: $file") + val signedData = file.readAll().deserialize>() + return signedData.verified() + } catch (e: Exception) { + logger.warn("Exception parsing NodeInfo from file. $file", e) + return null + } + } + + // Create a WatchService watching for changes in nodeInfoDirectory. + private fun initWatch() : WatchService? { + if (!nodeInfoDirectory.isDirectory()) { + logger.warn("Not watching folder $nodeInfoDirectory it doesn't exist or it's not a directory") + return null + } + val watchService = nodeInfoDirectory.fileSystem.newWatchService() + nodeInfoDirectory.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_MODIFY) + logger.info("Watching $nodeInfoDirectory for new files") + return watchService + } +} 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 f2f5e8396c..2dac97909a 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,11 +1,11 @@ package net.corda.node.services.network import net.corda.core.concurrent.CordaFuture -import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.VisibleForTesting +import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.internal.concurrent.map import net.corda.core.internal.concurrent.openFuture import net.corda.core.messaging.DataFeed @@ -40,6 +40,7 @@ import java.security.PublicKey import java.security.SignatureException import java.util.* import javax.annotation.concurrent.ThreadSafe +import kotlin.collections.HashMap /** * Extremely simple in-memory cache of the network map. @@ -87,6 +88,8 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) .sortedBy { it.name.toString() } } + private val nodeInfoSerializer = NodeInfoWatcher(serviceHub.configuration.baseDirectory) + init { loadFromFiles() serviceHub.database.transaction { loadFromDB() } @@ -94,9 +97,7 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) private fun loadFromFiles() { logger.info("Loading network map from files..") - for (node in NodeInfoSerializer().loadFromDirectory(serviceHub.configuration.baseDirectory)) { - addNode(node) - } + nodeInfoSerializer.nodeInfoUpdates().subscribe { node -> addNode(node) } } override fun getPartyInfo(party: Party): PartyInfo? { diff --git a/node/src/test/kotlin/net/corda/node/services/network/NodeInfoSerializerTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NodeInfoSerializerTest.kt deleted file mode 100644 index 6fe8ea6a28..0000000000 --- a/node/src/test/kotlin/net/corda/node/services/network/NodeInfoSerializerTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -package net.corda.node.services.network - -import net.corda.cordform.CordformNode -import net.corda.core.internal.div -import net.corda.core.node.NodeInfo -import net.corda.core.node.services.KeyManagementService -import net.corda.node.services.identity.InMemoryIdentityService -import net.corda.testing.* -import net.corda.testing.node.MockKeyManagementService -import net.corda.testing.node.NodeBasedTest -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.rules.TemporaryFolder -import java.nio.charset.Charset -import kotlin.test.assertEquals -import kotlin.test.assertTrue -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.contentOf - -class NodeInfoSerializerTest : NodeBasedTest() { - - @Rule @JvmField var folder = TemporaryFolder() - - lateinit var keyManagementService: KeyManagementService - - // Object under test - val nodeInfoSerializer = NodeInfoSerializer() - - companion object { - val nodeInfoFileRegex = Regex("nodeInfo\\-.*") - val nodeInfo = NodeInfo(listOf(), listOf(getTestPartyAndCertificate(ALICE)), 0, 0) - } - - @Before - fun start() { - val identityService = InMemoryIdentityService(trustRoot = DEV_TRUST_ROOT) - keyManagementService = MockKeyManagementService(identityService, ALICE_KEY) - } - - @Test - fun `save a NodeInfo`() { - nodeInfoSerializer.saveToFile(folder.root.toPath(), nodeInfo, keyManagementService) - - assertEquals(1, folder.root.list().size) - val fileName = folder.root.list()[0] - assertTrue(fileName.matches(nodeInfoFileRegex)) - val file = (folder.root.path / fileName).toFile() - // Just check that something is written, another tests verifies that the written value can be read back. - assertThat(contentOf(file)).isNotEmpty() - } - - @Test - fun `load an empty Directory`() { - assertEquals(0, nodeInfoSerializer.loadFromDirectory(folder.root.toPath()).size) - } - - @Test - fun `load a non empty Directory`() { - val nodeInfoFolder = folder.newFolder(CordformNode.NODE_INFO_DIRECTORY) - nodeInfoSerializer.saveToFile(nodeInfoFolder.toPath(), nodeInfo, keyManagementService) - val nodeInfos = nodeInfoSerializer.loadFromDirectory(folder.root.toPath()) - - assertEquals(1, nodeInfos.size) - assertEquals(nodeInfo, nodeInfos.first()) - } -} 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 new file mode 100644 index 0000000000..758374f5c9 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt @@ -0,0 +1,113 @@ +package net.corda.node.services.network + +import net.corda.cordform.CordformNode +import net.corda.core.internal.createDirectories +import net.corda.core.internal.div +import net.corda.core.node.NodeInfo +import net.corda.core.node.services.KeyManagementService +import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.testing.* +import net.corda.testing.node.MockKeyManagementService +import net.corda.testing.node.NodeBasedTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import rx.observers.TestSubscriber +import rx.schedulers.TestScheduler +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.contentOf +import java.nio.file.Path + +class NodeInfoWatcherTest : NodeBasedTest() { + + @Rule @JvmField var folder = TemporaryFolder() + + lateinit var keyManagementService: KeyManagementService + lateinit var nodeInfoPath: Path + val scheduler = TestScheduler(); + val testSubscriber = TestSubscriber() + + // Object under test + lateinit var nodeInfoWatcher: NodeInfoWatcher + + companion object { + val nodeInfoFileRegex = Regex("nodeInfo\\-.*") + val nodeInfo = NodeInfo(listOf(), listOf(getTestPartyAndCertificate(ALICE)), 0, 0) + } + + @Before + fun start() { + val identityService = InMemoryIdentityService(trustRoot = DEV_TRUST_ROOT) + keyManagementService = MockKeyManagementService(identityService, ALICE_KEY) + nodeInfoWatcher = NodeInfoWatcher(folder.root.toPath(), scheduler) + nodeInfoPath = folder.root.toPath() / CordformNode.NODE_INFO_DIRECTORY + } + + @Test + fun `save a NodeInfo`() { + NodeInfoWatcher.saveToFile(folder.root.toPath(), nodeInfo, keyManagementService) + + assertEquals(1, folder.root.list().size) + val fileName = folder.root.list()[0] + assertTrue(fileName.matches(nodeInfoFileRegex)) + val file = (folder.root.path / fileName).toFile() + // Just check that something is written, another tests verifies that the written value can be read back. + assertThat(contentOf(file)).isNotEmpty() + } + + @Test + fun `load an empty Directory`() { + nodeInfoPath.createDirectories() + + nodeInfoWatcher.nodeInfoUpdates() + .subscribe(testSubscriber) + + val readNodes = testSubscriber.onNextEvents.distinct() + scheduler.advanceTimeBy(1, TimeUnit.HOURS) + assertEquals(0, readNodes.size) + } + + @Test + fun `load a non empty Directory`() { + createNodeInfoFileInPath(nodeInfo) + + nodeInfoWatcher.nodeInfoUpdates() + .subscribe(testSubscriber) + + val readNodes = testSubscriber.onNextEvents.distinct() + + assertEquals(1, readNodes.size) + assertEquals(nodeInfo, readNodes.first()) + } + + @Test + fun `polling folder`() { + nodeInfoPath.createDirectories() + + // Start polling with an empty folder. + nodeInfoWatcher.nodeInfoUpdates() + .subscribe(testSubscriber) + // Ensure the watch service is started. + scheduler.advanceTimeBy(1, TimeUnit.HOURS) + + // Check no nodeInfos are read. + assertEquals(0, testSubscriber.valueCount) + createNodeInfoFileInPath(nodeInfo) + + scheduler.advanceTimeBy(1, TimeUnit.HOURS) + + // The same folder can be reported more than once, so take unique values. + val readNodes = testSubscriber.onNextEvents.distinct() + assertEquals(1, readNodes.size) + assertEquals(nodeInfo, readNodes.first()) + } + + // Write a nodeInfo under the right path. + private fun createNodeInfoFileInPath(nodeInfo: NodeInfo) { + NodeInfoWatcher.saveToFile(nodeInfoPath, nodeInfo, keyManagementService) + } +} From 248c898818da157e8368dab34efc7afe1dd04db4 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Thu, 5 Oct 2017 15:20:21 +0100 Subject: [PATCH 091/185] Instantiate Contract classes outside of LedgerTransaction.verify() (#1770) * Load the contract classes when we create the LedgerTransaction. * Fix Verifier tests by making ContractResult serialisable. * Use named parameters to create ContractResult. * Make the @JvmStatic function private. * Refactor ContractResult to use Try.on{} instead. --- .../core/transactions/LedgerTransaction.kt | 36 ++++++++++++------- .../net/corda/verifier/VerifierTests.kt | 2 +- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 9f8bdf1001..d62b1c0e6e 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -7,6 +7,7 @@ import net.corda.core.internal.UpgradeCommand import net.corda.core.internal.castIfPossible import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.Try import java.security.PublicKey import java.util.* import java.util.function.Predicate @@ -48,6 +49,16 @@ data class LedgerTransaction( checkEncumbrancesValid() } + private companion object { + @JvmStatic + private fun createContractFor(className: ContractClassName): Try { + return Try.on { this::class.java.classLoader.loadClass(className).asSubclass(Contract::class.java).getConstructor().newInstance() } + } + } + + private val contracts: Map> = (inputs.map { it.state.contract } + outputs.map { it.contract }) + .toSet().map { it to createContractFor(it) }.toMap() + val inputStates: List get() = inputs.map { it.state.data } /** @@ -98,19 +109,18 @@ data class LedgerTransaction( * If any contract fails to verify, the whole transaction is considered to be invalid. */ private fun verifyContracts() { - val contracts = (inputs.map { it.state.contract } + outputs.map { it.contract }).toSet() - for (contractClassName in contracts) { - val contract = try { - assert(javaClass.classLoader == ClassLoader.getSystemClassLoader()) - javaClass.classLoader.loadClass(contractClassName).asSubclass(Contract::class.java).getConstructor().newInstance() - } catch (e: ClassNotFoundException) { - throw TransactionVerificationException.ContractCreationError(id, contractClassName, e) - } - - try { - contract.verify(this) - } catch (e: Throwable) { - throw TransactionVerificationException.ContractRejection(id, contract, e) + for (contractEntry in contracts.entries) { + val result = contractEntry.value + when (result) { + is Try.Failure -> throw TransactionVerificationException.ContractCreationError(id, contractEntry.key, result.exception) + is Try.Success -> { + val contract = result.value + try { + contract.verify(this) + } catch (e: Throwable) { + throw TransactionVerificationException.ContractRejection(id, contract, e) + } + } } } } diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt index f9965a5ffa..4ce985d0f5 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt @@ -23,7 +23,7 @@ import java.util.concurrent.atomic.AtomicInteger class VerifierTests { private fun generateTransactions(number: Int): List { var currentLedger = GeneratedLedger.empty - val transactions = ArrayList() + val transactions = arrayListOf() val random = SplittableRandom() for (i in 0 until number) { val (tx, ledger) = currentLedger.transactionGenerator.generateOrFail(random) From 9239050fc88aeb9e8f649317f671039bdac5afc1 Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Thu, 5 Oct 2017 15:38:25 +0100 Subject: [PATCH 092/185] Make cordapp-example java tree buildable (#1809) --- .../src/main/kotlin/net/corda/testing/driver/Driver.kt | 7 +++++-- .../src/main/kotlin/net/corda/testing/node/MockNode.kt | 5 +---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index 15fd43b655..8f62fa88c3 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -133,12 +133,15 @@ interface DriverDSLExposedInterface : CordformContext { rpcUsers: List = emptyList(), startInSameProcess: Boolean? = null): CordaFuture>> + /** Call [startWebserver] with a default maximumHeapSize. */ + fun startWebserver(handle: NodeHandle): CordaFuture = startWebserver(handle, "200m") + /** * Starts a web server for a node - * * @param handle The handle for the node that this webserver connects to via RPC. + * @param maximumHeapSize Argument for JVM -Xmx option e.g. "200m". */ - fun startWebserver(handle: NodeHandle, maximumHeapSize: String = "200m"): CordaFuture + fun startWebserver(handle: NodeHandle, maximumHeapSize: String): CordaFuture /** * Starts a network map service node. Note that only a single one should ever be running, so you will probably want diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index 92a40391b5..af479b9382 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -404,10 +404,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, advertisedServices = *arrayOf(ServiceInfo(ValidatingNotaryService.type, serviceName))) } - // Convenience method for Java - fun createPartyNode(networkMapAddress: SingleMessageRecipient, - legalName: CordaX500Name) = createPartyNode(networkMapAddress, legalName, null) - + @JvmOverloads fun createPartyNode(networkMapAddress: SingleMessageRecipient, legalName: CordaX500Name? = null, overrideServices: Map? = null): StartedNode { From e8d21f2311d7e1e63cddc00c7b6b82a92f15cc46 Mon Sep 17 00:00:00 2001 From: Alberto Arri <30873160+al-r3@users.noreply.github.com> Date: Thu, 5 Oct 2017 17:15:40 +0100 Subject: [PATCH 093/185] Deflake NodeInfoWatcherTest (#1811) * Deflake NodeInfoWatcherTest * Moved to integration tests, added eventually to check the validity --- .../services/network/NodeInfoWatcherTest.kt | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) rename node/src/{test => integration-test}/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt (82%) diff --git a/node/src/test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt similarity index 82% rename from node/src/test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt rename to node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt index 758374f5c9..61dee26ad0 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt @@ -5,22 +5,27 @@ import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.node.NodeInfo import net.corda.core.node.services.KeyManagementService +import net.corda.core.utilities.seconds import net.corda.node.services.identity.InMemoryIdentityService -import net.corda.testing.* +import net.corda.testing.ALICE +import net.corda.testing.ALICE_KEY +import net.corda.testing.DEV_TRUST_ROOT +import net.corda.testing.eventually +import net.corda.testing.getTestPartyAndCertificate import net.corda.testing.node.MockKeyManagementService import net.corda.testing.node.NodeBasedTest +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.contentOf import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder import rx.observers.TestSubscriber import rx.schedulers.TestScheduler +import java.nio.file.Path import java.util.concurrent.TimeUnit import kotlin.test.assertEquals import kotlin.test.assertTrue -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.contentOf -import java.nio.file.Path class NodeInfoWatcherTest : NodeBasedTest() { @@ -67,7 +72,7 @@ class NodeInfoWatcherTest : NodeBasedTest() { .subscribe(testSubscriber) val readNodes = testSubscriber.onNextEvents.distinct() - scheduler.advanceTimeBy(1, TimeUnit.HOURS) + advanceTime() assertEquals(0, readNodes.size) } @@ -92,18 +97,24 @@ class NodeInfoWatcherTest : NodeBasedTest() { nodeInfoWatcher.nodeInfoUpdates() .subscribe(testSubscriber) // Ensure the watch service is started. - scheduler.advanceTimeBy(1, TimeUnit.HOURS) - + advanceTime() // Check no nodeInfos are read. assertEquals(0, testSubscriber.valueCount) createNodeInfoFileInPath(nodeInfo) - scheduler.advanceTimeBy(1, TimeUnit.HOURS) + advanceTime() - // The same folder can be reported more than once, so take unique values. - val readNodes = testSubscriber.onNextEvents.distinct() - assertEquals(1, readNodes.size) - assertEquals(nodeInfo, readNodes.first()) + // We need the WatchService to report a change and that might not happen immediately. + eventually(5.seconds) { + // The same folder can be reported more than once, so take unique values. + val readNodes = testSubscriber.onNextEvents.distinct() + assertEquals(1, readNodes.size) + assertEquals(nodeInfo, readNodes.first()) + } + } + + private fun advanceTime() { + scheduler.advanceTimeBy(1, TimeUnit.HOURS) } // Write a nodeInfo under the right path. From 33ba145149c87ae1f19ee37b09c77513588c8efb Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Thu, 5 Oct 2017 17:42:16 +0100 Subject: [PATCH 094/185] CORDA-540: Add verification to ensure that private keys can only be serialized with specific contexts (#1800) --- .../serialization/AllButBlacklisted.kt | 3 ++ .../nodeapi/internal/serialization/Kryo.kt | 18 ++++---- .../serialization/UseCaseAwareness.kt | 12 ++++++ .../internal/serialization/KryoTests.kt | 2 +- .../PrivateKeySerializationTest.kt | 42 +++++++++++++++++++ 5 files changed, 67 insertions(+), 10 deletions(-) create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/UseCaseAwareness.kt create mode 100644 node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/PrivateKeySerializationTest.kt diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt index eadd539414..4bc36e9a8b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt @@ -30,6 +30,9 @@ import kotlin.collections.LinkedHashSet * we can often end up pulling in a lot of objects that do not make sense to put in a checkpoint. * Thus, by blacklisting classes/interfaces we don't expect to be serialised, we can better handle/monitor the aforementioned behaviour. * Inheritance works for blacklisted items, but one can specifically exclude classes from blacklisting as well. + * Note: Custom serializer registration trumps white/black lists. So if a given type has a custom serializer and has its name + * in the blacklist - it will still be serialized as specified by custom serializer. + * For more details, see [net.corda.nodeapi.internal.serialization.CordaClassResolver.getRegistration] */ object AllButBlacklisted : ClassWhitelist { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt index 2905922460..7b998d5cd7 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt @@ -10,30 +10,23 @@ import com.esotericsoftware.kryo.util.MapReferenceResolver import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.PrivacySalt import net.corda.core.contracts.StateRef -import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.Crypto import net.corda.core.crypto.TransactionSignature import net.corda.core.identity.Party import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.SerializationContext.UseCase.* import net.corda.core.serialization.SerializeAsTokenContext import net.corda.core.serialization.SerializedBytes import net.corda.core.toFuture import net.corda.core.toObservable import net.corda.core.transactions.* -import net.i2p.crypto.eddsa.EdDSAPrivateKey -import net.i2p.crypto.eddsa.EdDSAPublicKey -import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec -import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec -import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec import org.bouncycastle.asn1.ASN1InputStream import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.cert.X509CertificateHolder import org.slf4j.Logger import org.slf4j.LoggerFactory import rx.Observable -import sun.security.ec.ECPublicKeyImpl -import sun.security.util.DerValue import java.io.ByteArrayInputStream import java.io.InputStream import java.lang.reflect.InvocationTargetException @@ -285,9 +278,16 @@ object SignedTransactionSerializer : Serializer() { } } +sealed class UseCaseSerializer(private val allowedUseCases: EnumSet) : Serializer() { + protected fun checkUseCase() { + checkUseCase(allowedUseCases) + } +} + @ThreadSafe -object PrivateKeySerializer : Serializer() { +object PrivateKeySerializer : UseCaseSerializer(EnumSet.of(Storage, Checkpoint)) { override fun write(kryo: Kryo, output: Output, obj: PrivateKey) { + checkUseCase() output.writeBytesWithLength(obj.encoded) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/UseCaseAwareness.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/UseCaseAwareness.kt new file mode 100644 index 0000000000..fdf57d28be --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/UseCaseAwareness.kt @@ -0,0 +1,12 @@ +package net.corda.nodeapi.internal.serialization + +import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.SerializationFactory +import java.util.* + +internal fun checkUseCase(allowedUseCases: EnumSet) { + val currentContext: SerializationContext = SerializationFactory.currentFactory?.currentContext ?: throw IllegalStateException("Current context is not set") + if(!allowedUseCases.contains(currentContext.useCase)) { + throw IllegalStateException("UseCase '${currentContext.useCase}' is not within '$allowedUseCases'") + } +} \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt index 1855a4c8c7..bf9c157c86 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt @@ -43,7 +43,7 @@ class KryoTests : TestDependencyInjectionBase() { AllWhitelist, emptyMap(), true, - SerializationContext.UseCase.P2P) + SerializationContext.UseCase.Storage) } @Test diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/PrivateKeySerializationTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/PrivateKeySerializationTest.kt new file mode 100644 index 0000000000..bf9cd8f2e5 --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/PrivateKeySerializationTest.kt @@ -0,0 +1,42 @@ +package net.corda.nodeapi.internal.serialization + +import net.corda.core.crypto.Crypto +import net.corda.core.serialization.SerializationContext.UseCase.* +import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.serialize +import net.corda.testing.TestDependencyInjectionBase +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import java.security.PrivateKey +import kotlin.test.assertTrue + +@RunWith(Parameterized::class) +class PrivateKeySerializationTest(private val privateKey: PrivateKey, private val testName: String) : TestDependencyInjectionBase() { + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{1}") + fun data(): Collection> { + val privateKeys: List = Crypto.supportedSignatureSchemes().filterNot { Crypto.COMPOSITE_KEY === it } + .map { Crypto.generateKeyPair(it).private } + + return privateKeys.map { arrayOf(it, PrivateKeySerializationTest::class.java.simpleName + "-" + it.javaClass.simpleName) } + } + } + + @Test + fun `passed with expected UseCases`() { + assertTrue { privateKey.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes.isNotEmpty() } + assertTrue { privateKey.serialize(context = SerializationDefaults.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") + + } +} \ No newline at end of file From d499ee32bbd25c41e3127e00fc3927770e0a8819 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Fri, 6 Oct 2017 10:16:40 +0100 Subject: [PATCH 095/185] Correct bad exception reference and various rendering issues --- docs/source/tutorial-contract.rst | 6 +++--- docs/source/tutorial-test-dsl.rst | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/source/tutorial-contract.rst b/docs/source/tutorial-contract.rst index 11e6ef0755..8c48d33358 100644 --- a/docs/source/tutorial-contract.rst +++ b/docs/source/tutorial-contract.rst @@ -327,7 +327,7 @@ little odd: we have a *requireThat* construct that looks like it's built into th ordinary function provided by the platform's contract API. Kotlin supports the creation of *domain specific languages* through the intersection of several features of the language, and we use it here to support the natural listing of requirements. To see what it compiles down to, look at the Java version. Each ``"string" using (expression)`` statement -inside a ``requireThat`` turns into an assertion that the given expression is true, with an ``IllegalStateException`` +inside a ``requireThat`` turns into an assertion that the given expression is true, with an ``IllegalArgumentException`` being thrown that contains the string if not. It's just another way to write out a regular assertion, but with the English-language requirement being put front and center. @@ -340,8 +340,8 @@ the owner. 1. We still check there is a CP input state. 2. We want to see that the face value of the CP is being moved as a cash claim against some party, that is, the issuer of the CP is really paying back the face value. -2. The transaction must be happening after the maturity date. -3. The commercial paper must *not* be propagated by this transaction: it must be deleted, by the group having no +3. The transaction must be happening after the maturity date. +4. The commercial paper must *not* be propagated by this transaction: it must be deleted, by the group having no output state. This prevents the same CP being considered redeemable multiple times. To calculate how much cash is moving, we use the ``sumCashBy`` utility function. Again, this is an extension function, diff --git a/docs/source/tutorial-test-dsl.rst b/docs/source/tutorial-test-dsl.rst index 8bfbab79b0..b32be173c7 100644 --- a/docs/source/tutorial-test-dsl.rst +++ b/docs/source/tutorial-test-dsl.rst @@ -141,11 +141,11 @@ Let's take a look at a transaction that fails. :end-before: DOCEND 3 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java -:language: java - :start-after: DOCSTART 3 - :end-before: DOCEND 3 - :dedent: 4 + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 3 + :end-before: DOCEND 3 + :dedent: 4 When run, that code produces the following error: @@ -297,4 +297,4 @@ verification (``this.fails()`` at the end). As in previous examples we can use ` :language: java :start-after: DOCSTART 10 :end-before: DOCEND 10 - :dedent: 4 \ No newline at end of file + :dedent: 4 From 66d78fcf65110962a993998cdf1132f88ed6967b Mon Sep 17 00:00:00 2001 From: Katarzyna Streich Date: Fri, 6 Oct 2017 10:32:54 +0100 Subject: [PATCH 096/185] Cleanup of mock network pre-NetworkParameters (#1802) * MockNode rename overrideServices to notaryIdentity Permit only one override of service. * MockNetwork: force creation of NetworkMapNode Tests cleanup: decouple normal nodes and network map. NetworkMap node is created as first. --- .../confidential/IdentitySyncFlowTests.kt | 14 +-- .../confidential/SwapIdentitiesFlowTests.kt | 5 +- .../net/corda/core/flows/FlowsInJavaTest.java | 7 +- .../net/corda/core/flows/AttachmentTests.kt | 18 +-- .../core/flows/CollectSignaturesFlowTests.kt | 6 +- .../core/flows/ContractUpgradeFlowTest.kt | 4 +- .../net/corda/core/flows/FinalityFlowTests.kt | 6 +- .../internal/ResolveTransactionsFlowTest.kt | 4 +- .../AttachmentSerializationTest.kt | 8 +- .../net/corda/docs/CustomVaultQueryTest.kt | 9 +- .../docs/FxTransactionBuildTutorialTest.kt | 9 +- .../WorkflowTransactionBuildTutorialTest.kt | 9 +- .../corda/finance/flows/CashExitFlowTests.kt | 2 +- .../corda/finance/flows/CashIssueFlowTests.kt | 2 +- .../finance/flows/CashPaymentFlowTests.kt | 2 +- .../node/services/BFTNotaryServiceTests.kt | 1 - .../net/corda/node/CordaRPCOpsImplTest.kt | 7 +- .../node/messaging/InMemoryMessagingTests.kt | 16 +-- .../node/messaging/TwoPartyTradeFlowTests.kt | 46 +++----- .../corda/node/services/NotaryChangeTests.kt | 7 +- .../services/events/ScheduledFlowTests.kt | 4 +- .../network/AbstractNetworkMapServiceTest.kt | 13 +-- .../services/network/NetworkMapCacheTest.kt | 15 ++- .../PersistentNetworkMapServiceTest.kt | 4 +- .../statemachine/FlowFrameworkTests.kt | 32 +++--- .../transactions/NotaryServiceTests.kt | 2 +- .../ValidatingNotaryServiceTests.kt | 2 +- .../corda/irs/api/NodeInterestRatesTest.kt | 2 +- .../net/corda/netmap/simulation/Simulation.kt | 35 +++--- .../corda/testing/FlowStackSnapshotTest.kt | 4 +- .../kotlin/net/corda/testing/node/MockNode.kt | 108 ++++++++++-------- 31 files changed, 195 insertions(+), 208 deletions(-) diff --git a/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt b/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt index ac5863d818..7482825440 100644 --- a/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt +++ b/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt @@ -42,12 +42,12 @@ class IdentitySyncFlowTests { @Test fun `sync confidential identities`() { // Set up values we'll need - val notaryNode = mockNet.createNotaryNode() - val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) - val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) + mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(ALICE.name) + val bobNode = mockNet.createPartyNode(BOB.name) val alice: Party = aliceNode.services.myInfo.chooseIdentity() val bob: Party = bobNode.services.myInfo.chooseIdentity() - val notary = notaryNode.services.getDefaultNotary() + val notary = aliceNode.services.getDefaultNotary() bobNode.internals.registerInitiatedFlow(Receive::class.java) // Alice issues then pays some cash to a new confidential identity that Bob doesn't know about @@ -73,9 +73,9 @@ class IdentitySyncFlowTests { fun `don't offer other's identities confidential identities`() { // Set up values we'll need val notaryNode = mockNet.createNotaryNode() - val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) - val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) - val charlieNode = mockNet.createPartyNode(notaryNode.network.myAddress, CHARLIE.name) + val aliceNode = mockNet.createPartyNode(ALICE.name) + val bobNode = mockNet.createPartyNode(BOB.name) + val charlieNode = mockNet.createPartyNode(CHARLIE.name) val alice: Party = aliceNode.services.myInfo.chooseIdentity() val bob: Party = bobNode.services.myInfo.chooseIdentity() val charlie: Party = charlieNode.services.myInfo.chooseIdentity() diff --git a/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt b/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt index 202da9412e..5554b8ea34 100644 --- a/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt +++ b/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt @@ -23,11 +23,10 @@ class SwapIdentitiesFlowTests { // Set up values we'll need val notaryNode = mockNet.createNotaryNode() - val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) - val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) + val aliceNode = mockNet.createPartyNode(ALICE.name) + val bobNode = mockNet.createPartyNode(BOB.name) val alice: Party = aliceNode.services.myInfo.chooseIdentity() val bob: Party = bobNode.services.myInfo.chooseIdentity() - mockNet.registerIdentities() // Run the flows val requesterFlow = aliceNode.services.startFlow(SwapIdentitiesFlow(bob)) diff --git a/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java b/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java index ff30e6547d..6e836dbaba 100644 --- a/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java +++ b/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java @@ -20,15 +20,14 @@ import static org.junit.Assert.fail; public class FlowsInJavaTest { private final MockNetwork mockNet = new MockNetwork(); - private StartedNode notaryNode; private StartedNode aliceNode; private StartedNode bobNode; @Before public void setUp() throws Exception { - notaryNode = mockNet.createNotaryNode(); - aliceNode = mockNet.createPartyNode(notaryNode.getNetwork().getMyAddress(), TestConstants.getALICE().getName()); - bobNode = mockNet.createPartyNode(notaryNode.getNetwork().getMyAddress(), TestConstants.getBOB().getName()); + mockNet.createNotaryNode(); + aliceNode = mockNet.createPartyNode(TestConstants.getALICE().getName()); + bobNode = mockNet.createPartyNode(TestConstants.getBOB().getName()); mockNet.runNetwork(); // Ensure registration was successful aliceNode.getInternals().getNodeReadyFuture().get(); diff --git a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt index bf5c6af06f..5da5583af6 100644 --- a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt @@ -56,9 +56,9 @@ class AttachmentTests { @Test fun `download and store`() { - val notaryNode = mockNet.createNotaryNode() - val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) - val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) + mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(ALICE.name) + val bobNode = mockNet.createPartyNode(BOB.name) // Ensure that registration was successful before progressing any further mockNet.runNetwork() @@ -94,9 +94,9 @@ class AttachmentTests { @Test fun `missing`() { - val notaryNode = mockNet.createNotaryNode() - val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) - val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) + mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(ALICE.name) + val bobNode = mockNet.createPartyNode(BOB.name) // Ensure that registration was successful before progressing any further mockNet.runNetwork() @@ -120,14 +120,14 @@ class AttachmentTests { val aliceNode = mockNet.createNode(legalName = ALICE.name, nodeFactory = object : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, advertisedServices: Set, id: Int, - overrideServices: Map?, + notaryIdentity: Pair?, entropyRoot: BigInteger): MockNetwork.MockNode { - return object : MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { + return object : MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, id, notaryIdentity, entropyRoot) { override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false } } } }, advertisedServices = *arrayOf(ServiceInfo(SimpleNotaryService.type))) - val bobNode = mockNet.createNode(aliceNode.network.myAddress, legalName = BOB.name) + val bobNode = mockNet.createNode(legalName = BOB.name) // Ensure that registration was successful before progressing any further mockNet.runNetwork() diff --git a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt index 8a913d5983..619ddec8a6 100644 --- a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -34,9 +34,9 @@ class CollectSignaturesFlowTests { setCordappPackages("net.corda.testing.contracts") mockNet = MockNetwork() val notaryNode = mockNet.createNotaryNode() - aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) - bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) - charlieNode = mockNet.createPartyNode(notaryNode.network.myAddress, CHARLIE.name) + aliceNode = mockNet.createPartyNode(ALICE.name) + bobNode = mockNet.createPartyNode(BOB.name) + charlieNode = mockNet.createPartyNode(CHARLIE.name) mockNet.runNetwork() notary = notaryNode.services.getDefaultNotary() aliceNode.internals.ensureRegistered() diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index 18a422441a..197f8ca00c 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -43,8 +43,8 @@ class ContractUpgradeFlowTest { setCordappPackages("net.corda.testing.contracts", "net.corda.finance.contracts.asset", "net.corda.core.flows") mockNet = MockNetwork() val notaryNode = mockNet.createNotaryNode() - aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) - bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) + aliceNode = mockNet.createPartyNode(ALICE.name) + bobNode = mockNet.createPartyNode(BOB.name) // Process registration mockNet.runNetwork() diff --git a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt index 0d38513c25..fb865fd291 100644 --- a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt @@ -25,9 +25,9 @@ class FinalityFlowTests { fun setup() { setCordappPackages("net.corda.finance.contracts.asset") mockNet = MockNetwork() - val notaryNode = mockNet.createNotaryNode() - aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) - bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) + mockNet.createNotaryNode() + aliceNode = mockNet.createPartyNode(ALICE.name) + bobNode = mockNet.createPartyNode(BOB.name) mockNet.runNetwork() aliceNode.internals.ensureRegistered() notary = aliceNode.services.getDefaultNotary() diff --git a/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt b/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt index 6e446d0bd8..bf16c93f53 100644 --- a/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt @@ -39,8 +39,8 @@ class ResolveTransactionsFlowTest { setCordappPackages("net.corda.testing.contracts") mockNet = MockNetwork() notaryNode = mockNet.createNotaryNode() - megaCorpNode = mockNet.createPartyNode(notaryNode.network.myAddress, MEGA_CORP.name) - miniCorpNode = mockNet.createPartyNode(notaryNode.network.myAddress, MINI_CORP.name) + megaCorpNode = mockNet.createPartyNode(MEGA_CORP.name) + miniCorpNode = mockNet.createPartyNode(MINI_CORP.name) megaCorpNode.internals.registerInitiatedFlow(TestResponseFlow::class.java) miniCorpNode.internals.registerInitiatedFlow(TestResponseFlow::class.java) mockNet.runNetwork() diff --git a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt index 37183b1fd8..db7d27d06a 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt @@ -70,7 +70,7 @@ class AttachmentSerializationTest { fun setUp() { mockNet = MockNetwork() server = mockNet.createNode() - client = mockNet.createNode(server.network.myAddress) + client = mockNet.createNode() client.internals.disableDBCloseOnStop() // Otherwise the in-memory database may disappear (taking the checkpoint with it) while we reboot the client. mockNet.runNetwork() server.internals.ensureRegistered() @@ -159,11 +159,11 @@ class AttachmentSerializationTest { private fun rebootClientAndGetAttachmentContent(checkAttachmentsOnLoad: Boolean = true): String { client.dispose() - client = mockNet.createNode(server.network.myAddress, client.internals.id, object : MockNetwork.Factory { + client = mockNet.createNode(client.internals.id, object : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, overrideServices: Map?, + advertisedServices: Set, id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): MockNetwork.MockNode { - return object : MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { + return object : MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, id, notaryIdentity, entropyRoot) { override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = checkAttachmentsOnLoad } } } 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 3105f151cb..36a98ebe8b 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 @@ -22,7 +22,6 @@ import java.util.* class CustomVaultQueryTest { lateinit var mockNet: MockNetwork - lateinit var notaryNode: StartedNode lateinit var nodeA: StartedNode lateinit var nodeB: StartedNode lateinit var notary: Party @@ -32,12 +31,12 @@ class CustomVaultQueryTest { setCordappPackages("net.corda.finance.contracts.asset") mockNet = MockNetwork(threadPerNode = true) val notaryService = ServiceInfo(ValidatingNotaryService.type) - notaryNode = mockNet.createNode( + mockNet.createNode( legalName = DUMMY_NOTARY.name, - overrideServices = mapOf(notaryService to DUMMY_NOTARY_KEY), + notaryIdentity = notaryService to DUMMY_NOTARY_KEY, advertisedServices = *arrayOf(notaryService)) - nodeA = mockNet.createPartyNode(notaryNode.network.myAddress) - nodeB = mockNet.createPartyNode(notaryNode.network.myAddress) + nodeA = mockNet.createPartyNode() + nodeB = mockNet.createPartyNode() nodeA.internals.registerInitiatedFlow(TopupIssuerFlow.TopupIssuer::class.java) nodeA.internals.installCordaService(CustomVaultQuery.Service::class.java) 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 4548af15a3..3152da5650 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 @@ -20,7 +20,6 @@ import kotlin.test.assertEquals class FxTransactionBuildTutorialTest { lateinit var mockNet: MockNetwork - lateinit var notaryNode: StartedNode lateinit var nodeA: StartedNode lateinit var nodeB: StartedNode lateinit var notary: Party @@ -30,12 +29,12 @@ class FxTransactionBuildTutorialTest { setCordappPackages("net.corda.finance.contracts.asset") mockNet = MockNetwork(threadPerNode = true) val notaryService = ServiceInfo(ValidatingNotaryService.type) - notaryNode = mockNet.createNode( + mockNet.createNode( legalName = DUMMY_NOTARY.name, - overrideServices = mapOf(notaryService to DUMMY_NOTARY_KEY), + notaryIdentity = notaryService to DUMMY_NOTARY_KEY, advertisedServices = *arrayOf(notaryService)) - nodeA = mockNet.createPartyNode(notaryNode.network.myAddress) - nodeB = mockNet.createPartyNode(notaryNode.network.myAddress) + nodeA = mockNet.createPartyNode() + nodeB = mockNet.createPartyNode() nodeA.internals.registerCustomSchemas(setOf(CashSchemaV1)) nodeB.internals.registerCustomSchemas(setOf(CashSchemaV1)) nodeB.internals.registerInitiatedFlow(ForeignExchangeRemoteFlow::class.java) 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 4242dd79e3..eed9b53759 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 @@ -20,7 +20,6 @@ import kotlin.test.assertEquals class WorkflowTransactionBuildTutorialTest { lateinit var mockNet: MockNetwork - lateinit var notaryNode: StartedNode lateinit var nodeA: StartedNode lateinit var nodeB: StartedNode @@ -35,12 +34,12 @@ class WorkflowTransactionBuildTutorialTest { setCordappPackages("net.corda.docs") mockNet = MockNetwork(threadPerNode = true) val notaryService = ServiceInfo(ValidatingNotaryService.type) - notaryNode = mockNet.createNode( + mockNet.createNode( legalName = DUMMY_NOTARY.name, - overrideServices = mapOf(Pair(notaryService, DUMMY_NOTARY_KEY)), + notaryIdentity = Pair(notaryService, DUMMY_NOTARY_KEY), advertisedServices = *arrayOf(notaryService)) - nodeA = mockNet.createPartyNode(notaryNode.network.myAddress) - nodeB = mockNet.createPartyNode(notaryNode.network.myAddress) + nodeA = mockNet.createPartyNode() + nodeB = mockNet.createPartyNode() nodeA.internals.registerInitiatedFlow(RecordCompletionFlow::class.java) } diff --git a/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt b/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt index 60c50f3211..515480760a 100644 --- a/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt @@ -31,7 +31,7 @@ class CashExitFlowTests { setCordappPackages("net.corda.finance.contracts.asset") mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) notaryNode = mockNet.createNotaryNode() - bankOfCordaNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) + bankOfCordaNode = mockNet.createPartyNode(BOC.name) notary = notaryNode.services.getDefaultNotary() bankOfCorda = bankOfCordaNode.info.chooseIdentity() 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 87299eea8d..791dc0d9da 100644 --- a/finance/src/test/kotlin/net/corda/finance/flows/CashIssueFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashIssueFlowTests.kt @@ -33,7 +33,7 @@ class CashIssueFlowTests { setCordappPackages("net.corda.finance.contracts.asset") mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) notaryNode = mockNet.createNotaryNode() - bankOfCordaNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) + bankOfCordaNode = mockNet.createPartyNode(BOC.name) bankOfCorda = bankOfCordaNode.info.chooseIdentity() notary = notaryNode.services.getDefaultNotary() mockNet.runNetwork() diff --git a/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt b/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt index 5ba26a9712..9304425778 100644 --- a/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt @@ -34,7 +34,7 @@ class CashPaymentFlowTests { setCordappPackages("net.corda.finance.contracts.asset") mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) notaryNode = mockNet.createNotaryNode() - bankOfCordaNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) + bankOfCordaNode = mockNet.createPartyNode(BOC.name) bankOfCorda = bankOfCordaNode.info.chooseIdentity() notary = notaryNode.services.getDefaultNotary() val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, notary)).resultFuture diff --git a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt index 97859a5cdb..c014788530 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt @@ -58,7 +58,6 @@ class BFTNotaryServiceTests { val notaryClusterAddresses = replicaIds.map { NetworkHostAndPort("localhost", 11000 + it * 10) } replicaIds.forEach { replicaId -> mockNet.createNode( - node.network.myAddress, advertisedServices = bftNotaryService, configOverrides = { whenever(it.bftSMaRt).thenReturn(BFTSMaRtConfiguration(replicaId, false, exposeRaces)) diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index 8fae3f60cc..ce3f6f3a6b 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -67,9 +67,8 @@ class CordaRPCOpsImplTest { setCordappPackages("net.corda.finance.contracts.asset") mockNet = MockNetwork() - val networkMap = mockNet.createNode() - aliceNode = mockNet.createNode(networkMapAddress = networkMap.network.myAddress) - notaryNode = mockNet.createNode(advertisedServices = ServiceInfo(SimpleNotaryService.type), networkMapAddress = networkMap.network.myAddress) + aliceNode = mockNet.createNode() + notaryNode = mockNet.createNode(advertisedServices = ServiceInfo(SimpleNotaryService.type)) rpc = CordaRPCOpsImpl(aliceNode.services, aliceNode.smm, aliceNode.database) CURRENT_RPC_CONTEXT.set(RpcContext(User("user", "pwd", permissions = setOf( startFlowPermission(), @@ -77,7 +76,7 @@ class CordaRPCOpsImplTest { )))) mockNet.runNetwork() - networkMap.internals.ensureRegistered() + mockNet.networkMapNode.internals.ensureRegistered() notary = rpc.notaryIdentities().first() } diff --git a/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt b/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt index e949ca11d2..b3a3fb32e4 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt @@ -47,9 +47,9 @@ class InMemoryMessagingTests { @Test fun basics() { - val node1 = mockNet.createNode() - val node2 = mockNet.createNode(networkMapAddress = node1.network.myAddress) - val node3 = mockNet.createNode(networkMapAddress = node1.network.myAddress) + val node1 = mockNet.networkMapNode + val node2 = mockNet.createNode() + val node3 = mockNet.createNode() val bits = "test-content".toByteArray() var finalDelivery: Message? = null @@ -76,9 +76,9 @@ class InMemoryMessagingTests { @Test fun broadcast() { - val node1 = mockNet.createNode() - val node2 = mockNet.createNode(networkMapAddress = node1.network.myAddress) - val node3 = mockNet.createNode(networkMapAddress = node1.network.myAddress) + val node1 = mockNet.networkMapNode + val node2 = mockNet.createNode() + val node3 = mockNet.createNode() val bits = "test-content".toByteArray() @@ -95,8 +95,8 @@ class InMemoryMessagingTests { */ @Test fun `skip unhandled messages`() { - val node1 = mockNet.createNode() - val node2 = mockNet.createNode(networkMapAddress = node1.network.myAddress) + val node1 = mockNet.networkMapNode + val node2 = mockNet.createNode() var received = 0 node1.network.addMessageHandler("valid_message") { _, _ -> diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 4cdcbc42e4..294d758cf4 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -104,9 +104,9 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { ledger(initialiseSerialization = false) { val notaryNode = mockNet.createNotaryNode() - val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) - val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) - val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) + val aliceNode = mockNet.createPartyNode(ALICE.name) + val bobNode = mockNet.createPartyNode(BOB.name) + val bankNode = mockNet.createPartyNode(BOC.name) val notary = notaryNode.services.getDefaultNotary() val cashIssuer = bankNode.info.chooseIdentity().ref(1) val cpIssuer = bankNode.info.chooseIdentity().ref(1, 2, 3) @@ -153,9 +153,9 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { ledger(initialiseSerialization = false) { val notaryNode = mockNet.createNotaryNode() - val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) - val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) - val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) + val aliceNode = mockNet.createPartyNode(ALICE.name) + val bobNode = mockNet.createPartyNode(BOB.name) + val bankNode = mockNet.createPartyNode(BOC.name) val issuer = bankNode.info.chooseIdentity().ref(1) val notary = aliceNode.services.getDefaultNotary() @@ -207,14 +207,11 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { mockNet = MockNetwork(false) ledger(initialiseSerialization = false) { val notaryNode = mockNet.createNotaryNode() - val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) - var bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) - val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) + val aliceNode = mockNet.createPartyNode(ALICE.name) + var bobNode = mockNet.createPartyNode(BOB.name) + val bankNode = mockNet.createPartyNode(BOC.name) val issuer = bankNode.info.chooseIdentity().ref(1, 2, 3) - // Let the nodes know about each other - normally the network map would handle this - mockNet.registerIdentities() - aliceNode.database.transaction { aliceNode.services.identityService.verifyAndRegisterIdentity(bobNode.info.chooseIdentityAndCert()) } @@ -276,11 +273,11 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { // ... bring the node back up ... the act of constructing the SMM will re-register the message handlers // that Bob was waiting on before the reboot occurred. - bobNode = mockNet.createNode(networkMapAddress, bobAddr.id, object : MockNetwork.Factory { + bobNode = mockNet.createNode(bobAddr.id, object : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, overrideServices: Map?, + advertisedServices: Set, id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): MockNetwork.MockNode { - return MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, bobAddr.id, overrideServices, entropyRoot) + return MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, bobAddr.id, notaryIdentity, entropyRoot) } }, BOB.name) @@ -319,14 +316,14 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { networkMapAddress: SingleMessageRecipient?, name: CordaX500Name): StartedNode { // Create a node in the mock network ... - return mockNet.createNode(networkMapAddress, nodeFactory = object : MockNetwork.Factory { + return mockNet.createNode(nodeFactory = object : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, advertisedServices: Set, id: Int, - overrideServices: Map?, + notaryIdentity: Pair?, entropyRoot: BigInteger): MockNetwork.MockNode { - return object : MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { + return object : MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, id, notaryIdentity, entropyRoot) { // That constructs a recording tx storage override fun makeTransactionStorage(): WritableTransactionStorage { return RecordingTransactionStorage(database, super.makeTransactionStorage()) @@ -349,8 +346,6 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { notaryNode.internals.ensureRegistered() val notary = aliceNode.services.getDefaultNotary() - mockNet.registerIdentities() - ledger(aliceNode.services, initialiseSerialization = false) { // Insert a prospectus type attachment into the commercial paper transaction. @@ -457,8 +452,6 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { notaryNode.internals.ensureRegistered() val notary = aliceNode.services.getDefaultNotary() - mockNet.registerIdentities() - ledger(aliceNode.services, initialiseSerialization = false) { // Insert a prospectus type attachment into the commercial paper transaction. val stream = ByteArrayOutputStream() @@ -604,18 +597,15 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { expectedMessageSubstring: String ) { val notaryNode = mockNet.createNotaryNode() - val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) - val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) - val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) + val aliceNode = mockNet.createPartyNode(ALICE.name) + val bobNode = mockNet.createPartyNode(BOB.name) + val bankNode = mockNet.createPartyNode(BOC.name) val issuer = bankNode.info.chooseIdentity().ref(1, 2, 3) mockNet.runNetwork() notaryNode.internals.ensureRegistered() val notary = aliceNode.services.getDefaultNotary() - // Let the nodes know about each other - normally the network map would handle this - mockNet.registerIdentities() - val bobsBadCash = bobNode.database.transaction { fillUpForBuyer(bobError, issuer, bobNode.info.chooseIdentity(), notary).second diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index e1a560c1ca..4bb4b6aa62 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -42,10 +42,9 @@ class NotaryChangeTests { oldNotaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, advertisedServices = *arrayOf(ServiceInfo(ValidatingNotaryService.type))) - clientNodeA = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress) - clientNodeB = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress) - newNotaryNode = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress, advertisedServices = ServiceInfo(ValidatingNotaryService.type)) - mockNet.registerIdentities() + clientNodeA = mockNet.createNode() + clientNodeB = mockNet.createNode() + newNotaryNode = mockNet.createNode(advertisedServices = ServiceInfo(ValidatingNotaryService.type)) mockNet.runNetwork() // Clear network map registration messages oldNotaryNode.internals.ensureRegistered() newNotaryParty = newNotaryNode.info.legalIdentities[1] diff --git a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt index 9e4215a814..399f58f196 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt @@ -98,8 +98,8 @@ class ScheduledFlowTests { notaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, advertisedServices = *arrayOf(ServiceInfo(ValidatingNotaryService.type))) - val a = mockNet.createUnstartedNode(notaryNode.network.myAddress) - val b = mockNet.createUnstartedNode(notaryNode.network.myAddress) + val a = mockNet.createUnstartedNode() + val b = mockNet.createUnstartedNode() notaryNode.internals.ensureRegistered() diff --git a/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt index 403a149183..a429b84d1f 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt @@ -50,11 +50,8 @@ abstract class AbstractNetworkMapServiceTest @Before fun setup() { mockNet = MockNetwork(defaultFactory = nodeFactory) - mapServiceNode = mockNet.createNode( - nodeFactory = nodeFactory, - legalName = DUMMY_MAP.name, - advertisedServices = *arrayOf(ServiceInfo(SimpleNotaryService.type))) - alice = mockNet.createNode(mapServiceNode.network.myAddress, nodeFactory = nodeFactory, legalName = ALICE.name) + mapServiceNode = mockNet.networkMapNode + alice = mockNet.createNode(nodeFactory = nodeFactory, legalName = ALICE.name) mockNet.runNetwork() lastSerial = System.currentTimeMillis() } @@ -249,7 +246,7 @@ abstract class AbstractNetworkMapServiceTest } private fun addNewNodeToNetworkMap(legalName: CordaX500Name): StartedNode { - val node = mockNet.createNode(mapServiceNode.network.myAddress, legalName = legalName) + val node = mockNet.createNode(legalName = legalName) mockNet.runNetwork() lastSerial = System.currentTimeMillis() return node @@ -275,9 +272,9 @@ abstract class AbstractNetworkMapServiceTest networkMapAddr: SingleMessageRecipient?, advertisedServices: Set, id: Int, - overrideServices: Map?, + notaryIdentity: Pair?, entropyRoot: BigInteger): MockNode { - return object : MockNode(config, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { + return object : MockNode(config, network, null, advertisedServices, id, notaryIdentity, entropyRoot) { override fun makeNetworkMapService() = NullNetworkMapService } } 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 2baba6ff3c..d097f8d614 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 @@ -28,9 +28,9 @@ class NetworkMapCacheTest { @Test fun registerWithNetwork() { - val mapNode = mockNet.createNotaryNode() - val aliceNode = mockNet.createPartyNode(mapNode.network.myAddress, ALICE.name) - val future = aliceNode.services.networkMapCache.addMapService(aliceNode.network, mapNode.network.myAddress, false, null) + mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(ALICE.name) + val future = aliceNode.services.networkMapCache.addMapService(aliceNode.network, mockNet.networkMapNode.network.myAddress, false, null) mockNet.runNetwork() future.getOrThrow() } @@ -39,13 +39,12 @@ class NetworkMapCacheTest { fun `key collision`() { val entropy = BigInteger.valueOf(24012017L) val aliceNode = mockNet.createNode(nodeFactory = MockNetwork.DefaultFactory, legalName = ALICE.name, entropyRoot = entropy) - val bobNode = mockNet.createNode(nodeFactory = MockNetwork.DefaultFactory, legalName = BOB.name, entropyRoot = entropy) - assertEquals(aliceNode.info.chooseIdentity(), bobNode.info.chooseIdentity()) - mockNet.runNetwork() // Node A currently knows only about itself, so this returns node A assertEquals(aliceNode.services.networkMapCache.getNodesByLegalIdentityKey(aliceNode.info.chooseIdentity().owningKey).singleOrNull(), aliceNode.info) + val bobNode = mockNet.createNode(nodeFactory = MockNetwork.DefaultFactory, legalName = BOB.name, entropyRoot = entropy) + assertEquals(aliceNode.info.chooseIdentity(), bobNode.info.chooseIdentity()) aliceNode.services.networkMapCache.addNode(bobNode.info) // The details of node B write over those for node A @@ -55,7 +54,7 @@ class NetworkMapCacheTest { @Test fun `getNodeByLegalIdentity`() { val notaryNode = mockNet.createNotaryNode() - val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) + val aliceNode = mockNet.createPartyNode(ALICE.name) val notaryCache: NetworkMapCache = notaryNode.services.networkMapCache val expected = aliceNode.info @@ -69,7 +68,7 @@ class NetworkMapCacheTest { @Test fun `remove node from cache`() { val notaryNode = mockNet.createNotaryNode() - val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) + val aliceNode = mockNet.createPartyNode(ALICE.name) val notaryLegalIdentity = notaryNode.info.chooseIdentity() val alice = aliceNode.info.chooseIdentity() val notaryCache = notaryNode.services.networkMapCache as PersistentNetworkMapCache diff --git a/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt index 2f6966097a..19bf42cf65 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt @@ -32,9 +32,9 @@ class PersistentNetworkMapServiceTest : AbstractNetworkMapServiceTest, id: Int, - overrideServices: Map?, + notaryIdentity: Pair?, entropyRoot: BigInteger): MockNode { - return object : MockNode(config, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { + return object : MockNode(config, network, networkMapAddr, advertisedServices, id, notaryIdentity, entropyRoot) { override fun makeNetworkMapService() = SwizzleNetworkMapService(services) } } diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 0d832f164a..34f5e256a5 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -79,7 +79,7 @@ class FlowFrameworkTests { setCordappPackages("net.corda.finance.contracts", "net.corda.testing.contracts") mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) node1 = mockNet.createNode() - node2 = mockNet.createNode(networkMapAddress = node1.network.myAddress) + node2 = mockNet.createNode() mockNet.runNetwork() node1.internals.ensureRegistered() @@ -87,17 +87,15 @@ class FlowFrameworkTests { // We intentionally create our own notary and ignore the one provided by the network val notaryKeyPair = generateKeyPair() val notaryService = ServiceInfo(ValidatingNotaryService.type, CordaX500Name(commonName = ValidatingNotaryService.type.id, organisation = "Notary service 2000", locality = "London", country = "GB")) - val overrideServices = mapOf(Pair(notaryService, notaryKeyPair)) + val notaryIdentityOverride = Pair(notaryService, notaryKeyPair) // Note that these notaries don't operate correctly as they don't share their state. They are only used for testing // service addressing. - notary1 = mockNet.createNotaryNode(networkMapAddress = node1.network.myAddress, overrideServices = overrideServices, serviceName = notaryService.name) - notary2 = mockNet.createNotaryNode(networkMapAddress = node1.network.myAddress, overrideServices = overrideServices, serviceName = notaryService.name) + notary1 = mockNet.createNotaryNode(notaryIdentity = notaryIdentityOverride, serviceName = notaryService.name) + notary2 = mockNet.createNotaryNode(notaryIdentity = notaryIdentityOverride, serviceName = notaryService.name) receivedSessionMessagesObservable().forEach { receivedSessionMessages += it } mockNet.runNetwork() - // We don't create a network map, so manually handle registrations - mockNet.registerIdentities() notary1Identity = notary1.services.myInfo.legalIdentities[1] notary2Identity = notary2.services.myInfo.legalIdentities[1] } @@ -160,7 +158,7 @@ class FlowFrameworkTests { @Test fun `flow added before network map does run after init`() { - val node3 = mockNet.createNode(node1.network.myAddress) //create vanilla node + val node3 = mockNet.createNode() //create vanilla node val flow = NoOpFlow() node3.services.startFlow(flow) assertEquals(false, flow.flowStarted) // Not started yet as no network activity has been allowed yet @@ -170,7 +168,7 @@ class FlowFrameworkTests { @Test fun `flow added before network map will be init checkpointed`() { - var node3 = mockNet.createNode(node1.network.myAddress) //create vanilla node + var node3 = mockNet.createNode() //create vanilla node val flow = NoOpFlow() node3.services.startFlow(flow) assertEquals(false, flow.flowStarted) // Not started yet as no network activity has been allowed yet @@ -178,7 +176,7 @@ class FlowFrameworkTests { node3.services.networkMapCache.clearNetworkMapCache() // zap persisted NetworkMapCache to force use of network. node3.dispose() - node3 = mockNet.createNode(node1.network.myAddress, node3.internals.id) + node3 = mockNet.createNode(node3.internals.id) val restoredFlow = node3.getSingleFlow().first assertEquals(false, restoredFlow.flowStarted) // Not started yet as no network activity has been allowed yet mockNet.runNetwork() // Allow network map messages to flow @@ -189,7 +187,7 @@ class FlowFrameworkTests { node3.dispose() // Now it is completed the flow should leave no Checkpoint. - node3 = mockNet.createNode(node1.network.myAddress, node3.internals.id) + node3 = mockNet.createNode(node3.internals.id) mockNet.runNetwork() // Allow network map messages to flow node3.smm.executor.flush() assertTrue(node3.smm.findStateMachines(NoOpFlow::class.java).isEmpty()) @@ -215,7 +213,7 @@ class FlowFrameworkTests { var sentCount = 0 mockNet.messagingNetwork.sentMessages.toSessionTransfers().filter { it.isPayloadTransfer }.forEach { sentCount++ } - val node3 = mockNet.createNode(node1.network.myAddress) + val node3 = mockNet.createNode() val secondFlow = node3.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) } mockNet.runNetwork() @@ -233,7 +231,7 @@ class FlowFrameworkTests { assertEquals(1, node2.checkpointStorage.checkpoints().size) // confirm checkpoint node2.services.networkMapCache.clearNetworkMapCache() } - val node2b = mockNet.createNode(node1.network.myAddress, node2.internals.id, advertisedServices = *node2.internals.advertisedServices.toTypedArray()) + val node2b = mockNet.createNode(node2.internals.id, advertisedServices = *node2.internals.advertisedServices.toTypedArray()) node2.internals.manuallyCloseDB() val (firstAgain, fut1) = node2b.getSingleFlow() // Run the network which will also fire up the second flow. First message should get deduped. So message data stays in sync. @@ -260,7 +258,7 @@ class FlowFrameworkTests { @Test fun `sending to multiple parties`() { - val node3 = mockNet.createNode(node1.network.myAddress) + val node3 = mockNet.createNode() mockNet.runNetwork() node2.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } node3.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } @@ -292,7 +290,7 @@ class FlowFrameworkTests { @Test fun `receiving from multiple parties`() { - val node3 = mockNet.createNode(node1.network.myAddress) + val node3 = mockNet.createNode() mockNet.runNetwork() val node2Payload = "Test 1" val node3Payload = "Test 2" @@ -501,7 +499,7 @@ class FlowFrameworkTests { @Test fun `FlowException propagated in invocation chain`() { - val node3 = mockNet.createNode(node1.network.myAddress) + val node3 = mockNet.createNode() mockNet.runNetwork() node3.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } } @@ -515,7 +513,7 @@ class FlowFrameworkTests { @Test fun `FlowException thrown and there is a 3rd unrelated party flow`() { - val node3 = mockNet.createNode(node1.network.myAddress) + val node3 = mockNet.createNode() mockNet.runNetwork() // Node 2 will send its payload and then block waiting for the receive from node 1. Meanwhile node 1 will move @@ -763,7 +761,7 @@ class FlowFrameworkTests { private inline fun > StartedNode.restartAndGetRestoredFlow(networkMapNode: StartedNode<*>? = null) = internals.run { disableDBCloseOnStop() // Handover DB to new node copy stop() - val newNode = mockNet.createNode(networkMapNode?.network?.myAddress, id, advertisedServices = *advertisedServices.toTypedArray()) + val newNode = mockNet.createNode(id, advertisedServices = *advertisedServices.toTypedArray()) newNode.internals.acceptableLiveFiberCountOnStop = 1 manuallyCloseDB() mockNet.runNetwork() // allow NetworkMapService messages to stabilise and thus start the state machine diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt index eac332f7f6..e395ce739e 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt @@ -39,7 +39,7 @@ class NotaryServiceTests { notaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, advertisedServices = *arrayOf(ServiceInfo(SimpleNotaryService.type))) - clientNode = mockNet.createNode(notaryNode.network.myAddress) + clientNode = mockNet.createNode() mockNet.runNetwork() // Clear network map registration messages notaryNode.internals.ensureRegistered() notary = clientNode.services.getDefaultNotary() diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt index 4de1dfba75..fb4741e373 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt @@ -40,7 +40,7 @@ class ValidatingNotaryServiceTests { legalName = DUMMY_NOTARY.name, advertisedServices = *arrayOf(ServiceInfo(ValidatingNotaryService.type)) ) - clientNode = mockNet.createNode(notaryNode.network.myAddress) + clientNode = mockNet.createNode() mockNet.runNetwork() // Clear network map registration messages notaryNode.internals.ensureRegistered() notary = clientNode.services.getDefaultNotary() diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index 905ee4dbdc..a6b720cf72 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -204,7 +204,7 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { fun `network tearoff`() { val mockNet = MockNetwork(initialiseSerialization = false) val n1 = mockNet.createNotaryNode() - val oracleNode = mockNet.createNode(n1.network.myAddress).apply { + val oracleNode = mockNet.createNode().apply { internals.registerInitiatedFlow(NodeInterestRates.FixQueryHandler::class.java) internals.registerInitiatedFlow(NodeInterestRates.FixSignHandler::class.java) database.transaction { diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt index 47483f7610..ea09fa73c0 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt @@ -54,9 +54,9 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, // This puts together a mock network of SimulatedNodes. open class SimulatedNode(config: NodeConfiguration, mockNet: MockNetwork, networkMapAddress: SingleMessageRecipient?, - advertisedServices: Set, id: Int, overrideServices: Map?, + advertisedServices: Set, id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger) - : MockNetwork.MockNode(config, mockNet, networkMapAddress, advertisedServices, id, overrideServices, entropyRoot) { + : MockNetwork.MockNode(config, mockNet, networkMapAddress, advertisedServices, id, notaryIdentity, entropyRoot) { override val started: StartedNode? get() = uncheckedCast(super.started) override fun findMyLocation(): WorldMapLocation? { return configuration.myLegalName.locality.let { CityDatabase[it] } @@ -67,7 +67,7 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, var counter = 0 override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, overrideServices: Map?, + advertisedServices: Set, id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): SimulatedNode { val letter = 'A' + counter val (city, country) = bankLocations[counter++ % bankLocations.size] @@ -75,13 +75,13 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, val cfg = testNodeConfiguration( baseDirectory = config.baseDirectory, myLegalName = CordaX500Name(organisation = "Bank $letter", locality = city, country = country)) - return SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) + return SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, notaryIdentity, entropyRoot) } fun createAll(): List { return bankLocations.mapIndexed { i, _ -> // Use deterministic seeds so the simulation is stable. Needed so that party owning keys are stable. - mockNet.createUnstartedNode(networkMap.network.myAddress, nodeFactory = this, entropyRoot = BigInteger.valueOf(i.toLong())) + mockNet.createUnstartedNode(nodeFactory = this, entropyRoot = BigInteger.valueOf(i.toLong())) } } } @@ -90,24 +90,24 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, object NetworkMapNodeFactory : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, overrideServices: Map?, + advertisedServices: Set, id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): SimulatedNode { val cfg = testNodeConfiguration( baseDirectory = config.baseDirectory, myLegalName = DUMMY_MAP.name) - return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) {} + return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, notaryIdentity, entropyRoot) {} } } object NotaryNodeFactory : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, overrideServices: Map?, + advertisedServices: Set, id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): SimulatedNode { require(advertisedServices.containsType(SimpleNotaryService.type)) val cfg = testNodeConfiguration( baseDirectory = config.baseDirectory, myLegalName = DUMMY_NOTARY.name) - return SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) + return SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, notaryIdentity, entropyRoot) } } @@ -116,12 +116,12 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, val RATES_SERVICE_NAME = CordaX500Name(organisation = "Rates Service Provider", locality = "Madrid", country = "ES") override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, overrideServices: Map?, + advertisedServices: Set, id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): SimulatedNode { val cfg = testNodeConfiguration( baseDirectory = config.baseDirectory, myLegalName = RATES_SERVICE_NAME) - return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { + return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, notaryIdentity, entropyRoot) { override fun start() = super.start().apply { registerInitiatedFlow(NodeInterestRates.FixQueryHandler::class.java) registerInitiatedFlow(NodeInterestRates.FixSignHandler::class.java) @@ -137,12 +137,12 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, object RegulatorFactory : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, overrideServices: Map?, + advertisedServices: Set, id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): SimulatedNode { val cfg = testNodeConfiguration( baseDirectory = config.baseDirectory, myLegalName = DUMMY_REGULATOR.name) - return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { + return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, notaryIdentity, entropyRoot) { // TODO: Regulatory nodes don't actually exist properly, this is a last minute demo request. // So we just fire a message at a node that doesn't know how to handle it, and it'll ignore it. // But that's fine for visualisation purposes. @@ -152,10 +152,10 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, val mockNet = MockNetwork(networkSendManuallyPumped, runAsync) // This one must come first. - val networkMap = mockNet.createNode(nodeFactory = NetworkMapNodeFactory) - val notary = mockNet.createNode(networkMap.network.myAddress, nodeFactory = NotaryNodeFactory, advertisedServices = ServiceInfo(SimpleNotaryService.type)) - val regulators = listOf(mockNet.createUnstartedNode(networkMap.network.myAddress, nodeFactory = RegulatorFactory)) - val ratesOracle = mockNet.createUnstartedNode(networkMap.network.myAddress, nodeFactory = RatesOracleFactory) + val networkMap = mockNet.startNetworkMapNode(nodeFactory = NetworkMapNodeFactory) + val notary = mockNet.createNode(nodeFactory = NotaryNodeFactory, advertisedServices = ServiceInfo(SimpleNotaryService.type)) + val regulators = listOf(mockNet.createUnstartedNode(nodeFactory = RegulatorFactory)) + val ratesOracle = mockNet.createUnstartedNode(nodeFactory = RatesOracleFactory) // All nodes must be in one of these two lists for the purposes of the visualiser tool. val serviceProviders: List = listOf(notary.internals, ratesOracle, networkMap.internals) @@ -264,7 +264,6 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, fun start(): Future { setCordappPackages("net.corda.irs.contract", "net.corda.finance.contract") mockNet.startNodes() - mockNet.registerIdentities() // Wait for all the nodes to have finished registering with the network map service. return networkInitialisationFinished.thenCompose { startMainSimulation() } } diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt index 15c8fb4c77..f9fc3f3002 100644 --- a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt @@ -295,9 +295,9 @@ class FlowStackSnapshotTest { val notaryService = ServiceInfo(ValidatingNotaryService.type) val notaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, - overrideServices = mapOf(notaryService to DUMMY_NOTARY_KEY), + notaryIdentity = notaryService to DUMMY_NOTARY_KEY, advertisedServices = *arrayOf(notaryService)) - val node = mockNet.createPartyNode(notaryNode.network.myAddress) + val node = mockNet.createPartyNode() node.internals.registerInitiatedFlow(DummyFlow::class.java) node.services.startFlow(FlowStackSnapshotSerializationTestingFlow()).resultFuture.get() val thrown = try { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index af479b9382..2f8f50b7c3 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -6,6 +6,7 @@ import com.nhaarman.mockito_kotlin.whenever import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.random63BitValue import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.cert import net.corda.core.internal.concurrent.doneFuture @@ -76,6 +77,11 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random(), private val defaultFactory: Factory<*> = MockNetwork.DefaultFactory, private val initialiseSerialization: Boolean = true) { + companion object { + // TODO In future PR we're removing the concept of network map node so the details of this mock are not important. + val MOCK_NET_MAP = Party(CordaX500Name(organisation = "Mock Network Map", locality = "Madrid", country = "ES"), DUMMY_KEY_1.public) + } + var nextNodeId = 0 private set private val filesystem = Jimfs.newFileSystem(unix()) @@ -87,6 +93,9 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, /** A read only view of the current set of executing nodes. */ val nodes: List get() = _nodes + private var _networkMapNode: StartedNode? = null + val networkMapNode: StartedNode get() = _networkMapNode ?: startNetworkMapNode() + init { if (initialiseSerialization) initialiseTestSerialization() filesystem.getPath("/nodes").createDirectory() @@ -95,21 +104,21 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, /** Allows customisation of how nodes are created. */ interface Factory { /** - * @param overrideServices a set of service entries to use in place of the node's default service entries, - * for example where a node's service is part of a cluster. + * @param notaryIdentity is an additional override to use in place of the node's default notary service, + * main usage is for when the node is part of a notary cluster. * @param entropyRoot the initial entropy value to use when generating keys. Defaults to an (insecure) random value, * but can be overriden to cause nodes to have stable or colliding identity/service keys. */ fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, overrideServices: Map?, + advertisedServices: Set, id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): N } object DefaultFactory : Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, overrideServices: Map?, + advertisedServices: Set, id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): MockNode { - return MockNode(config, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) + return MockNode(config, network, networkMapAddr, advertisedServices, id, notaryIdentity, entropyRoot) } } @@ -137,8 +146,8 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, } /** - * @param overrideServices a set of service entries to use in place of the node's default service entries, - * for example where a node's service is part of a cluster. + * @param notaryIdentity is an additional override to use in place of the node's default notary service, + * main usage is for when the node is part of a notary cluster. * @param entropyRoot the initial entropy value to use when generating keys. Defaults to an (insecure) random value, * but can be overriden to cause nodes to have stable or colliding identity/service keys. */ @@ -147,7 +156,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, override val networkMapAddress: SingleMessageRecipient?, advertisedServices: Set, val id: Int, - val overrideServices: Map?, + internal val notaryIdentity: Pair?, val entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue())) : AbstractNode(config, advertisedServices, TestClock(), mockNet.busyLatch) { var counter = entropyRoot @@ -199,7 +208,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, } override fun makeKeyManagementService(identityService: IdentityService): KeyManagementService { - return E2ETestKeyManagementService(identityService, partyKeys + (overrideServices?.values ?: emptySet())) + return E2ETestKeyManagementService(identityService, partyKeys + (notaryIdentity?.let { setOf(it.second) } ?: emptySet())) } override fun startMessagingService(rpcOps: RPCOps) { @@ -212,12 +221,11 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, override fun getNotaryIdentity(): PartyAndCertificate? { val defaultIdentity = super.getNotaryIdentity() - val override = overrideServices?.filter { it.key.type.isNotary() }?.entries?.singleOrNull() - return if (override == null || defaultIdentity == null) + return if (notaryIdentity == null || !notaryIdentity.first.type.isNotary() || defaultIdentity == null) defaultIdentity else { // Ensure that we always have notary in name and type of it. TODO It is temporary solution until we will have proper handling of NetworkParameters - myNotaryIdentity = getTestPartyAndCertificate(defaultIdentity.name, override.value.public) + myNotaryIdentity = getTestPartyAndCertificate(defaultIdentity.name, notaryIdentity.second.public) myNotaryIdentity } } @@ -294,49 +302,67 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, } } - fun createUnstartedNode(networkMapAddress: SingleMessageRecipient? = null, forcedID: Int? = null, - legalName: CordaX500Name? = null, overrideServices: Map? = null, + fun startNetworkMapNode(nodeFactory: Factory? = null): StartedNode { + check(_networkMapNode == null) { "Trying to start more than one network map node" } + return uncheckedCast(createNodeImpl(networkMapAddress = null, + forcedID = null, + nodeFactory = nodeFactory ?: defaultFactory, + legalName = MOCK_NET_MAP.name, + notaryIdentity = null, + advertisedServices = arrayOf(), + entropyRoot = BigInteger.valueOf(random63BitValue()), + configOverrides = {}, + start = true + ).started!!.apply { + _networkMapNode = this + }) + } + + fun createUnstartedNode(forcedID: Int? = null, + legalName: CordaX500Name? = null, notaryIdentity: Pair? = null, entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), vararg advertisedServices: ServiceInfo, configOverrides: (NodeConfiguration) -> Any? = {}): MockNode { - return createUnstartedNode(networkMapAddress, forcedID, defaultFactory, legalName, overrideServices, entropyRoot, *advertisedServices, configOverrides = configOverrides) + return createUnstartedNode(forcedID, defaultFactory, legalName, notaryIdentity, entropyRoot, *advertisedServices, configOverrides = configOverrides) } - fun createUnstartedNode(networkMapAddress: SingleMessageRecipient? = null, forcedID: Int? = null, nodeFactory: Factory, - legalName: CordaX500Name? = null, overrideServices: Map? = null, + fun createUnstartedNode(forcedID: Int? = null, nodeFactory: Factory, + legalName: CordaX500Name? = null, notaryIdentity: Pair? = null, entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), vararg advertisedServices: ServiceInfo, configOverrides: (NodeConfiguration) -> Any? = {}): N { - return createNodeImpl(networkMapAddress, forcedID, nodeFactory, false, legalName, overrideServices, entropyRoot, advertisedServices, configOverrides) + val networkMapAddress = networkMapNode.network.myAddress + return createNodeImpl(networkMapAddress, forcedID, nodeFactory, false, legalName, notaryIdentity, entropyRoot, advertisedServices, configOverrides) } /** * Returns a node, optionally created by the passed factory method. - * @param overrideServices a set of service entries to use in place of the node's default service entries, + * @param notaryIdentity a set of service entries to use in place of the node's default service entries, * for example where a node's service is part of a cluster. * @param entropyRoot the initial entropy value to use when generating keys. Defaults to an (insecure) random value, * but can be overridden to cause nodes to have stable or colliding identity/service keys. * @param configOverrides add/override behaviour of the [NodeConfiguration] mock object. */ - fun createNode(networkMapAddress: SingleMessageRecipient? = null, forcedID: Int? = null, - legalName: CordaX500Name? = null, overrideServices: Map? = null, + fun createNode(forcedID: Int? = null, + legalName: CordaX500Name? = null, notaryIdentity: Pair? = null, entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), vararg advertisedServices: ServiceInfo, configOverrides: (NodeConfiguration) -> Any? = {}): StartedNode { - return createNode(networkMapAddress, forcedID, defaultFactory, legalName, overrideServices, entropyRoot, *advertisedServices, configOverrides = configOverrides) + return createNode(forcedID, defaultFactory, legalName, notaryIdentity, entropyRoot, *advertisedServices, configOverrides = configOverrides) } /** Like the other [createNode] but takes a [Factory] and propagates its [MockNode] subtype. */ - fun createNode(networkMapAddress: SingleMessageRecipient? = null, forcedID: Int? = null, nodeFactory: Factory, - legalName: CordaX500Name? = null, overrideServices: Map? = null, + fun createNode(forcedID: Int? = null, nodeFactory: Factory, + legalName: CordaX500Name? = null, notaryIdentity: Pair? = null, entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), vararg advertisedServices: ServiceInfo, configOverrides: (NodeConfiguration) -> Any? = {}): StartedNode { - return uncheckedCast(createNodeImpl(networkMapAddress, forcedID, nodeFactory, true, legalName, overrideServices, entropyRoot, advertisedServices, configOverrides).started)!! + val networkMapAddress = networkMapNode.network.myAddress + return uncheckedCast(createNodeImpl(networkMapAddress, forcedID, nodeFactory, true, legalName, notaryIdentity, entropyRoot, advertisedServices, configOverrides).started)!! } private fun createNodeImpl(networkMapAddress: SingleMessageRecipient?, forcedID: Int?, nodeFactory: Factory, - start: Boolean, legalName: CordaX500Name?, overrideServices: Map?, + start: Boolean, legalName: CordaX500Name?, notaryIdentity: Pair?, entropyRoot: BigInteger, advertisedServices: Array, configOverrides: (NodeConfiguration) -> Any?): N { @@ -347,7 +373,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, whenever(it.dataSourceProperties).thenReturn(makeTestDataSourceProperties("node_${id}_net_$networkId")) configOverrides(it) } - return nodeFactory.create(config, this, networkMapAddress, advertisedServices.toSet(), id, overrideServices, entropyRoot).apply { + return nodeFactory.create(config, this, networkMapAddress, advertisedServices.toSet(), id, notaryIdentity, entropyRoot).apply { if (start) { start() if (threadPerNode && networkMapAddress != null) nodeReadyFuture.getOrThrow() // XXX: What about manually-started nodes? @@ -379,36 +405,22 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, } } - /** - * Register network identities in identity service, normally it's done on network map cache change, but we may run without - * network map service. - */ - fun registerIdentities(){ - nodes.forEach { itNode -> - itNode.started!!.database.transaction { - nodes.map { it.started!!.info.legalIdentitiesAndCerts.first() }.map(itNode.started!!.services.identityService::verifyAndRegisterIdentity) - } - } - } - /** * Construct a default notary node. */ - fun createNotaryNode() = createNotaryNode(null, DUMMY_NOTARY.name, null, null) + fun createNotaryNode() = createNotaryNode(DUMMY_NOTARY.name, null, null) - fun createNotaryNode(networkMapAddress: SingleMessageRecipient? = null, - legalName: CordaX500Name? = null, - overrideServices: Map? = null, + fun createNotaryNode(legalName: CordaX500Name? = null, + notaryIdentity: Pair? = null, serviceName: CordaX500Name? = null): StartedNode { - return createNode(networkMapAddress, legalName = legalName, overrideServices = overrideServices, + return createNode(legalName = legalName, notaryIdentity = notaryIdentity, advertisedServices = *arrayOf(ServiceInfo(ValidatingNotaryService.type, serviceName))) } @JvmOverloads - fun createPartyNode(networkMapAddress: SingleMessageRecipient, - legalName: CordaX500Name? = null, - overrideServices: Map? = null): StartedNode { - return createNode(networkMapAddress, legalName = legalName, overrideServices = overrideServices) + fun createPartyNode(legalName: CordaX500Name? = null, + notaryIdentity: Pair? = null): StartedNode { + return createNode(legalName = legalName, notaryIdentity = notaryIdentity) } @Suppress("unused") // This is used from the network visualiser tool. From fde30d01811fb82d579f8edc04e1c170af192cdb Mon Sep 17 00:00:00 2001 From: Alberto Arri <30873160+al-r3@users.noreply.github.com> Date: Fri, 6 Oct 2017 10:45:17 +0100 Subject: [PATCH 097/185] removed unused variable in PersistentNetworkMapCache (#1813) val publicKey in unused in PersistentNetworkMapCache, removed. --- .../corda/node/services/network/PersistentNetworkMapCache.kt | 2 -- 1 file changed, 2 deletions(-) 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 2dac97909a..b2fb8c3bde 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 @@ -19,7 +19,6 @@ import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor -import net.corda.core.utilities.parsePublicKeyBase58 import net.corda.core.utilities.toBase58String import net.corda.node.services.api.NetworkCacheException import net.corda.node.services.api.NetworkMapCacheInternal @@ -274,7 +273,6 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) for (nodeInfo in result) { try { logger.info("Loaded node info: $nodeInfo") - val publicKey = parsePublicKeyBase58(nodeInfo.legalIdentitiesAndCerts.single { it.isMain }.owningKey) val node = nodeInfo.toNodeInfo() addNode(node) _loadDBSuccess = true // This is used in AbstractNode to indicate that node is ready. From fe4635a81e4f4cf216a8f3cdecd762cfe79317ce Mon Sep 17 00:00:00 2001 From: Matthew Nesbit Date: Tue, 3 Oct 2017 17:47:47 +0100 Subject: [PATCH 098/185] Start Flows from services Update tests Add a test to show progress tracking works Include service startable flow in Cordapp data structures. Respond to PR comments Fixup change of api inline with PR comments. Address PR comments --- .../kotlin/net/corda/core/cordapp/Cordapp.kt | 2 + .../net/corda/core/flows/FlowInitiator.kt | 4 + .../corda/core/flows/StartableByService.kt | 12 ++ .../core/internal/cordapp/CordappImpl.kt | 1 + .../net/corda/core/node/AppServiceHub.kt | 30 ++++ .../corda/core/node/services/CordaService.kt | 2 +- .../net/corda/node/internal/AbstractNode.kt | 60 +++++++- .../node/internal/cordapp/CordappLoader.kt | 14 +- .../node/shell/FlowWatchPrintingSubscriber.kt | 1 + .../corda/node/internal/CordaServiceTest.kt | 132 ++++++++++++++++++ .../internal/cordapp/CordappLoaderTest.kt | 5 + .../corda/testing/node/MockCordappProvider.kt | 2 +- 12 files changed, 254 insertions(+), 11 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/flows/StartableByService.kt create mode 100644 core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt create mode 100644 node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt 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 32c011866e..8f7f5f8d56 100644 --- a/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt +++ b/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt @@ -17,6 +17,7 @@ import java.net.URL * @property contractClassNames List of contracts * @property initiatedFlows List of initiatable flow classes * @property rpcFlows List of RPC initiable flows classes + * @property serviceFlows List of [CordaService] initiable flows classes * @property schedulableFlows List of flows startable by the scheduler * @property servies List of RPC services * @property serializationWhitelists List of Corda plugin registries @@ -28,6 +29,7 @@ interface Cordapp { val contractClassNames: List val initiatedFlows: List>> val rpcFlows: List>> + val serviceFlows: List>> val schedulableFlows: List>> val services: List> val serializationWhitelists: List diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowInitiator.kt b/core/src/main/kotlin/net/corda/core/flows/FlowInitiator.kt index 3860e73a06..549af46dcf 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowInitiator.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowInitiator.kt @@ -20,6 +20,10 @@ sealed class FlowInitiator : Principal { data class Peer(val party: Party) : FlowInitiator() { override fun getName(): String = party.name.toString() } + /** Started by a CordaService. */ + data class Service(val serviceClassName: String) : FlowInitiator() { + override fun getName(): String = serviceClassName + } /** Started as scheduled activity. */ data class Scheduled(val scheduledState: ScheduledStateRef) : FlowInitiator() { override fun getName(): String = "Scheduler" diff --git a/core/src/main/kotlin/net/corda/core/flows/StartableByService.kt b/core/src/main/kotlin/net/corda/core/flows/StartableByService.kt new file mode 100644 index 0000000000..3674c53e04 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/flows/StartableByService.kt @@ -0,0 +1,12 @@ +package net.corda.core.flows + +import kotlin.annotation.AnnotationTarget.CLASS + +/** + * Any [FlowLogic] which is to be started by the [AppServiceHub] interface from + * within a [CordaService] must have this annotation. If it's missing the + * flow will not be allowed to start and an exception will be thrown. + */ +@Target(CLASS) +@MustBeDocumented +annotation class StartableByService \ No newline at end of file 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 e03e83f92c..02410db37a 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 @@ -12,6 +12,7 @@ data class CordappImpl( override val contractClassNames: List, override val initiatedFlows: List>>, override val rpcFlows: List>>, + override val serviceFlows: List>>, override val schedulableFlows: List>>, override val services: List>, override val serializationWhitelists: List, diff --git a/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt new file mode 100644 index 0000000000..94ab6da2e9 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt @@ -0,0 +1,30 @@ +package net.corda.core.node + +import net.corda.core.flows.FlowLogic +import net.corda.core.messaging.FlowHandle +import net.corda.core.messaging.FlowProgressHandle +import rx.Observable + +/** + * A [CordaService] annotated class requires a constructor taking a + * single parameter of type [AppServiceHub]. + * With the [AppServiceHub] parameter a [CordaService] is able to access to privileged operations. + * In particular such a [CordaService] can initiate and track flows marked with [net.corda.core.flows.StartableByService]. + */ +interface AppServiceHub : ServiceHub { + + /** + * Start the given flow with the given arguments. [flow] must be annotated + * with [net.corda.core.flows.StartableByService]. + * TODO it is assumed here that the flow object has an appropriate classloader. + */ + fun startFlow(flow: FlowLogic): FlowHandle + + /** + * Start the given flow with the given arguments, returning an [Observable] with a single observation of the + * result of running the flow. [flow] must be annotated with [net.corda.core.flows.StartableByService]. + * TODO it is assumed here that the flow object has an appropriate classloader. + */ + fun startTrackedFlow(flow: FlowLogic): FlowProgressHandle + +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/node/services/CordaService.kt b/core/src/main/kotlin/net/corda/core/node/services/CordaService.kt index 0e40e3acce..cdefa92037 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/CordaService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/CordaService.kt @@ -7,7 +7,7 @@ import kotlin.annotation.AnnotationTarget.CLASS /** * Annotate any class that needs to be a long-lived service within the node, such as an oracle, with this annotation. - * Such a class needs to have a constructor with a single parameter of type [ServiceHub]. This constructor will be invoked + * Such a class needs to have a constructor with a single parameter of type [AppServiceHub]. This constructor will be invoked * during node start to initialise the service. The service hub provided can be used to get information about the node * that may be necessary for the service. Corda services are created as singletons within the node and are available * to flows via [ServiceHub.cordaService]. 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 d8146ff1fe..15ed414620 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -20,9 +20,8 @@ import net.corda.core.internal.concurrent.flatMap import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.toX509CertHolder import net.corda.core.internal.uncheckedCast -import net.corda.core.messaging.CordaRPCOps -import net.corda.core.messaging.RPCOps -import net.corda.core.messaging.SingleMessageRecipient +import net.corda.core.messaging.* +import net.corda.core.node.AppServiceHub import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub import net.corda.core.node.services.* @@ -264,6 +263,49 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } } + /** + * This customizes the ServiceHub for each CordaService that is initiating flows + */ + private class AppServiceHubImpl(val serviceHub: ServiceHubInternal): AppServiceHub, ServiceHub by serviceHub { + lateinit var serviceInstance: T + override fun startTrackedFlow(flow: FlowLogic): FlowProgressHandle { + val stateMachine = startFlowChecked(flow) + return FlowProgressHandleImpl( + id = stateMachine.id, + returnValue = stateMachine.resultFuture, + progress = stateMachine.logic.track()?.updates ?: Observable.empty() + ) + } + + override fun startFlow(flow: FlowLogic): FlowHandle { + val stateMachine = startFlowChecked(flow) + return FlowHandleImpl(id = stateMachine.id, returnValue = stateMachine.resultFuture) + } + + private fun startFlowChecked(flow: FlowLogic): FlowStateMachineImpl { + val logicType = flow.javaClass + require(logicType.isAnnotationPresent(StartableByService::class.java)) { "${logicType.name} was not designed for starting by a CordaService" } + val currentUser = FlowInitiator.Service(serviceInstance.javaClass.name) + return serviceHub.startFlow(flow, currentUser) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is AppServiceHubImpl<*>) return false + + if (serviceHub != other.serviceHub) return false + if (serviceInstance != other.serviceInstance) return false + + return true + } + + override fun hashCode(): Int { + var result = serviceHub.hashCode() + result = 31 * result + serviceInstance.hashCode() + return result + } + } + /** * Use this method to install your Corda services in your tests. This is automatically done by the node when it * starts up for all classes it finds which are annotated with [CordaService]. @@ -276,8 +318,16 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val constructor = serviceClass.getDeclaredConstructor(ServiceHub::class.java, PublicKey::class.java).apply { isAccessible = true } constructor.newInstance(services, myNotaryIdentity!!.owningKey) } else { - val constructor = serviceClass.getDeclaredConstructor(ServiceHub::class.java).apply { isAccessible = true } - constructor.newInstance(services) + try { + val extendedServiceConstructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java).apply { isAccessible = true } + val serviceContext = AppServiceHubImpl(services) + 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) + } } } catch (e: InvocationTargetException) { throw ServiceInstantiationException(e.cause) diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt index 1449a32fb8..e1cec24244 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt @@ -169,6 +169,7 @@ class CordappLoader private constructor(private val cordappJarPaths: List) listOf(), listOf(), listOf(), + listOf(), setOf(), ContractUpgradeFlow.javaClass.protectionDomain.codeSource.location // Core JAR location ) @@ -180,6 +181,7 @@ class CordappLoader private constructor(private val cordappJarPaths: List) CordappImpl(findContractClassNames(scanResult), findInitiatedFlows(scanResult), findRPCFlows(scanResult), + findServiceFlows(scanResult), findSchedulableFlows(scanResult), findServices(scanResult), findPlugins(it), @@ -207,14 +209,18 @@ class CordappLoader private constructor(private val cordappJarPaths: List) } } - private fun findRPCFlows(scanResult: ScanResult): List>> { - fun Class>.isUserInvokable(): Boolean { - return Modifier.isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || Modifier.isStatic(modifiers)) - } + private fun Class>.isUserInvokable(): Boolean { + return Modifier.isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || Modifier.isStatic(modifiers)) + } + private fun findRPCFlows(scanResult: ScanResult): List>> { return scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class).filter { it.isUserInvokable() } } + private fun findServiceFlows(scanResult: ScanResult): List>> { + return scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByService::class) + } + private fun findSchedulableFlows(scanResult: ScanResult): List>> { return scanResult.getClassesWithAnnotation(FlowLogic::class, SchedulableFlow::class) } diff --git a/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt b/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt index 0c6db1a7b8..32920ffd23 100644 --- a/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt +++ b/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt @@ -111,6 +111,7 @@ class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Sub is FlowInitiator.Shell -> "Shell" // TODO Change when we will have more information on shell user. is FlowInitiator.Peer -> flowInitiator.party.name.organisation is FlowInitiator.RPC -> "RPC: " + flowInitiator.username + is FlowInitiator.Service -> "Service: " + flowInitiator.name } } diff --git a/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt b/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt new file mode 100644 index 0000000000..579b865c1f --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt @@ -0,0 +1,132 @@ +package net.corda.node.internal + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.FlowInitiator +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByService +import net.corda.core.node.AppServiceHub +import net.corda.core.node.ServiceHub +import net.corda.core.node.services.CordaService +import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.ProgressTracker +import net.corda.finance.DOLLARS +import net.corda.finance.flows.CashIssueFlow +import net.corda.node.internal.cordapp.DummyRPCFlow +import net.corda.node.services.transactions.ValidatingNotaryService +import net.corda.nodeapi.internal.ServiceInfo +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.node.MockNetwork +import net.corda.testing.setCordappPackages +import net.corda.testing.unsetCordappPackages +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.concurrent.atomic.AtomicInteger +import kotlin.test.* + +@StartableByService +class DummyServiceFlow : FlowLogic() { + companion object { + object TEST_STEP : ProgressTracker.Step("Custom progress step") + } + override val progressTracker: ProgressTracker = ProgressTracker(TEST_STEP) + + @Suspendable + override fun call(): FlowInitiator { + // We call a subFlow, otehrwise there is no chance to subscribe to the ProgressTracker + subFlow(CashIssueFlow(100.DOLLARS, OpaqueBytes.of(1), serviceHub.networkMapCache.notaryIdentities.first())) + progressTracker.currentStep = TEST_STEP + return stateMachine.flowInitiator + } +} + +@CordaService +class TestCordaService(val appServiceHub: AppServiceHub): SingletonSerializeAsToken() { + fun startServiceFlow() { + val handle = appServiceHub.startFlow(DummyServiceFlow()) + val initiator = handle.returnValue.get() + initiator as FlowInitiator.Service + assertEquals(this.javaClass.name, initiator.serviceClassName) + } + + fun startServiceFlowAndTrack() { + val handle = appServiceHub.startTrackedFlow(DummyServiceFlow()) + val count = AtomicInteger(0) + val subscriber = handle.progress.subscribe { count.incrementAndGet() } + handle.returnValue.get() + // Simply prove some progress was made. + // The actual number is currently 11, but don't want to hard code an implementation detail. + assertTrue(count.get() > 1) + subscriber.unsubscribe() + } + +} + +@CordaService +class TestCordaService2(val appServiceHub: AppServiceHub): SingletonSerializeAsToken() { + fun startInvalidRPCFlow() { + val handle = appServiceHub.startFlow(DummyRPCFlow()) + handle.returnValue.get() + } + +} + +@CordaService +class LegacyCordaService(val simpleServiceHub: ServiceHub): SingletonSerializeAsToken() { + +} + +class CordaServiceTest { + lateinit var mockNet: MockNetwork + lateinit var notaryNode: StartedNode + lateinit var nodeA: StartedNode + + @Before + fun start() { + setCordappPackages("net.corda.node.internal","net.corda.finance") + mockNet = MockNetwork(threadPerNode = true) + notaryNode = mockNet.createNode( + legalName = DUMMY_NOTARY.name, + advertisedServices = *arrayOf(ServiceInfo(ValidatingNotaryService.type))) + nodeA = mockNet.createNode(notaryNode.network.myAddress) + mockNet.startNodes() + } + + @After + fun cleanUp() { + mockNet.stopNodes() + unsetCordappPackages() + } + + @Test + fun `Can find distinct services on node`() { + val service = nodeA.services.cordaService(TestCordaService::class.java) + val service2 = nodeA.services.cordaService(TestCordaService2::class.java) + val legacyService = nodeA.services.cordaService(LegacyCordaService::class.java) + assertEquals(TestCordaService::class.java, service.javaClass) + assertEquals(TestCordaService2::class.java, service2.javaClass) + assertNotEquals(service.appServiceHub, service2.appServiceHub) // Each gets a customised AppServiceHub + assertEquals(LegacyCordaService::class.java, legacyService.javaClass) + } + + @Test + fun `Can start StartableByService flows`() { + val service = nodeA.services.cordaService(TestCordaService::class.java) + service.startServiceFlow() + } + + @Test + fun `Can't start StartableByRPC flows`() { + val service = nodeA.services.cordaService(TestCordaService2::class.java) + assertFailsWith { service.startInvalidRPCFlow() } + } + + + @Test + fun `Test flow with progress tracking`() { + val service = nodeA.services.cordaService(TestCordaService::class.java) + service.startServiceFlowAndTrack() + } + +} \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt index f2352d475d..c1a1b6e5b0 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt @@ -1,5 +1,6 @@ package net.corda.node.internal.cordapp +import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.* import org.assertj.core.api.Assertions.assertThat import org.junit.Test @@ -7,21 +8,25 @@ import java.nio.file.Paths @InitiatingFlow class DummyFlow : FlowLogic() { + @Suspendable override fun call() { } } @InitiatedBy(DummyFlow::class) class LoaderTestFlow(unusedSession: FlowSession) : FlowLogic() { + @Suspendable override fun call() { } } @SchedulableFlow class DummySchedulableFlow : FlowLogic() { + @Suspendable override fun call() { } } @StartableByRPC class DummyRPCFlow : FlowLogic() { + @Suspendable override fun call() { } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt index f51d59d20d..976d595ec6 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt @@ -14,7 +14,7 @@ class MockCordappProvider(cordappLoader: CordappLoader) : CordappProviderImpl(co val cordappRegistry = mutableListOf>() fun addMockCordapp(contractClassName: ContractClassName, services: ServiceHub) { - val cordapp = CordappImpl(listOf(contractClassName), emptyList(), emptyList(), emptyList(), emptyList(), emptyList(), emptySet(), Paths.get(".").toUri().toURL()) + val cordapp = CordappImpl(listOf(contractClassName), emptyList(), emptyList(), emptyList(), emptyList(), emptyList(), emptyList(), emptySet(), Paths.get(".").toUri().toURL()) if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) }) { cordappRegistry.add(Pair(cordapp, findOrImportAttachment(contractClassName.toByteArray(), services))) } From 82d1979b94451697f0a7b225b213f2984fe1b136 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Fri, 6 Oct 2017 11:25:32 +0100 Subject: [PATCH 099/185] Better error for driver instantiated outside package. --- .../src/main/kotlin/net/corda/testing/driver/Driver.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index 8f62fa88c3..344244bfa4 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -990,7 +990,8 @@ class DriverDSL( return Exception() .stackTrace .first { it.fileName != "Driver.kt" } - .let { Class.forName(it.className).`package`.name } + .let { Class.forName(it.className).`package`?.name } + ?: throw IllegalStateException("Function instantiating driver must be defined in a package.") } } } From b5a77a7a9cd4cf82a537252535db0d554b93a2ed Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Fri, 6 Oct 2017 10:29:29 +0100 Subject: [PATCH 100/185] Remove unnecessary `this` in sample code + minor changes to contract paragraph --- .../docs/tutorial/testdsl/TutorialTestDSL.kt | 42 +++++++++---------- docs/source/tutorial-contract.rst | 2 +- docs/source/tutorial-test-dsl.rst | 10 ++--- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt index 2b25fe726e..a52961b0e0 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt @@ -31,7 +31,7 @@ class CommercialPaperTest { transaction { attachments(CP_PROGRAM_ID) input(CP_PROGRAM_ID) { inState } - this.verifies() + verifies() } } } @@ -46,7 +46,7 @@ class CommercialPaperTest { input(CP_PROGRAM_ID) { inState } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } attachments(CP_PROGRAM_ID) - this.verifies() + verifies() } } } @@ -61,7 +61,7 @@ class CommercialPaperTest { input(CP_PROGRAM_ID) { inState } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } attachments(CP_PROGRAM_ID) - this `fails with` "the state is propagated" + `fails with`("the state is propagated") } } } @@ -76,9 +76,9 @@ class CommercialPaperTest { input(CP_PROGRAM_ID) { inState } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } attachments(CP_PROGRAM_ID) - this `fails with` "the state is propagated" + `fails with`("the state is propagated") output(CP_PROGRAM_ID, "alice's paper") { inState.withOwner(ALICE) } - this.verifies() + verifies() } } } @@ -95,11 +95,11 @@ class CommercialPaperTest { // The wrong pubkey. command(BIG_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } timeWindow(TEST_TX_TIME) - this `fails with` "output states are issued by a command signer" + `fails with`("output states are issued by a command signer") } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } timeWindow(TEST_TX_TIME) - this.verifies() + verifies() } } } @@ -115,11 +115,11 @@ class CommercialPaperTest { // The wrong pubkey. command(BIG_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } timeWindow(TEST_TX_TIME) - this `fails with` "output states are issued by a command signer" + `fails with`("output states are issued by a command signer") } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } timeWindow(TEST_TX_TIME) - this.verifies() + verifies() } } // DOCEND 7 @@ -141,7 +141,7 @@ class CommercialPaperTest { command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } attachments(CP_PROGRAM_ID) timeWindow(TEST_TX_TIME) - this.verifies() + verifies() } @@ -152,7 +152,7 @@ class CommercialPaperTest { output(CP_PROGRAM_ID, "alice's paper") { "paper".output().withOwner(ALICE) } command(ALICE_PUBKEY) { Cash.Commands.Move() } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } - this.verifies() + verifies() } } } @@ -174,7 +174,7 @@ class CommercialPaperTest { command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } attachments(CP_PROGRAM_ID) timeWindow(TEST_TX_TIME) - this.verifies() + verifies() } transaction("Trade") { @@ -184,7 +184,7 @@ class CommercialPaperTest { output(CP_PROGRAM_ID, "alice's paper") { "paper".output().withOwner(ALICE) } command(ALICE_PUBKEY) { Cash.Commands.Move() } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } - this.verifies() + verifies() } transaction { @@ -192,10 +192,10 @@ class CommercialPaperTest { // We moved a paper to another pubkey. output(CP_PROGRAM_ID, "bob's paper") { "paper".output().withOwner(BOB) } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } - this.verifies() + verifies() } - this.fails() + fails() } } // DOCEND 9 @@ -216,7 +216,7 @@ class CommercialPaperTest { command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } attachments(CP_PROGRAM_ID) timeWindow(TEST_TX_TIME) - this.verifies() + verifies() } transaction("Trade") { @@ -226,7 +226,7 @@ class CommercialPaperTest { output(CP_PROGRAM_ID, "alice's paper") { "paper".output().withOwner(ALICE) } command(ALICE_PUBKEY) { Cash.Commands.Move() } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } - this.verifies() + verifies() } tweak { @@ -235,13 +235,13 @@ class CommercialPaperTest { // We moved a paper to another pubkey. output(CP_PROGRAM_ID, "bob's paper") { "paper".output().withOwner(BOB) } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } - this.verifies() + verifies() } - this.fails() + fails() } - this.verifies() + verifies() } } // DOCEND 10 -} \ No newline at end of file +} diff --git a/docs/source/tutorial-contract.rst b/docs/source/tutorial-contract.rst index 8c48d33358..23c1430286 100644 --- a/docs/source/tutorial-contract.rst +++ b/docs/source/tutorial-contract.rst @@ -476,7 +476,7 @@ generate methods should operate on the same transaction. You can see an example for the commercial paper contract. The paper is given to us as a ``StateAndRef`` object. This is exactly what it sounds like: -a small object that has a (copy of) a state object, and also the (txhash, index) that indicates the location of this +a small object that has a (copy of a) state object, and also the ``(txhash, index)`` that indicates the location of this state on the ledger. We add the existing paper state as an input, the same paper state with the owner field adjusted as an output, diff --git a/docs/source/tutorial-test-dsl.rst b/docs/source/tutorial-test-dsl.rst index b32be173c7..ce66251a5c 100644 --- a/docs/source/tutorial-test-dsl.rst +++ b/docs/source/tutorial-test-dsl.rst @@ -114,7 +114,7 @@ The above code however doesn't compile: Error:(35, 27) java: incompatible types: bad return type in lambda expression missing return value -This is deliberate: The DSL forces us to specify either ``this.verifies()`` or ``this `fails with` "some text"`` on the +This is deliberate: The DSL forces us to specify either ``verifies()`` or ```fails with`("some text")`` on the last line of ``transaction``: .. container:: codeset @@ -160,7 +160,7 @@ When run, that code produces the following error: net.corda.core.contracts.TransactionVerificationException$ContractRejection: java.lang.IllegalStateException: the state is propagated The transaction verification failed, because we wanted to move paper but didn't specify an output - but the state should be propagated. -However we can specify that this is an intended behaviour by changing ``this.verifies()`` to ``this `fails with` "the state is propagated"``: +However we can specify that this is an intended behaviour by changing ``verifies()`` to ```fails with`("the state is propagated")``: .. container:: codeset @@ -256,7 +256,7 @@ Now that we know how to define a single transaction, let's look at how to define :dedent: 4 In this example we declare that ``ALICE`` has $900 but we don't care where from. For this we can use -``unverifiedTransaction``. Note how we don't need to specify ``this.verifies()``. +``unverifiedTransaction``. Note how we don't need to specify ``verifies()``. Notice that we labelled output with ``"alice's $900"``, also in transaction named ``"Issuance"`` we labelled a commercial paper with ``"paper"``. Now we can subsequently refer to them in other transactions, e.g. @@ -265,7 +265,7 @@ by ``input("alice's $900")`` or ``"paper".output()``. The last transaction named ``"Trade"`` exemplifies simple fact of selling the ``CommercialPaper`` to Alice for her $900, $100 less than the face value at 10% interest after only 7 days. -We can also test whole ledger calling ``this.verifies()`` and ``this.fails()`` on the ledger level. +We can also test whole ledger calling ``verifies()`` and ``fails()`` on the ledger level. To do so let's create a simple example that uses the same input twice: .. container:: codeset @@ -283,7 +283,7 @@ To do so let's create a simple example that uses the same input twice: :dedent: 4 The transactions ``verifies()`` individually, however the state was spent twice! That's why we need the global ledger -verification (``this.fails()`` at the end). As in previous examples we can use ``tweak`` to create a local copy of the whole ledger: +verification (``fails()`` at the end). As in previous examples we can use ``tweak`` to create a local copy of the whole ledger: .. container:: codeset From d9f086cf9199cf8e0691d9f611716f8a96127f4a Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Fri, 6 Oct 2017 11:53:40 +0100 Subject: [PATCH 101/185] Modify the API Scanner plugin to filter method annotations. --- .ci/api-current.txt | 168 +++++++++--------- constants.properties | 2 +- .../main/java/net/corda/plugins/ScanApi.java | 25 ++- 3 files changed, 109 insertions(+), 86 deletions(-) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 6d69bc4289..86d889b83d 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -64,8 +64,8 @@ public final class net.corda.core.contracts.Amount extends java.lang.Object impl @org.jetbrains.annotations.NotNull public final Object component3() @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount copy(long, java.math.BigDecimal, Object) public boolean equals(Object) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.Amount fromDecimal(java.math.BigDecimal, Object) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.Amount fromDecimal(java.math.BigDecimal, Object, java.math.RoundingMode) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.Amount fromDecimal(java.math.BigDecimal, Object) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.Amount fromDecimal(java.math.BigDecimal, Object, java.math.RoundingMode) @org.jetbrains.annotations.NotNull public final java.math.BigDecimal getDisplayTokenSize() @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.math.BigDecimal getDisplayTokenSize(Object) public final long getQuantity() @@ -86,8 +86,8 @@ public final class net.corda.core.contracts.Amount extends java.lang.Object impl public static final net.corda.core.contracts.Amount$Companion Companion ## public static final class net.corda.core.contracts.Amount$Companion extends java.lang.Object - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount fromDecimal(java.math.BigDecimal, Object) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount fromDecimal(java.math.BigDecimal, Object, java.math.RoundingMode) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount fromDecimal(java.math.BigDecimal, Object) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount fromDecimal(java.math.BigDecimal, Object, java.math.RoundingMode) @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final java.math.BigDecimal getDisplayTokenSize(Object) @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Amount parseCurrency(String) @kotlin.jvm.JvmStatic @org.jetbrains.annotations.Nullable public final net.corda.core.contracts.Amount sumOrNull(Iterable) @@ -100,8 +100,8 @@ public final class net.corda.core.contracts.AmountTransfer extends java.lang.Obj @org.jetbrains.annotations.NotNull public final List apply(List, Object) @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.AmountTransfer copy(long, Object, Object, Object) public boolean equals(Object) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.AmountTransfer fromDecimal(java.math.BigDecimal, Object, Object, Object) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.AmountTransfer fromDecimal(java.math.BigDecimal, Object, Object, Object, java.math.RoundingMode) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.AmountTransfer fromDecimal(java.math.BigDecimal, Object, Object, Object) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.contracts.AmountTransfer fromDecimal(java.math.BigDecimal, Object, Object, Object, java.math.RoundingMode) @org.jetbrains.annotations.NotNull public final Object getDestination() public final long getQuantityDelta() @org.jetbrains.annotations.NotNull public final Object getSource() @@ -115,8 +115,8 @@ public final class net.corda.core.contracts.AmountTransfer extends java.lang.Obj public static final net.corda.core.contracts.AmountTransfer$Companion Companion ## public static final class net.corda.core.contracts.AmountTransfer$Companion extends java.lang.Object - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.AmountTransfer fromDecimal(java.math.BigDecimal, Object, Object, Object) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.AmountTransfer fromDecimal(java.math.BigDecimal, Object, Object, Object, java.math.RoundingMode) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.AmountTransfer fromDecimal(java.math.BigDecimal, Object, Object, Object) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.AmountTransfer fromDecimal(java.math.BigDecimal, Object, Object, Object, java.math.RoundingMode) @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.AmountTransfer zero(Object, Object, Object) ## public interface net.corda.core.contracts.Attachment extends net.corda.core.contracts.NamedByHash @@ -370,9 +370,9 @@ public final class net.corda.core.contracts.TransactionResolutionException exten @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getHash() ## public final class net.corda.core.contracts.TransactionState extends java.lang.Object - @kotlin.jvm.JvmOverloads public (net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party) - @kotlin.jvm.JvmOverloads public (net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party, Integer) - @kotlin.jvm.JvmOverloads public (net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party, Integer, net.corda.core.contracts.AttachmentConstraint) + public (net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party) + public (net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party, Integer) + public (net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party, Integer, net.corda.core.contracts.AttachmentConstraint) @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ContractState component1() @org.jetbrains.annotations.NotNull public final String component2() @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component3() @@ -613,9 +613,9 @@ public final class net.corda.core.crypto.Crypto extends java.lang.Object @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme findSignatureScheme(java.security.PrivateKey) @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme findSignatureScheme(java.security.PublicKey) @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.crypto.SignatureScheme findSignatureScheme(org.bouncycastle.asn1.x509.AlgorithmIdentifier) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final java.security.KeyPair generateKeyPair() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.KeyPair generateKeyPair() @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.KeyPair generateKeyPair(String) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final java.security.KeyPair generateKeyPair(net.corda.core.crypto.SignatureScheme) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final java.security.KeyPair generateKeyPair(net.corda.core.crypto.SignatureScheme) @kotlin.jvm.JvmStatic public static final boolean isSupportedSignatureScheme(net.corda.core.crypto.SignatureScheme) @kotlin.jvm.JvmStatic public static final boolean isValid(java.security.PublicKey, byte[], byte[]) @kotlin.jvm.JvmStatic public static final boolean isValid(net.corda.core.crypto.SecureHash, net.corda.core.crypto.TransactionSignature) @@ -920,10 +920,10 @@ public final class net.corda.core.flows.CollectSignatureFlow extends net.corda.c @org.jetbrains.annotations.NotNull public final List getSigningKeys() ## public final class net.corda.core.flows.CollectSignaturesFlow extends net.corda.core.flows.FlowLogic - @kotlin.jvm.JvmOverloads public (net.corda.core.transactions.SignedTransaction, Collection) - @kotlin.jvm.JvmOverloads public (net.corda.core.transactions.SignedTransaction, Collection, Iterable) - @kotlin.jvm.JvmOverloads public (net.corda.core.transactions.SignedTransaction, Collection, Iterable, net.corda.core.utilities.ProgressTracker) - @kotlin.jvm.JvmOverloads public (net.corda.core.transactions.SignedTransaction, Collection, net.corda.core.utilities.ProgressTracker) + public (net.corda.core.transactions.SignedTransaction, Collection) + public (net.corda.core.transactions.SignedTransaction, Collection, Iterable) + public (net.corda.core.transactions.SignedTransaction, Collection, Iterable, net.corda.core.utilities.ProgressTracker) + public (net.corda.core.transactions.SignedTransaction, Collection, net.corda.core.utilities.ProgressTracker) @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public net.corda.core.transactions.SignedTransaction call() @org.jetbrains.annotations.Nullable public final Iterable getMyOptionalKeys() @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction getPartiallySignedTx() @@ -1234,9 +1234,9 @@ public static final class net.corda.core.flows.StateMachineRunId$Companion exten @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StateMachineRunId createRandom() ## public class net.corda.core.flows.StateReplacementException extends net.corda.core.flows.FlowException - @kotlin.jvm.JvmOverloads public () - @kotlin.jvm.JvmOverloads public (String) - @kotlin.jvm.JvmOverloads public (String, Throwable) + public () + public (String) + public (String, Throwable) ## public final class net.corda.core.flows.TransactionParts extends java.lang.Object public (net.corda.core.crypto.SecureHash, List, net.corda.core.contracts.TimeWindow, net.corda.core.identity.Party) @@ -1831,9 +1831,9 @@ public final class net.corda.core.node.services.vault.BinaryLogicalOperator exte public static net.corda.core.node.services.vault.BinaryLogicalOperator[] values() ## public final class net.corda.core.node.services.vault.Builder extends java.lang.Object - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression avg(reflect.Field) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression avg(reflect.Field, List) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression avg(reflect.Field, List, net.corda.core.node.services.vault.Sort$Direction) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression avg(reflect.Field) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression avg(reflect.Field, List) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression avg(reflect.Field, List, net.corda.core.node.services.vault.Sort$Direction) @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression avg(kotlin.reflect.KProperty1, List, net.corda.core.node.services.vault.Sort$Direction) @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$Between between(Comparable, Comparable) @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression between(reflect.Field, Comparable, Comparable) @@ -1867,13 +1867,13 @@ public final class net.corda.core.node.services.vault.Builder extends java.lang. @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression lessThanOrEqual(kotlin.reflect.KProperty1, Comparable) @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression like(reflect.Field, String) @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression like(kotlin.reflect.KProperty1, String) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression max(reflect.Field) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression max(reflect.Field, List) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression max(reflect.Field, List, net.corda.core.node.services.vault.Sort$Direction) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression max(reflect.Field) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression max(reflect.Field, List) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression max(reflect.Field, List, net.corda.core.node.services.vault.Sort$Direction) @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression max(kotlin.reflect.KProperty1, List, net.corda.core.node.services.vault.Sort$Direction) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression min(reflect.Field) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression min(reflect.Field, List) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression min(reflect.Field, List, net.corda.core.node.services.vault.Sort$Direction) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression min(reflect.Field) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression min(reflect.Field, List) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression min(reflect.Field, List, net.corda.core.node.services.vault.Sort$Direction) @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression min(kotlin.reflect.KProperty1, List, net.corda.core.node.services.vault.Sort$Direction) @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.ColumnPredicate$EqualityComparison notEqual(Object) @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression notEqual(reflect.Field, Object) @@ -1887,9 +1887,9 @@ public final class net.corda.core.node.services.vault.Builder extends java.lang. @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression notNull(kotlin.reflect.KProperty1) @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression predicate(reflect.Field, net.corda.core.node.services.vault.ColumnPredicate) @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$ColumnPredicateExpression predicate(kotlin.reflect.KProperty1, net.corda.core.node.services.vault.ColumnPredicate) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression sum(reflect.Field) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression sum(reflect.Field, List) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression sum(reflect.Field, List, net.corda.core.node.services.vault.Sort$Direction) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression sum(reflect.Field) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression sum(reflect.Field, List) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression sum(reflect.Field, List, net.corda.core.node.services.vault.Sort$Direction) @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression$AggregateFunctionExpression sum(kotlin.reflect.KProperty1, List, net.corda.core.node.services.vault.Sort$Direction) public static final net.corda.core.node.services.vault.Builder INSTANCE ## @@ -2082,14 +2082,14 @@ public abstract static class net.corda.core.node.services.vault.QueryCriteria$Co @org.jetbrains.annotations.NotNull public Collection visit(net.corda.core.node.services.vault.IQueryCriteriaParser) ## public static final class net.corda.core.node.services.vault.QueryCriteria$FungibleAssetQueryCriteria extends net.corda.core.node.services.vault.QueryCriteria$CommonQueryCriteria - @kotlin.jvm.JvmOverloads public () - @kotlin.jvm.JvmOverloads public (List) - @kotlin.jvm.JvmOverloads public (List, List) - @kotlin.jvm.JvmOverloads public (List, List, net.corda.core.node.services.vault.ColumnPredicate) - @kotlin.jvm.JvmOverloads public (List, List, net.corda.core.node.services.vault.ColumnPredicate, List) - @kotlin.jvm.JvmOverloads public (List, List, net.corda.core.node.services.vault.ColumnPredicate, List, List) - @kotlin.jvm.JvmOverloads public (List, List, net.corda.core.node.services.vault.ColumnPredicate, List, List, net.corda.core.node.services.Vault$StateStatus) - @kotlin.jvm.JvmOverloads public (List, List, net.corda.core.node.services.vault.ColumnPredicate, List, List, net.corda.core.node.services.Vault$StateStatus, Set) + public () + public (List) + public (List, List) + public (List, List, net.corda.core.node.services.vault.ColumnPredicate) + public (List, List, net.corda.core.node.services.vault.ColumnPredicate, List) + public (List, List, net.corda.core.node.services.vault.ColumnPredicate, List, List) + public (List, List, net.corda.core.node.services.vault.ColumnPredicate, List, List, net.corda.core.node.services.Vault$StateStatus) + public (List, List, net.corda.core.node.services.vault.ColumnPredicate, List, List, net.corda.core.node.services.Vault$StateStatus, Set) @org.jetbrains.annotations.Nullable public final List component1() @org.jetbrains.annotations.Nullable public final List component2() @org.jetbrains.annotations.Nullable public final net.corda.core.node.services.vault.ColumnPredicate component3() @@ -2111,12 +2111,12 @@ public static final class net.corda.core.node.services.vault.QueryCriteria$Fungi @org.jetbrains.annotations.NotNull public Collection visit(net.corda.core.node.services.vault.IQueryCriteriaParser) ## public static final class net.corda.core.node.services.vault.QueryCriteria$LinearStateQueryCriteria extends net.corda.core.node.services.vault.QueryCriteria$CommonQueryCriteria - @kotlin.jvm.JvmOverloads public () - @kotlin.jvm.JvmOverloads public (List) - @kotlin.jvm.JvmOverloads public (List, List) - @kotlin.jvm.JvmOverloads public (List, List, List) - @kotlin.jvm.JvmOverloads public (List, List, List, net.corda.core.node.services.Vault$StateStatus) - @kotlin.jvm.JvmOverloads public (List, List, List, net.corda.core.node.services.Vault$StateStatus, Set) + public () + public (List) + public (List, List) + public (List, List, List) + public (List, List, List, net.corda.core.node.services.Vault$StateStatus) + public (List, List, List, net.corda.core.node.services.Vault$StateStatus, Set) public (List, List, net.corda.core.node.services.Vault$StateStatus, Set) @org.jetbrains.annotations.Nullable public final List component1() @org.jetbrains.annotations.Nullable public final List component2() @@ -2167,9 +2167,9 @@ public static final class net.corda.core.node.services.vault.QueryCriteria$TimeI public static net.corda.core.node.services.vault.QueryCriteria$TimeInstantType[] values() ## public static final class net.corda.core.node.services.vault.QueryCriteria$VaultCustomQueryCriteria extends net.corda.core.node.services.vault.QueryCriteria$CommonQueryCriteria - @kotlin.jvm.JvmOverloads public (net.corda.core.node.services.vault.CriteriaExpression) - @kotlin.jvm.JvmOverloads public (net.corda.core.node.services.vault.CriteriaExpression, net.corda.core.node.services.Vault$StateStatus) - @kotlin.jvm.JvmOverloads public (net.corda.core.node.services.vault.CriteriaExpression, net.corda.core.node.services.Vault$StateStatus, Set) + public (net.corda.core.node.services.vault.CriteriaExpression) + public (net.corda.core.node.services.vault.CriteriaExpression, net.corda.core.node.services.Vault$StateStatus) + public (net.corda.core.node.services.vault.CriteriaExpression, net.corda.core.node.services.Vault$StateStatus, Set) @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.CriteriaExpression component1() @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$StateStatus component2() @org.jetbrains.annotations.Nullable public final Set component3() @@ -2183,13 +2183,13 @@ public static final class net.corda.core.node.services.vault.QueryCriteria$Vault @org.jetbrains.annotations.NotNull public Collection visit(net.corda.core.node.services.vault.IQueryCriteriaParser) ## public static final class net.corda.core.node.services.vault.QueryCriteria$VaultQueryCriteria extends net.corda.core.node.services.vault.QueryCriteria$CommonQueryCriteria - @kotlin.jvm.JvmOverloads public () - @kotlin.jvm.JvmOverloads public (net.corda.core.node.services.Vault$StateStatus) - @kotlin.jvm.JvmOverloads public (net.corda.core.node.services.Vault$StateStatus, Set) - @kotlin.jvm.JvmOverloads public (net.corda.core.node.services.Vault$StateStatus, Set, List) - @kotlin.jvm.JvmOverloads public (net.corda.core.node.services.Vault$StateStatus, Set, List, List) - @kotlin.jvm.JvmOverloads public (net.corda.core.node.services.Vault$StateStatus, Set, List, List, net.corda.core.node.services.vault.QueryCriteria$SoftLockingCondition) - @kotlin.jvm.JvmOverloads public (net.corda.core.node.services.Vault$StateStatus, Set, List, List, net.corda.core.node.services.vault.QueryCriteria$SoftLockingCondition, net.corda.core.node.services.vault.QueryCriteria$TimeCondition) + public () + public (net.corda.core.node.services.Vault$StateStatus) + public (net.corda.core.node.services.Vault$StateStatus, Set) + public (net.corda.core.node.services.Vault$StateStatus, Set, List) + public (net.corda.core.node.services.Vault$StateStatus, Set, List, List) + public (net.corda.core.node.services.Vault$StateStatus, Set, List, List, net.corda.core.node.services.vault.QueryCriteria$SoftLockingCondition) + public (net.corda.core.node.services.Vault$StateStatus, Set, List, List, net.corda.core.node.services.vault.QueryCriteria$SoftLockingCondition, net.corda.core.node.services.vault.QueryCriteria$TimeCondition) @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.Vault$StateStatus component1() @org.jetbrains.annotations.Nullable public final Set component2() @org.jetbrains.annotations.Nullable public final List component3() @@ -2697,11 +2697,11 @@ public final class net.corda.core.transactions.SignedTransaction extends java.la @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction plus(Collection) @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction plus(net.corda.core.crypto.TransactionSignature) @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeLedgerTransaction resolveNotaryChangeTransaction(net.corda.core.node.ServiceHub) - @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServiceHub) - @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServiceHub, boolean) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServiceHub) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServiceHub, boolean) @org.jetbrains.annotations.NotNull public String toString() - @kotlin.jvm.JvmOverloads public final void verify(net.corda.core.node.ServiceHub) - @kotlin.jvm.JvmOverloads public final void verify(net.corda.core.node.ServiceHub, boolean) + public final void verify(net.corda.core.node.ServiceHub) + public final void verify(net.corda.core.node.ServiceHub, boolean) public void verifyRequiredSignatures() @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction withAdditionalSignature(java.security.KeyPair, net.corda.core.crypto.SignatureMetadata) @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction withAdditionalSignature(net.corda.core.crypto.TransactionSignature) @@ -2728,12 +2728,12 @@ public class net.corda.core.transactions.TransactionBuilder extends java.lang.Ob @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addCommand(net.corda.core.contracts.Command) @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addCommand(net.corda.core.contracts.CommandData, List) @org.jetbrains.annotations.NotNull public net.corda.core.transactions.TransactionBuilder addInputState(net.corda.core.contracts.StateAndRef) - @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.ContractState, String) - @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.ContractState, String, net.corda.core.contracts.AttachmentConstraint) - @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party) - @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party, Integer) - @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party, Integer, net.corda.core.contracts.AttachmentConstraint) - @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.TransactionState) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.ContractState, String) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.ContractState, String, net.corda.core.contracts.AttachmentConstraint) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party, Integer) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.ContractState, String, net.corda.core.identity.Party, Integer, net.corda.core.contracts.AttachmentConstraint) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder addOutputState(net.corda.core.contracts.TransactionState) @org.jetbrains.annotations.NotNull public final List attachments() @org.jetbrains.annotations.NotNull public final List commands() @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionBuilder copy() @@ -2810,9 +2810,9 @@ public abstract class net.corda.core.utilities.ByteSequence extends java.lang.Ob public abstract int getOffset() public abstract int getSize() public int hashCode() - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.ByteSequence of(byte[]) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.ByteSequence of(byte[], int) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.ByteSequence of(byte[], int, int) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.ByteSequence of(byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.ByteSequence of(byte[], int) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.ByteSequence of(byte[], int, int) @org.jetbrains.annotations.NotNull public final java.io.ByteArrayInputStream open() @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence subSequence(int, int) @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence take(int) @@ -2820,9 +2820,9 @@ public abstract class net.corda.core.utilities.ByteSequence extends java.lang.Ob public static final net.corda.core.utilities.ByteSequence$Companion Companion ## public static final class net.corda.core.utilities.ByteSequence$Companion extends java.lang.Object - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence of(byte[]) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence of(byte[], int) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence of(byte[], int, int) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence of(byte[]) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence of(byte[], int) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence of(byte[], int, int) ## public final class net.corda.core.utilities.EncodingUtils extends java.lang.Object @org.jetbrains.annotations.NotNull public static final byte[] base58ToByteArray(String) @@ -3048,14 +3048,14 @@ public interface net.corda.core.utilities.VariablePropertyDelegate extends net.c public abstract void setValue(Object, kotlin.reflect.KProperty, Object) ## public final class net.corda.client.jackson.JacksonSupport extends java.lang.Object - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createDefaultMapper(net.corda.core.messaging.CordaRPCOps) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createDefaultMapper(net.corda.core.messaging.CordaRPCOps, com.fasterxml.jackson.core.JsonFactory) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createDefaultMapper(net.corda.core.messaging.CordaRPCOps, com.fasterxml.jackson.core.JsonFactory, boolean) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createInMemoryMapper(net.corda.core.node.services.IdentityService) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createInMemoryMapper(net.corda.core.node.services.IdentityService, com.fasterxml.jackson.core.JsonFactory) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createInMemoryMapper(net.corda.core.node.services.IdentityService, com.fasterxml.jackson.core.JsonFactory, boolean) - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createNonRpcMapper() - @kotlin.jvm.JvmStatic @kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createNonRpcMapper(com.fasterxml.jackson.core.JsonFactory) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createDefaultMapper(net.corda.core.messaging.CordaRPCOps) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createDefaultMapper(net.corda.core.messaging.CordaRPCOps, com.fasterxml.jackson.core.JsonFactory) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createDefaultMapper(net.corda.core.messaging.CordaRPCOps, com.fasterxml.jackson.core.JsonFactory, boolean) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createInMemoryMapper(net.corda.core.node.services.IdentityService) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createInMemoryMapper(net.corda.core.node.services.IdentityService, com.fasterxml.jackson.core.JsonFactory) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createInMemoryMapper(net.corda.core.node.services.IdentityService, com.fasterxml.jackson.core.JsonFactory, boolean) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createNonRpcMapper() + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final com.fasterxml.jackson.databind.ObjectMapper createNonRpcMapper(com.fasterxml.jackson.core.JsonFactory) @org.jetbrains.annotations.NotNull public final com.fasterxml.jackson.databind.Module getCordaModule() public static final net.corda.client.jackson.JacksonSupport INSTANCE ## @@ -3174,8 +3174,8 @@ public abstract static class net.corda.client.jackson.JacksonSupport$WireTransac @com.fasterxml.jackson.annotation.JsonIgnore @org.jetbrains.annotations.NotNull public abstract List getOutputStates() ## public class net.corda.client.jackson.StringToMethodCallParser extends java.lang.Object - @kotlin.jvm.JvmOverloads public (Class) - @kotlin.jvm.JvmOverloads public (Class, com.fasterxml.jackson.databind.ObjectMapper) + public (Class) + public (Class, com.fasterxml.jackson.databind.ObjectMapper) public (kotlin.reflect.KClass) @org.jetbrains.annotations.NotNull public final Map getAvailableCommands() @org.jetbrains.annotations.NotNull protected final com.google.common.collect.Multimap getMethodMap() @@ -3216,8 +3216,8 @@ public static final class net.corda.client.jackson.StringToMethodCallParser$Unpa @org.jetbrains.annotations.NotNull public final String getMethodName() ## public final class net.corda.client.rpc.CordaRPCClient extends java.lang.Object - @kotlin.jvm.JvmOverloads public (net.corda.core.utilities.NetworkHostAndPort) - @kotlin.jvm.JvmOverloads public (net.corda.core.utilities.NetworkHostAndPort, net.corda.client.rpc.CordaRPCClientConfiguration) + public (net.corda.core.utilities.NetworkHostAndPort) + public (net.corda.core.utilities.NetworkHostAndPort, net.corda.client.rpc.CordaRPCClientConfiguration) @org.jetbrains.annotations.NotNull public final net.corda.client.rpc.CordaRPCConnection start(String, String) public final Object use(String, String, kotlin.jvm.functions.Function1) ## diff --git a/constants.properties b/constants.properties index 286d4dbc1c..1965e53715 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=1.1.0 +gradlePluginsVersion=1.1.1 kotlinVersion=1.1.50 guavaVersion=21.0 bouncycastleVersion=1.57 diff --git a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java index 53f7ee9fc0..6e10ec1fe3 100644 --- a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java +++ b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java @@ -36,6 +36,13 @@ public class ScanApi extends DefaultTask { private static final int FIELD_MASK = Modifier.fieldModifiers(); private static final int VISIBILITY_MASK = Modifier.PUBLIC | Modifier.PROTECTED; + private static final Set ANNOTATION_BLACKLIST; + static { + Set blacklist = new LinkedHashSet<>(); + blacklist.add("kotlin.jvm.JvmOverloads"); + ANNOTATION_BLACKLIST = unmodifiableSet(blacklist); + } + /** * This information has been lifted from: * @link Metadata.kt @@ -251,7 +258,7 @@ public class ScanApi extends DefaultTask { if (isVisible(method.getAccessFlags()) // Only public and protected methods && isValid(method.getAccessFlags(), METHOD_MASK) // Excludes bridge and synthetic methods && !isKotlinInternalScope(method)) { - writer.append(" ").println(method); + writer.append(" ").println(filterAnnotationsFor(method)); } } } @@ -278,6 +285,22 @@ public class ScanApi extends DefaultTask { } return 0; } + + private String filterAnnotationsFor(MethodInfo method) { + return new MethodInfo( + method.getClassName(), + method.getMethodName(), + method.getAccessFlags(), + method.getTypeDescriptor(), + method.getAnnotationNames().stream() + .filter(ScanApi::isVisibleAnnotation) + .collect(toList()) + ).toString(); + } + } + + private static boolean isVisibleAnnotation(String annotationName) { + return !ANNOTATION_BLACKLIST.contains(annotationName); } private static boolean isKotlinInternalScope(MethodInfo method) { From 3f6ce3c861063c9755c4d40c023290b5466079fe Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Fri, 6 Oct 2017 14:33:24 +0100 Subject: [PATCH 102/185] Removes broken link. --- docs/source/flow-testing.rst | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/source/flow-testing.rst b/docs/source/flow-testing.rst index 939da0769e..967dbe9877 100644 --- a/docs/source/flow-testing.rst +++ b/docs/source/flow-testing.rst @@ -63,8 +63,4 @@ transaction as shown here. With regards to initiated flows (see :doc:`flow-state-machines` for information on initiated and initiating flows), the full node automatically registers them by scanning the CorDapp jars. In a unit test environment this is not possible so -``MockNode`` has the ``registerInitiatedFlow`` method to manually register an initiated flow. - -And that's it: you can explore the documentation for the -`MockNetwork API `_ -here. +``MockNode`` has the ``registerInitiatedFlow`` method to manually register an initiated flow. \ No newline at end of file From 3afe85504216f24348e4330be57dfe73bbd19124 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Fri, 6 Oct 2017 14:50:54 +0100 Subject: [PATCH 103/185] Update to match MockNetwork API change (#1831) --- .../src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt b/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt index 579b865c1f..bbe685017a 100644 --- a/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt @@ -89,7 +89,7 @@ class CordaServiceTest { notaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, advertisedServices = *arrayOf(ServiceInfo(ValidatingNotaryService.type))) - nodeA = mockNet.createNode(notaryNode.network.myAddress) + nodeA = mockNet.createNode() mockNet.startNodes() } From d761d52c16520bd1131cf692f423ed9fa88d5c48 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Fri, 6 Oct 2017 14:55:30 +0100 Subject: [PATCH 104/185] Fix filterAnnotationsFor() to return MethodInfo. --- .../api-scanner/src/main/java/net/corda/plugins/ScanApi.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java index 6e10ec1fe3..f0220add42 100644 --- a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java +++ b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java @@ -286,7 +286,7 @@ public class ScanApi extends DefaultTask { return 0; } - private String filterAnnotationsFor(MethodInfo method) { + private MethodInfo filterAnnotationsFor(MethodInfo method) { return new MethodInfo( method.getClassName(), method.getMethodName(), @@ -295,7 +295,7 @@ public class ScanApi extends DefaultTask { method.getAnnotationNames().stream() .filter(ScanApi::isVisibleAnnotation) .collect(toList()) - ).toString(); + ); } } From 6562579e8e9ae362ab458e02c9e1f63b570472e6 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Fri, 6 Oct 2017 15:23:20 +0100 Subject: [PATCH 105/185] Extend NetworkMapCache API (#1766) * Add functions to: ** Return PartyAndCertificate rather than just Party ** Return all NodeInfo entries for a name (rather than just by key) * General documentation improvements --- .../core/node/services/IdentityService.kt | 31 ++++++++++------- .../core/node/services/NetworkMapCache.kt | 33 ++++++++++++++----- .../network/PersistentNetworkMapCache.kt | 19 ++++++++++- .../services/network/NetworkMapCacheTest.kt | 12 +++++++ 4 files changed, 74 insertions(+), 21 deletions(-) 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 645afb5550..489e316e01 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 @@ -10,7 +10,11 @@ import java.security.cert.* /** * An identity service maintains a directory of parties by their associated distinguished name/public keys and thus * supports lookup of a party given its key, or name. The service also manages the certificates linking confidential - * identities back to the well known identity (i.e. the identity in the network map) of a party. + * identities back to the well known identity. + * + * Well known identities in Corda are the public identity of a party, registered with the network map directory, + * whereas confidential identities are distributed only on a need to know basis (typically between parties in + * a transaction being built). See [NetworkMapCache] for retrieving well known identities from the network map. */ interface IdentityService { val trustRoot: X509Certificate @@ -43,8 +47,9 @@ interface IdentityService { fun getAllIdentities(): Iterable /** - * Get the certificate and path for a known identity's owning key. + * Resolves a public key to the well known identity [PartyAndCertificate] instance which is owned by the key. * + * @param owningKey The [PublicKey] to determine well known identity for. * @return the party and certificate, or null if unknown. */ fun certificateFromKey(owningKey: PublicKey): PartyAndCertificate? @@ -59,17 +64,18 @@ interface IdentityService { fun partyFromKey(key: PublicKey): Party? /** - * Resolves a party name to the well known identity [Party] instance for this name. - * @param name The [CordaX500Name] to search for. + * Resolves a party name to the well known identity [Party] instance for this name. Where possible well known identity + * lookup from name should be done from the network map (via [NetworkMapCache]) instead, as it is the authoritative + * source of well known identities. + * + * @param name The [CordaX500Name] to determine well known identity for. * @return If known the canonical [Party] with that name, else null. */ fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? /** - * Returns the well known identity from an [AbstractParty]. This is intended to resolve the well known identity, - * as visible in the [NetworkMapCache] from a confidential identity. - * It transparently handles returning the well known identity back if - * a well known identity is passed in. + * Resolves a (optionally) confidential identity to the corresponding well known identity [Party]. + * It transparently handles returning the well known identity back if a well known identity is passed in. * * @param party identity to determine well known identity for. * @return well known identity, if found. @@ -77,11 +83,12 @@ interface IdentityService { fun wellKnownPartyFromAnonymous(party: AbstractParty): Party? /** - * Returns the well known identity from a PartyAndReference. This is intended to resolve the well known identity, - * as visible in the [NetworkMapCache] from a confidential identity. - * It transparently handles returning the well known identity back if - * a well known identity is passed in. + * Resolves a (optionally) confidential identity to the corresponding well known identity [Party]. + * Convenience method which unwraps the [Party] from the [PartyAndReference] and then resolves the + * well known identity as normal. + * It transparently handles returning the well known identity back if a well known identity is passed in. * + * @param partyRef identity (and reference, which is unused) to determine well known identity for. * @return the well known identity, or null if unknown. */ fun wellKnownPartyFromAnonymous(partyRef: PartyAndReference) = wellKnownPartyFromAnonymous(partyRef.party) diff --git a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt index 2bb1dc0bb4..8217eb69b4 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt @@ -4,6 +4,7 @@ import net.corda.core.concurrent.CordaFuture 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.messaging.DataFeed import net.corda.core.node.NodeInfo import net.corda.core.serialization.CordaSerializable @@ -72,26 +73,42 @@ interface NetworkMapCache { /** Look up the node info for a host and port. */ fun getNodeByAddress(address: NetworkHostAndPort): NodeInfo? - fun getPeerByLegalName(name: CordaX500Name): Party? = getNodeByLegalName(name)?.let { - it.legalIdentitiesAndCerts.singleOrNull { it.name == name }?.party - } + /** + * Look up a well known identity (including certificate path) of a legal name. This should be used in preference + * to well known identity lookup in the identity service where possible, as the network map is the authoritative + * source of well known identities. + */ + fun getPeerCertificateByLegalName(name: CordaX500Name): PartyAndCertificate? + + /** + * Look up the well known identity of a legal name. This should be used in preference + * to well known identity lookup in the identity service where possible, as the network map is the authoritative + * source of well known identities. + */ + fun getPeerByLegalName(name: CordaX500Name): Party? = getPeerCertificateByLegalName(name)?.party /** Return all [NodeInfo]s the node currently is aware of (including ourselves). */ val allNodes: List /** - * Look up the node infos for a specific peer key. - * In general, nodes can advertise multiple identities: a legal identity, and separate identities for each of - * the services it provides. In case of a distributed service – run by multiple nodes – each participant advertises - * the identity of the *whole group*. + * Look up the node information entries for a specific identity key. + * Note that normally there will be only one node for a key, but for clusters of nodes or distributed services there + * can be multiple nodes. */ fun getNodesByLegalIdentityKey(identityKey: PublicKey): List + /** + * Look up the node information entries for a legal name. + * Note that normally there will be only one node for a legal name, but for clusters of nodes or distributed services there + * can be multiple nodes. + */ + fun getNodesByLegalName(name: CordaX500Name): List + /** Returns information about the party, which may be a specific node or a service */ fun getPartyInfo(party: Party): PartyInfo? // DOCSTART 2 - /** Gets a notary identity by the given name. */ + /** Look up a well known identity of notary by legal name. */ fun getNotary(name: CordaX500Name): Party? = notaryIdentities.firstOrNull { it.name == name } // DOCEND 2 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 b2fb8c3bde..8b27a9a396 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 @@ -4,6 +4,7 @@ import net.corda.core.concurrent.CordaFuture 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.VisibleForTesting import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.internal.concurrent.map @@ -114,7 +115,8 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) return null } - override fun getNodeByLegalName(name: CordaX500Name): NodeInfo? = serviceHub.database.transaction { queryByLegalName(name).firstOrNull() } + override fun getNodeByLegalName(name: CordaX500Name): NodeInfo? = getNodesByLegalName(name).firstOrNull() + override fun getNodesByLegalName(name: CordaX500Name): List = serviceHub.database.transaction { queryByLegalName(name) } override fun getNodesByLegalIdentityKey(identityKey: PublicKey): List = serviceHub.database.transaction { queryByIdentityKey(identityKey) } override fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? { @@ -126,6 +128,8 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) override fun getNodeByAddress(address: NetworkHostAndPort): NodeInfo? = serviceHub.database.transaction { queryByAddress(address) } + override fun getPeerCertificateByLegalName(name: CordaX500Name): PartyAndCertificate? = serviceHub.database.transaction { queryIdentityByLegalName(name) } + override fun track(): DataFeed, MapChange> { synchronized(_changed) { return DataFeed(partyNodes, _changed.bufferUntilSubscribed().wrapWithDatabaseTransaction()) @@ -329,6 +333,19 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) } } + private fun queryIdentityByLegalName(name: CordaX500Name): PartyAndCertificate? { + createSession { + val query = it.createQuery( + // We do the JOIN here to restrict results to those present in the network map + "SELECT DISTINCT l FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.name = :name", + NodeInfoSchemaV1.DBPartyAndCertificate::class.java) + query.setParameter("name", name.toString()) + val candidates = query.resultList.map { it.toLegalIdentityAndCert() } + // The map is restricted to holding a single identity for any X.500 name, so firstOrNull() is correct here. + return candidates.firstOrNull() + } + } + private fun queryByLegalName(name: CordaX500Name): List { createSession { val query = it.createQuery( 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 d097f8d614..10aa2c62ac 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 @@ -65,6 +65,18 @@ class NetworkMapCacheTest { // TODO: Should have a test case with anonymous lookup } + @Test + fun `getPeerByLegalName`() { + val notaryNode = mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(ALICE.name) + val notaryCache: NetworkMapCache = notaryNode.services.networkMapCache + val expected = aliceNode.info.legalIdentities.single() + + mockNet.runNetwork() + val actual = notaryNode.database.transaction { notaryCache.getPeerByLegalName(ALICE.name) } + assertEquals(expected, actual) + } + @Test fun `remove node from cache`() { val notaryNode = mockNet.createNotaryNode() From 0b35a99c5a6c0498e577fc1bfbb16bf3a84c0473 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Fri, 6 Oct 2017 15:37:33 +0100 Subject: [PATCH 106/185] [CORDA-682]: Claim Jigsaw module names (#1824) [CORDA-682] Specify Jigsaw module names for all JAR projects. --- build.gradle | 4 +- client/jackson/build.gradle | 3 + client/jfx/build.gradle | 3 + client/mock/build.gradle | 3 + client/rpc/build.gradle | 3 + docs/source/api-index.rst | 64 ---------------- docs/source/building-a-cordapp-index.rst | 1 - docs/source/corda-modules.rst | 93 ++++++++++++++++++++++++ docs/source/getting-set-up.rst | 2 +- docs/source/other-index.rst | 1 + docs/source/release-notes.rst | 2 +- experimental/sandbox/build.gradle | 5 +- samples/attachment-demo/build.gradle | 8 ++ samples/bank-of-corda-demo/build.gradle | 8 ++ samples/irs-demo/build.gradle | 5 ++ samples/network-visualiser/build.gradle | 8 ++ samples/notary-demo/build.gradle | 8 ++ samples/simm-valuation-demo/build.gradle | 8 ++ samples/trader-demo/build.gradle | 8 ++ tools/demobench/build.gradle | 3 +- tools/explorer/build.gradle | 8 ++ tools/graphs/build.gradle | 8 ++ tools/loadtest/build.gradle | 8 ++ 23 files changed, 194 insertions(+), 70 deletions(-) delete mode 100644 docs/source/api-index.rst create mode 100644 docs/source/corda-modules.rst diff --git a/build.gradle b/build.gradle index acee113b33..6cfd34fed9 100644 --- a/build.gradle +++ b/build.gradle @@ -116,12 +116,14 @@ allprojects { } } - tasks.withType(Jar) { // Includes War and Ear + tasks.withType(Jar) { task -> + // Includes War and Ear manifest { attributes('Corda-Release-Version': corda_release_version) attributes('Corda-Platform-Version': corda_platform_version) attributes('Corda-Revision': corda_revision) attributes('Corda-Vendor': 'Corda Open Source') + attributes('Automatic-Module-Name': "net.corda.${task.project.name.replaceAll('-', '.')}") } } diff --git a/client/jackson/build.gradle b/client/jackson/build.gradle index bcff947dfb..fadfdf9975 100644 --- a/client/jackson/build.gradle +++ b/client/jackson/build.gradle @@ -25,6 +25,9 @@ dependencies { jar { baseName 'corda-jackson' + manifest { + attributes 'Automatic-Module-Name': 'net.corda.client.jackson' + } } publish { diff --git a/client/jfx/build.gradle b/client/jfx/build.gradle index 867ba2b9f0..aaf1d5fd14 100644 --- a/client/jfx/build.gradle +++ b/client/jfx/build.gradle @@ -57,6 +57,9 @@ task integrationTest(type: Test) { jar { baseName 'corda-jfx' + manifest { + attributes 'Automatic-Module-Name': 'net.corda.client.jfx' + } } publish { diff --git a/client/mock/build.gradle b/client/mock/build.gradle index 1080cf9b7a..b492455b49 100644 --- a/client/mock/build.gradle +++ b/client/mock/build.gradle @@ -21,6 +21,9 @@ dependencies { jar { baseName 'corda-mock' + manifest { + attributes 'Automatic-Module-Name': 'net.corda.client.mock' + } } publish { diff --git a/client/rpc/build.gradle b/client/rpc/build.gradle index d8c61b4a99..bd234e8ee1 100644 --- a/client/rpc/build.gradle +++ b/client/rpc/build.gradle @@ -90,6 +90,9 @@ task smokeTest(type: Test) { jar { baseName 'corda-rpc' + manifest { + attributes 'Automatic-Module-Name': 'net.corda.client.rpc' + } } publish { diff --git a/docs/source/api-index.rst b/docs/source/api-index.rst deleted file mode 100644 index c3486e4cea..0000000000 --- a/docs/source/api-index.rst +++ /dev/null @@ -1,64 +0,0 @@ -API -=== - -This section describes the APIs that are available for the development of CorDapps: - -.. toctree:: - :maxdepth: 1 - - api-states - api-persistence - api-contracts - api-vault-query - api-transactions - api-flows - api-identity - api-service-hub - api-rpc - api-core-types - -Before reading this page, you should be familiar with the :doc:`key concepts of Corda `. - -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. - -As of Corda 1.0, the following modules export public API that we promise to maintain backwards compatibility for, -unless an incompatible change is required for security reasons: - -* core -* client-rpc -* client-jackson - -The following modules don't yet have a completely stable API, but we will do our best to minimise disruption to -developers using them until we are able to graduate them into the public API: - -* the Gradle plugins (cordformation) -* node-driver -* confidential-identities -* test-utils -* client-jfx, client-mock -* finance -* anything under the experimental directory (sub-components here may never graduate) - -We hope to graduate the node-driver, test-utils and confidential-identities modules in the next feature release -after 1.0. The bulk of the Corda API is found in the core module. Other modules should be assumed to be fully internal. - -The web server module will be removed in future: you should build web front-ends for CorDapps using standard frameworks -like Spring Boot, J2EE, Play, etc. - -Code that falls into the following packages namespaces are for internal use only and not for public use: - -* Any package in the ``net.corda`` namespace which contains ``.internal`` -* ``net.corda.node`` - -In the future releases the node upon starting up will reject any CorDapps which uses classes from these packages. \ No newline at end of file diff --git a/docs/source/building-a-cordapp-index.rst b/docs/source/building-a-cordapp-index.rst index e073aef623..12466338ee 100644 --- a/docs/source/building-a-cordapp-index.rst +++ b/docs/source/building-a-cordapp-index.rst @@ -7,6 +7,5 @@ Building a CorDapp cordapp-overview writing-cordapps cordapp-build-systems - api-index flow-cookbook cheat-sheet \ No newline at end of file diff --git a/docs/source/corda-modules.rst b/docs/source/corda-modules.rst new file mode 100644 index 0000000000..0307db092c --- /dev/null +++ b/docs/source/corda-modules.rst @@ -0,0 +1,93 @@ +Corda modules +============= + +This section describes the APIs that are available for the development of CorDapps: + +.. toctree:: + :maxdepth: 1 + + api-states + api-persistence + api-contracts + api-vault-query + api-transactions + api-flows + api-identity + api-service-hub + api-rpc + api-core-types + +Before reading this page, you should be familiar with the :doc:`key concepts of Corda `. + +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 stable modules +-------------------- + +The following modules have a stable API we commit not to break in following releases, unless an incompatible change is required for security reasons: + +* **Core (net.corda.core)**: core Corda libraries such as crypto functions, types for Corda's building blocks: states, contracts, transactions, attachments, etc. and some interfaces for nodes and protocols +* **Client RPC (net.corda.client.rpc)**: client RPC +* **Client Jackson (net.corda.client.jackson)**: JSON support for client applications + +Corda incubating modules +------------------------ + +The following modules don't yet have a completely stable API, but we will do our best to minimise disruption to +developers using them until we are able to graduate them into the public API: + +* **net.corda.node.driver**: test utilities to run nodes programmatically +* **net.corda.confidential.identities**: experimental support for confidential identities on the ledger +* **net.corda.node.test.utils**: generic test utilities +* **net.corda.finance**: a range of elementary contracts (and associated schemas) and protocols, such as abstract fungible assets, cash, obligation and commercial paper +* **net.corda.client.jfx**: support for Java FX UI +* **net.corda.client.mock**: client mock utilities +* **Cordformation**: Gradle integration plugins + +Corda unstable modules +---------------------- + +The following modules are available but we do not commit to their stability or continuation in any sense: + +* **net.corda.buildSrc**: necessary gradle plugins to build Corda +* **net.corda.node**: core code of the Corda node (eg: node driver, node services, messaging, persistence) +* **net.corda.node.api**: data structures shared between the node and the client module, e.g. types sent via RPC +* **net.corda.samples.network.visualiser**: a network visualiser that uses a simulation to visualise the interaction and messages between nodes on the Corda network +* **net.corda.samples.demos.attachment**: demonstrates sending a transaction with an attachment from one to node to another, and the receiving node accessing the attachment +* **net.corda.samples.demos.bankofcorda**: simulates the role of an asset issuing authority (eg. central bank for cash) +* **net.corda.samples.demos.irs**: demonstrates an Interest Rate Swap agreement between two banks +* **net.corda.samples.demos.notary**: a simple demonstration of a node getting multiple transactions notarised by a distributed (Raft or BFT SMaRt) notary +* **net.corda.samples.demos.simmvaluation**: See our [main documentation site](https://docs.corda.net/initial-margin-agreement.html) regarding the SIMM valuation and agreement on a distributed ledger +* **net.corda.samples.demos.trader**: demonstrates four nodes, a notary, an issuer of cash (Bank of Corda), and two parties trading with each other, exchanging cash for a commercial paper +* **net.corda.node.smoke.test.utils**: test utilities for smoke testing +* **net.corda.node.test.common**: common test functionality +* **net.corda.tools.demobench**: a GUI tool that allows to run Corda nodes locally for demonstrations +* **net.corda.tools.explorer**: a GUI front-end for Corda +* **net.corda.tools.graphs**: utilities to infer project dependencies +* **net.corda.tools.loadtest**: Corda load tests +* **net.corda.verifier**: allows out-of-node transaction verification, allowing verification to scale horizontally +* **net.corda.webserver**: is a servlet container for CorDapps that export HTTP endpoints. This server is an RPC client of the node +* **net.corda.sandbox-creator**: sandbox utilities +* **net.corda.quasar.hook**: agent to hook into Quasar and provide types exclusion lists + +.. warning:: Code inside any package in the ``net.corda`` namespace which contains ``.internal`` or in ``net.corda.node`` for internal use only. + Future releases will reject any CorDapps that use types from these packages. + +.. warning:: The web server module will be removed in future. You should call Corda nodes through RPC from your web server of choice e.g., Spring Boot, Vertx, Undertow. \ No newline at end of file diff --git a/docs/source/getting-set-up.rst b/docs/source/getting-set-up.rst index 95bb99bfc9..1fbe7060b3 100644 --- a/docs/source/getting-set-up.rst +++ b/docs/source/getting-set-up.rst @@ -169,7 +169,7 @@ The best way to check that everything is working fine is by taking a deeper look Next, you should read through :doc:`Corda Key Concepts ` to understand how Corda works. By then, you'll be ready to start writing your own CorDapps. Learn how to do this in the -:doc:`Hello, World tutorial `. You may want to refer to the :doc:`API docs ` along the +:doc:`Hello, World tutorial `. You may want to refer to the :doc:`Modules documentation ` along the way. If you encounter any issues, please see the :doc:`troubleshooting` page, or get in touch with us on the diff --git a/docs/source/other-index.rst b/docs/source/other-index.rst index 7f6fa6ef66..539608be94 100644 --- a/docs/source/other-index.rst +++ b/docs/source/other-index.rst @@ -7,5 +7,6 @@ Other json secure-coding-guidelines corda-repo-layout + corda-modules building-the-docs codestyle \ No newline at end of file diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 845933db05..3552fc329e 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -32,7 +32,7 @@ Our extensive testing frameworks will continue to evolve alongside future Corda we have introduced a new test node driver module to encapsulate all test functionality in support of building standalone node integration tests using our DSL driver. -Please read :doc:`api-index` for complete details. +Please read :doc:`corda-modules` for complete details. .. note:: it may be necessary to recompile applications against future versions of the API until we begin offering `ABI (Application Binary Interface) `_ stability as well. diff --git a/experimental/sandbox/build.gradle b/experimental/sandbox/build.gradle index 53e244815a..2e64b70651 100644 --- a/experimental/sandbox/build.gradle +++ b/experimental/sandbox/build.gradle @@ -30,7 +30,10 @@ task standaloneJar(type: Jar) { } with jar manifest { - attributes 'Main-Class': 'net.corda.sandbox.tools.SandboxCreator' + attributes( + 'Main-Class': 'net.corda.sandbox.tools.SandboxCreator', + 'Automatic-Module-Name': 'net.corda.sandbox.creator' + ) } archiveName "corda-sandbox-creator-${version}.jar" } diff --git a/samples/attachment-demo/build.gradle b/samples/attachment-demo/build.gradle index 170f9e265c..1ca4d85d3d 100644 --- a/samples/attachment-demo/build.gradle +++ b/samples/attachment-demo/build.gradle @@ -102,3 +102,11 @@ task runRecipient(type: JavaExec) { args '--role' args 'RECIPIENT' } + +jar { + manifest { + attributes( + 'Automatic-Module-Name': 'net.corda.samples.demos.attachment' + ) + } +} diff --git a/samples/bank-of-corda-demo/build.gradle b/samples/bank-of-corda-demo/build.gradle index 4da684ed47..dcdbe408d0 100644 --- a/samples/bank-of-corda-demo/build.gradle +++ b/samples/bank-of-corda-demo/build.gradle @@ -143,3 +143,11 @@ task runWebCashIssue(type: JavaExec) { args '--currency' args 'GBP' } + +jar { + manifest { + attributes( + 'Automatic-Module-Name': 'net.corda.samples.demos.bankofcorda' + ) + } +} diff --git a/samples/irs-demo/build.gradle b/samples/irs-demo/build.gradle index d1bd45a246..6d9b908823 100644 --- a/samples/irs-demo/build.gradle +++ b/samples/irs-demo/build.gradle @@ -117,4 +117,9 @@ publishing { jar { from sourceSets.test.output + manifest { + attributes( + 'Automatic-Module-Name': 'net.corda.samples.demos.irs' + ) + } } diff --git a/samples/network-visualiser/build.gradle b/samples/network-visualiser/build.gradle index bad1295dbc..d0ee4ea3e3 100644 --- a/samples/network-visualiser/build.gradle +++ b/samples/network-visualiser/build.gradle @@ -42,3 +42,11 @@ task deployVisualiser(type: FatCapsule) { javaAgents = [configurations.quasar.singleFile.name] } } + +jar { + manifest { + attributes( + 'Automatic-Module-Name': 'net.corda.samples.network.visualiser' + ) + } +} diff --git a/samples/notary-demo/build.gradle b/samples/notary-demo/build.gradle index b2d3f3e341..fbbaa6a9a7 100644 --- a/samples/notary-demo/build.gradle +++ b/samples/notary-demo/build.gradle @@ -66,3 +66,11 @@ task notarise(type: JavaExec) { classpath = sourceSets.main.runtimeClasspath main = 'net.corda.notarydemo.NotariseKt' } + +jar { + manifest { + attributes( + 'Automatic-Module-Name': 'net.corda.samples.demos.notary' + ) + } +} diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index 1d7d654ee8..278608bd77 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -117,3 +117,11 @@ publishing { } } } + +jar { + manifest { + attributes( + 'Automatic-Module-Name': 'net.corda.samples.demos.simmvaluation' + ) + } +} diff --git a/samples/trader-demo/build.gradle b/samples/trader-demo/build.gradle index 216405f66f..5d2a1750eb 100644 --- a/samples/trader-demo/build.gradle +++ b/samples/trader-demo/build.gradle @@ -121,3 +121,11 @@ task runSeller(type: JavaExec) { args '--role' args 'SELLER' } + +jar { + manifest { + attributes( + 'Automatic-Module-Name': 'net.corda.samples.demos.trader' + ) + } +} \ No newline at end of file diff --git a/tools/demobench/build.gradle b/tools/demobench/build.gradle index 6a3452b8c9..727a5c8237 100644 --- a/tools/demobench/build.gradle +++ b/tools/demobench/build.gradle @@ -78,7 +78,8 @@ jar { manifest { attributes( 'Main-Class': mainClassName, - 'Class-Path': configurations.runtime.collect { it.getName() }.join(' ') + 'Class-Path': configurations.runtime.collect { it.getName() }.join(' '), + 'Automatic-Module-Name': 'net.corda.tools.demobench' ) } } diff --git a/tools/explorer/build.gradle b/tools/explorer/build.gradle index 3ffc1e543c..9eddf905ca 100644 --- a/tools/explorer/build.gradle +++ b/tools/explorer/build.gradle @@ -65,3 +65,11 @@ task(runSimulationNodes, dependsOn: 'classes', type: JavaExec) { classpath = sourceSets.main.runtimeClasspath args '-S' } + +jar { + manifest { + attributes( + 'Automatic-Module-Name': 'net.corda.tools.explorer' + ) + } +} \ No newline at end of file diff --git a/tools/graphs/build.gradle b/tools/graphs/build.gradle index 5e81dee1c9..01a4da9637 100644 --- a/tools/graphs/build.gradle +++ b/tools/graphs/build.gradle @@ -79,3 +79,11 @@ task graphs { } } } + +jar { + manifest { + attributes( + 'Automatic-Module-Name': 'net.corda.tools.graphs' + ) + } +} \ No newline at end of file diff --git a/tools/loadtest/build.gradle b/tools/loadtest/build.gradle index af7012b2e6..00480419c0 100644 --- a/tools/loadtest/build.gradle +++ b/tools/loadtest/build.gradle @@ -35,3 +35,11 @@ run { systemProperty "consoleLogLevel", System.properties.getProperty('consoleLogLevel') } } + +jar { + manifest { + attributes( + 'Automatic-Module-Name': 'net.corda.tools.loadtest' + ) + } +} From 894f05d84e4185fab3d694a16e29f365a4810a1b Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Sat, 7 Oct 2017 10:46:20 +0100 Subject: [PATCH 107/185] CORDA-599 Remove dependency of NetworkMapService impls on ServiceHub (#1713) --- .../core/node/services/VaultQueryService.kt | 0 .../net/corda/core/node/ServiceInfoTests.kt | 0 .../kotlin/net/corda/flows/IssuerFlowTest.kt | 0 .../net/corda/node/internal/AbstractNode.kt | 27 +++++++++++-------- .../kotlin/net/corda/node/internal/Node.kt | 12 ++++++--- .../node/services/api/AbstractNodeService.kt | 7 ++--- .../node/services/config/NodeConfiguration.kt | 1 + .../network/InMemoryNetworkMapCache.kt | 0 .../services/network/NetworkMapService.kt | 26 +++++++----------- .../network/PersistentNetworkMapService.kt | 7 ++--- .../services/vault/HibernateVaultQueryImpl.kt | 0 .../network/AbstractNetworkMapServiceTest.kt | 6 ++--- .../PersistentNetworkMapServiceTest.kt | 13 +++++---- .../kotlin/net/corda/testing/node/MockNode.kt | 13 ++++----- 14 files changed, 60 insertions(+), 52 deletions(-) delete mode 100644 core/src/main/kotlin/net/corda/core/node/services/VaultQueryService.kt delete mode 100644 core/src/test/kotlin/net/corda/core/node/ServiceInfoTests.kt delete mode 100644 finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt delete mode 100644 node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt delete mode 100644 node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultQueryService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultQueryService.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/core/src/test/kotlin/net/corda/core/node/ServiceInfoTests.kt b/core/src/test/kotlin/net/corda/core/node/ServiceInfoTests.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt deleted file mode 100644 index e69de29bb2..0000000000 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 15ed414620..ba3f1d06b6 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -33,6 +33,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.debug +import net.corda.node.VersionInfo import net.corda.node.internal.classloading.requireAnnotation import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl @@ -95,10 +96,17 @@ import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair // TODO: Where this node is the initial network map service, currently no networkMapService is provided. // In theory the NodeInfo for the node should be passed in, instead, however currently this is constructed by the // AbstractNode. It should be possible to generate the NodeInfo outside of AbstractNode, so it can be passed in. -abstract class AbstractNode(open val configuration: NodeConfiguration, +abstract class AbstractNode(config: NodeConfiguration, val advertisedServices: Set, val platformClock: Clock, + protected val versionInfo: VersionInfo, @VisibleForTesting val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() { + open val configuration = config.apply { + require(minimumPlatformVersion <= versionInfo.platformVersion) { + "minimumPlatformVersion cannot be greater than the node's own version" + } + } + private class StartedNodeImpl( override val internals: N, override val services: ServiceHubInternalImpl, @@ -119,7 +127,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, protected abstract val log: Logger protected abstract val networkMapAddress: SingleMessageRecipient? - protected abstract val platformVersion: Int // We will run as much stuff in this single thread as possible to keep the risk of thread safety bugs low during the // low-performance prototyping period. @@ -452,13 +459,13 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, legalIdentity = obtainIdentity() network = makeMessagingService(legalIdentity) info = makeInfo(legalIdentity) - + val networkMapCache = services.networkMapCache val tokenizableServices = mutableListOf(attachments, network, services.vaultService, services.keyManagementService, services.identityService, platformClock, services.schedulerService, - services.auditService, services.monitoringService, services.networkMapCache, services.schemaService, + services.auditService, services.monitoringService, networkMapCache, services.schemaService, services.transactionVerifierService, services.validatedTransactions, services.contractUpgradeService, services, cordappProvider, this) - makeNetworkServices(tokenizableServices) + makeNetworkServices(network, networkMapCache, tokenizableServices) return tokenizableServices } @@ -489,7 +496,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val allIdentitiesList = mutableListOf(legalIdentity) myNotaryIdentity?.let { allIdentitiesList.add(it) } val addresses = myAddresses() // TODO There is no support for multiple IP addresses yet. - return NodeInfo(addresses, allIdentitiesList, platformVersion, platformClock.instant().toEpochMilli()) + return NodeInfo(addresses, allIdentitiesList, versionInfo.platformVersion, platformClock.instant().toEpochMilli()) } /** @@ -550,9 +557,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } } - private fun makeNetworkServices(tokenizableServices: MutableList) { + private fun makeNetworkServices(network: MessagingService, networkMapCache: NetworkMapCacheInternal, tokenizableServices: MutableList) { val serviceTypes = advertisedServices.map { it.type } - inNodeNetworkMapService = if (configuration.networkMapService == null) makeNetworkMapService() else NullNetworkMapService + inNodeNetworkMapService = if (configuration.networkMapService == null) makeNetworkMapService(network, networkMapCache) else NullNetworkMapService val notaryServiceType = serviceTypes.singleOrNull { it.isNotary() } if (notaryServiceType != null) { val service = makeCoreNotaryService(notaryServiceType) @@ -631,9 +638,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, return PersistentKeyManagementService(identityService, partyKeys) } - open protected fun makeNetworkMapService(): NetworkMapService { - return PersistentNetworkMapService(services, configuration.minimumPlatformVersion) - } + abstract protected fun makeNetworkMapService(network: MessagingService, networkMapCache: NetworkMapCacheInternal): NetworkMapService open protected fun makeCoreNotaryService(type: ServiceType): NotaryService? { check(myNotaryIdentity != null) { "No notary identity initialized when creating a notary service" } 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 31cb1cb27f..adf92b70c1 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -19,6 +19,7 @@ import net.corda.node.serialization.KryoServerSerializationScheme import net.corda.node.serialization.NodeClock import net.corda.node.services.RPCUserService import net.corda.node.services.RPCUserServiceImpl +import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.api.SchemaService import net.corda.nodeapi.internal.ServiceInfo import net.corda.node.services.config.FullNodeConfiguration @@ -27,6 +28,8 @@ import net.corda.node.services.messaging.ArtemisMessagingServer.Companion.ipDete import net.corda.node.services.messaging.ArtemisMessagingServer.Companion.ipDetectResponseProperty import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.NodeMessagingClient +import net.corda.node.services.network.NetworkMapService +import net.corda.node.services.network.PersistentNetworkMapService import net.corda.node.utilities.AddressUtils import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.TestClock @@ -62,9 +65,9 @@ import kotlin.system.exitProcess */ open class Node(override val configuration: FullNodeConfiguration, advertisedServices: Set, - private val versionInfo: VersionInfo, + versionInfo: VersionInfo, val initialiseSerialization: Boolean = true -) : AbstractNode(configuration, advertisedServices, createClock(configuration)) { +) : AbstractNode(configuration, advertisedServices, createClock(configuration), versionInfo) { companion object { private val logger = loggerFor() var renderBasicInfoToConsole = true @@ -90,7 +93,6 @@ open class Node(override val configuration: FullNodeConfiguration, } override val log: Logger get() = logger - override val platformVersion: Int get() = versionInfo.platformVersion override val networkMapAddress: NetworkMapAddress? get() = configuration.networkMapService?.address?.let(::NetworkMapAddress) override fun makeTransactionVerifierService() = (network as NodeMessagingClient).verifierService @@ -277,6 +279,10 @@ open class Node(override val configuration: FullNodeConfiguration, return listOf(address.hostAndPort) } + override fun makeNetworkMapService(network: MessagingService, networkMapCache: NetworkMapCacheInternal): NetworkMapService { + return PersistentNetworkMapService(network, networkMapCache, configuration.minimumPlatformVersion) + } + /** * If the node is persisting to an embedded H2 database, then expose this via TCP with a JDBC URL of the form: * jdbc:h2:tcp://:/node diff --git a/node/src/main/kotlin/net/corda/node/services/api/AbstractNodeService.kt b/node/src/main/kotlin/net/corda/node/services/api/AbstractNodeService.kt index f14f0af01a..42b12629c4 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/AbstractNodeService.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/AbstractNodeService.kt @@ -10,10 +10,7 @@ import javax.annotation.concurrent.ThreadSafe * Abstract superclass for services that a node can host, which provides helper functions. */ @ThreadSafe -abstract class AbstractNodeService(val services: ServiceHubInternal) : SingletonSerializeAsToken() { - - val network: MessagingService get() = services.networkService - +abstract class AbstractNodeService(val network: MessagingService) : SingletonSerializeAsToken() { /** * Register a handler for a message topic. In comparison to using net.addMessageHandler() this manages a lot of * common boilerplate code. Exceptions are caught and passed to the provided consumer. If you just want a simple @@ -36,7 +33,7 @@ abstract class AbstractNodeService(val services: ServiceHubInternal) : Singleton val msg = network.createMessage(topic, request.sessionID, response.serialize().bytes) network.send(msg, request.replyTo) } - } catch(e: Exception) { + } catch (e: Exception) { exceptionConsumer(message, e) } } 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 cbaa194aad..ba865ecaba 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 @@ -93,6 +93,7 @@ data class FullNodeConfiguration( require(it.username.matches("\\w+".toRegex())) { "Username ${it.username} contains invalid characters" } } require(myLegalName.commonName == null) { "Common name must be null: $myLegalName" } + require(minimumPlatformVersion >= 1) { "minimumPlatformVersion cannot be less than 1" } } fun calculateServices(): Set { diff --git a/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt index 966d537954..06b04e55a7 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt @@ -20,8 +20,9 @@ import net.corda.core.serialization.serialize import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor import net.corda.node.services.api.AbstractNodeService -import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.messaging.MessageHandlerRegistration +import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.ServiceRequestMessage import net.corda.node.services.messaging.createMessage import net.corda.node.services.network.NetworkMapService.* @@ -114,8 +115,8 @@ interface NetworkMapService { object NullNetworkMapService : NetworkMapService @ThreadSafe -class InMemoryNetworkMapService(services: ServiceHubInternal, minimumPlatformVersion: Int) - : AbstractNetworkMapService(services, minimumPlatformVersion) { +class InMemoryNetworkMapService(network: MessagingService, networkMapCache: NetworkMapCacheInternal, minimumPlatformVersion: Int) + : AbstractNetworkMapService(network, networkMapCache, minimumPlatformVersion) { override val nodeRegistrations: MutableMap = ConcurrentHashMap() override val subscribers = ThreadBox(mutableMapOf()) @@ -132,8 +133,9 @@ class InMemoryNetworkMapService(services: ServiceHubInternal, minimumPlatformVer * subscriber clean up and is simpler to persist than the previous implementation based on a set of missing messages acks. */ @ThreadSafe -abstract class AbstractNetworkMapService(services: ServiceHubInternal, - val minimumPlatformVersion: Int) : NetworkMapService, AbstractNodeService(services) { +abstract class AbstractNetworkMapService(network: MessagingService, + private val networkMapCache: NetworkMapCacheInternal, + private val minimumPlatformVersion: Int) : NetworkMapService, AbstractNodeService(network) { companion object { /** * Maximum credible size for a registration request. Generally requests are around 2000-6000 bytes, so this gives a @@ -158,14 +160,6 @@ abstract class AbstractNetworkMapService(services: ServiceHubInternal, val maxUnacknowledgedUpdates = 10 private val handlers = ArrayList() - - init { - require(minimumPlatformVersion >= 1) { "minimumPlatformVersion cannot be less than 1" } - require(minimumPlatformVersion <= services.myInfo.platformVersion) { - "minimumPlatformVersion cannot be greater than the node's own version" - } - } - protected fun setup() { // Register message handlers handlers += addMessageHandler(FETCH_TOPIC) { req: FetchMapRequest -> processFetchAllRequest(req) } @@ -200,7 +194,7 @@ abstract class AbstractNetworkMapService(services: ServiceHubInternal, subscribers.locked { remove(subscriber) } } - private fun processAcknowledge(request: UpdateAcknowledge): Unit { + private fun processAcknowledge(request: UpdateAcknowledge) { if (request.replyTo !is SingleMessageRecipient) throw NodeMapException.InvalidSubscriber() subscribers.locked { val lastVersionAcked = this[request.replyTo]?.mapVersion @@ -280,11 +274,11 @@ abstract class AbstractNetworkMapService(services: ServiceHubInternal, when (change.type) { ADD -> { logger.info("Added node ${node.addresses} to network map") - services.networkMapCache.addNode(change.node) + networkMapCache.addNode(change.node) } REMOVE -> { logger.info("Removed node ${node.addresses} from network map") - services.networkMapCache.removeNode(change.node) + networkMapCache.removeNode(change.node) } } diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt index f62748dcba..6d3600d398 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt @@ -7,7 +7,8 @@ import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize -import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.api.NetworkMapCacheInternal +import net.corda.node.services.messaging.MessagingService import net.corda.node.utilities.* import net.corda.nodeapi.ArtemisMessagingComponent import java.io.ByteArrayInputStream @@ -23,8 +24,8 @@ import java.util.* * This class needs database transactions to be in-flight during method calls and init, otherwise it will throw * exceptions. */ -class PersistentNetworkMapService(services: ServiceHubInternal, minimumPlatformVersion: Int) - : AbstractNetworkMapService(services, minimumPlatformVersion) { +class PersistentNetworkMapService(network: MessagingService, networkMapCache: NetworkMapCacheInternal, minimumPlatformVersion: Int) + : AbstractNetworkMapService(network, networkMapCache, minimumPlatformVersion) { // Only the node_party_path column is needed to reconstruct a PartyAndCertificate but we have the others for human readability @Entity diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt index a429b84d1f..a8646caad0 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt @@ -7,6 +7,8 @@ import net.corda.core.node.NodeInfo import net.corda.core.serialization.deserialize import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode +import net.corda.node.services.api.NetworkMapCacheInternal +import net.corda.nodeapi.internal.ServiceInfo import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.send @@ -20,11 +22,9 @@ import net.corda.node.services.network.NetworkMapService.Companion.PUSH_TOPIC import net.corda.node.services.network.NetworkMapService.Companion.QUERY_TOPIC import net.corda.node.services.network.NetworkMapService.Companion.REGISTER_TOPIC import net.corda.node.services.network.NetworkMapService.Companion.SUBSCRIPTION_TOPIC -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.utilities.AddOrRemove import net.corda.node.utilities.AddOrRemove.ADD import net.corda.node.utilities.AddOrRemove.REMOVE -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode @@ -275,7 +275,7 @@ abstract class AbstractNetworkMapServiceTest notaryIdentity: Pair?, entropyRoot: BigInteger): MockNode { return object : MockNode(config, network, null, advertisedServices, id, notaryIdentity, entropyRoot) { - override fun makeNetworkMapService() = NullNetworkMapService + override fun makeNetworkMapService(network: MessagingService, networkMapCache: NetworkMapCacheInternal) = NullNetworkMapService } } } diff --git a/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt index 19bf42cf65..7e8e6a34a1 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt @@ -1,11 +1,13 @@ package net.corda.node.services.network import net.corda.core.messaging.SingleMessageRecipient +import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.messaging.MessagingService import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode +import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import java.math.BigInteger import java.security.KeyPair @@ -35,7 +37,7 @@ class PersistentNetworkMapServiceTest : AbstractNetworkMapServiceTest?, entropyRoot: BigInteger): MockNode { return object : MockNode(config, network, networkMapAddr, advertisedServices, id, notaryIdentity, entropyRoot) { - override fun makeNetworkMapService() = SwizzleNetworkMapService(services) + override fun makeNetworkMapService(network: MessagingService, networkMapCache: NetworkMapCacheInternal) = SwizzleNetworkMapService(network, networkMapCache) } } } @@ -44,12 +46,13 @@ class PersistentNetworkMapServiceTest : AbstractNetworkMapServiceTest PersistentNetworkMapService) : NetworkMapService { + constructor(network: MessagingService, networkMapCache: NetworkMapCacheInternal) : this({ PersistentNetworkMapService(network, networkMapCache, 1) }) + var delegate = delegateFactory() fun swizzle() { delegate.unregisterNetworkHandlers() - delegate = PersistentNetworkMapService(services, 1) + delegate = delegateFactory() } } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index 2f8f50b7c3..e1d1f5c4c3 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -27,6 +27,9 @@ import net.corda.core.utilities.loggerFor import net.corda.finance.utils.WorldMapLocation import net.corda.node.internal.AbstractNode import net.corda.node.internal.StartedNode +import net.corda.node.services.api.NetworkMapCacheInternal +import net.corda.nodeapi.internal.ServiceInfo +import net.corda.nodeapi.internal.ServiceType import net.corda.node.services.api.SchemaService import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.identity.PersistentIdentityService @@ -41,9 +44,8 @@ import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.node.utilities.CertificateAndKeyPair -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.nodeapi.internal.ServiceType import net.corda.testing.* +import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.apache.activemq.artemis.utils.ReusableLatch import org.slf4j.Logger @@ -158,10 +160,9 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, val id: Int, internal val notaryIdentity: Pair?, val entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue())) : - AbstractNode(config, advertisedServices, TestClock(), mockNet.busyLatch) { + AbstractNode(config, advertisedServices, TestClock(), MOCK_VERSION_INFO, mockNet.busyLatch) { var counter = entropyRoot override val log: Logger = loggerFor() - override val platformVersion: Int get() = 1 override val serverThread: AffinityExecutor = if (mockNet.threadPerNode) ServiceAffinityExecutor("Mock node $id thread", 1) @@ -215,8 +216,8 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, // Nothing to do } - override fun makeNetworkMapService(): NetworkMapService { - return InMemoryNetworkMapService(services, platformVersion) + override fun makeNetworkMapService(network: MessagingService, networkMapCache: NetworkMapCacheInternal): NetworkMapService { + return InMemoryNetworkMapService(network, networkMapCache, 1) } override fun getNotaryIdentity(): PartyAndCertificate? { From 83f37417ae25426e0a24ec4e63b51239b9dfc50c Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Sat, 7 Oct 2017 12:48:16 +0100 Subject: [PATCH 108/185] Fixes the API docs. --- docs/source/api-flows.rst | 119 ++++++++---------- docs/source/api-identity.rst | 4 +- docs/source/api-transactions.rst | 78 ++++++------ .../java/net/corda/docs/FlowCookbookJava.java | 32 ++--- .../kotlin/net/corda/docs/FlowCookbook.kt | 32 ++--- 5 files changed, 124 insertions(+), 141 deletions(-) diff --git a/docs/source/api-flows.rst b/docs/source/api-flows.rst index 931aa493df..4a0e284621 100644 --- a/docs/source/api-flows.rst +++ b/docs/source/api-flows.rst @@ -221,14 +221,14 @@ There are several ways to retrieve a notary from the network map: .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 1 - :end-before: DOCEND 1 - :dedent: 12 + :start-after: DOCSTART 01 + :end-before: DOCEND 01 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java - :start-after: DOCSTART 1 - :end-before: DOCEND 1 + :start-after: DOCSTART 01 + :end-before: DOCEND 01 :dedent: 12 Specific counterparties @@ -239,32 +239,14 @@ We can also use the network map to retrieve a specific counterparty: .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 2 - :end-before: DOCEND 2 - :dedent: 12 + :start-after: DOCSTART 02 + :end-before: DOCEND 02 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java - :start-after: DOCSTART 2 - :end-before: DOCEND 2 - :dedent: 12 - -Specific services -~~~~~~~~~~~~~~~~~ -Finally, we can use the map to identify nodes providing a specific service (e.g. a regulator or an oracle): - -.. container:: codeset - - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt - :language: kotlin - :start-after: DOCSTART 3 - :end-before: DOCEND 3 - :dedent: 12 - - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java - :language: java - :start-after: DOCSTART 3 - :end-before: DOCEND 3 + :start-after: DOCSTART 02 + :end-before: DOCEND 02 :dedent: 12 Communication between parties @@ -295,7 +277,7 @@ InitiateFlow :language: kotlin :start-after: DOCSTART initiateFlow :end-before: DOCEND initiateFlow - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -318,14 +300,14 @@ Once we have a ``FlowSession`` object we can send arbitrary data to a counterpar .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 4 - :end-before: DOCEND 4 - :dedent: 12 + :start-after: DOCSTART 04 + :end-before: DOCEND 04 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java - :start-after: DOCSTART 4 - :end-before: DOCEND 4 + :start-after: DOCSTART 04 + :end-before: DOCEND 04 :dedent: 12 The flow on the other side must eventually reach a corresponding ``receive`` call to get this message. @@ -350,14 +332,14 @@ be what it appears to be! We must unwrap the ``UntrustworthyData`` using a lambd .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 5 - :end-before: DOCEND 5 - :dedent: 12 + :start-after: DOCSTART 05 + :end-before: DOCEND 05 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java - :start-after: DOCSTART 5 - :end-before: DOCEND 5 + :start-after: DOCSTART 05 + :end-before: DOCEND 05 :dedent: 12 We're not limited to sending to and receiving from a single counterparty. A flow can send messages to as many parties @@ -367,14 +349,14 @@ as it likes, and each party can invoke a different response flow: .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 6 - :end-before: DOCEND 6 - :dedent: 12 + :start-after: DOCSTART 06 + :end-before: DOCEND 06 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java - :start-after: DOCSTART 6 - :end-before: DOCEND 6 + :start-after: DOCSTART 06 + :end-before: DOCEND 06 :dedent: 12 .. warning:: If you initiate several flows from the same ``@InitiatingFlow`` flow then on the receiving side you must be @@ -392,14 +374,14 @@ type of data sent doesn't need to match the type of the data received back: .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 7 - :end-before: DOCEND 7 - :dedent: 12 + :start-after: DOCSTART 07 + :end-before: DOCEND 07 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java - :start-after: DOCSTART 7 - :end-before: DOCEND 7 + :start-after: DOCSTART 07 + :end-before: DOCEND 07 :dedent: 12 Counterparty response @@ -417,14 +399,14 @@ Our side of the flow must mirror these calls. We could do this as follows: .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 8 - :end-before: DOCEND 8 - :dedent: 12 + :start-after: DOCSTART 08 + :end-before: DOCEND 08 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java - :start-after: DOCSTART 8 - :end-before: DOCEND 8 + :start-after: DOCSTART 08 + :end-before: DOCEND 08 :dedent: 12 Why sessions? @@ -481,7 +463,7 @@ explicit in the ``initiateFlow`` function call. To port existing code: :language: kotlin :start-after: DOCSTART FlowSession porting :end-before: DOCEND FlowSession porting - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -489,7 +471,6 @@ explicit in the ``initiateFlow`` function call. To port existing code: :end-before: DOCEND FlowSession porting :dedent: 12 - Subflows -------- @@ -560,14 +541,14 @@ the transaction's states: .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 9 - :end-before: DOCEND 9 - :dedent: 12 + :start-after: DOCSTART 09 + :end-before: DOCEND 09 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java - :start-after: DOCSTART 9 - :end-before: DOCEND 9 + :start-after: DOCSTART 09 + :end-before: DOCEND 09 :dedent: 12 We can also choose to send the transaction to additional parties who aren't one of the state's participants: @@ -578,7 +559,7 @@ We can also choose to send the transaction to additional parties who aren't one :language: kotlin :start-after: DOCSTART 10 :end-before: DOCEND 10 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -601,7 +582,7 @@ transaction ourselves, we can automatically gather the signatures of the other r :language: kotlin :start-after: DOCSTART 15 :end-before: DOCEND 15 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -618,7 +599,7 @@ transaction and provide their signature if they are satisfied: :language: kotlin :start-after: DOCSTART 16 :end-before: DOCEND 16 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -639,7 +620,7 @@ transaction data vending requests as the receiver walks the dependency chain usi :language: kotlin :start-after: DOCSTART 12 :end-before: DOCEND 12 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -656,7 +637,7 @@ dependencies and verify the transaction: :language: kotlin :start-after: DOCSTART 13 :end-before: DOCEND 13 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -672,7 +653,7 @@ We can also send and receive a ``StateAndRef`` dependency chain and automaticall :language: kotlin :start-after: DOCSTART 14 :end-before: DOCEND 14 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -731,7 +712,7 @@ To provide a progress tracker, we have to override ``FlowLogic.progressTracker`` :language: kotlin :start-after: DOCSTART 17 :end-before: DOCEND 17 - :dedent: 8 + :dedent: 4 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -747,7 +728,7 @@ We then update the progress tracker's current step as we progress through the fl :language: kotlin :start-after: DOCSTART 18 :end-before: DOCEND 18 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java diff --git a/docs/source/api-identity.rst b/docs/source/api-identity.rst index 270e31bafd..c87b50a45f 100644 --- a/docs/source/api-identity.rst +++ b/docs/source/api-identity.rst @@ -69,6 +69,7 @@ You can see an example of it being used in ``TwoPartyDealFlow.kt``: :language: kotlin :start-after: DOCSTART 2 :end-before: DOCEND 2 + :dedent: 8 The swap identities flow goes through the following key steps: @@ -100,6 +101,7 @@ entities (counterparties) to require to know which well known identities those c :language: kotlin :start-after: DOCSTART 6 :end-before: DOCEND 6 + :dedent: 12 The identity synchronization flow goes through the following key steps: @@ -126,4 +128,4 @@ identity. This is important for more complex transaction cases with 3+ parties, Alice may know all of the confidential identities ahead of time, but Bob not know about Charlie's and vice-versa. The assembled transaction therefore has three input states *x*, *y* and *z*, for which only Alice possesses certificates for all confidential identities. ``IdentitySyncFlow`` must send not just Alice's confidential identity but also any other -identities in the transaction to the Bob and Charlie. +identities in the transaction to the Bob and Charlie. \ No newline at end of file diff --git a/docs/source/api-transactions.rst b/docs/source/api-transactions.rst index 7d76a0624b..0c02f4b565 100644 --- a/docs/source/api-transactions.rst +++ b/docs/source/api-transactions.rst @@ -59,7 +59,7 @@ An input state is added to a transaction as a ``StateAndRef``, which combines: :language: kotlin :start-after: DOCSTART 21 :end-before: DOCEND 21 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -78,7 +78,7 @@ A ``StateRef`` uniquely identifies an input state, allowing the notary to mark i :language: kotlin :start-after: DOCSTART 20 :end-before: DOCEND 20 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -102,7 +102,7 @@ add them to the transaction directly: :language: kotlin :start-after: DOCSTART 22 :end-before: DOCEND 22 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -119,7 +119,7 @@ it on the input state: :language: kotlin :start-after: DOCSTART 23 :end-before: DOCEND 23 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -139,7 +139,7 @@ wrapping the output state in a ``StateAndContract``, which combines: :language: kotlin :start-after: DOCSTART 47 :end-before: DOCEND 47 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -160,7 +160,7 @@ A command is added to the transaction as a ``Command``, which combines: :language: kotlin :start-after: DOCSTART 24 :end-before: DOCEND 24 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -178,7 +178,7 @@ Attachments are identified by their hash: :language: kotlin :start-after: DOCSTART 25 :end-before: DOCEND 25 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -199,7 +199,7 @@ time, or be open at either end: :language: kotlin :start-after: DOCSTART 26 :end-before: DOCEND 26 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -215,7 +215,7 @@ We can also define a time window as an ``Instant`` plus/minus a time tolerance ( :language: kotlin :start-after: DOCSTART 42 :end-before: DOCEND 42 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -231,7 +231,7 @@ Or as a start-time plus a duration: :language: kotlin :start-after: DOCSTART 43 :end-before: DOCEND 43 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -255,7 +255,7 @@ that will notarise the inputs and verify the time-window: :language: kotlin :start-after: DOCSTART 19 :end-before: DOCEND 19 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -274,7 +274,7 @@ instantiated without one: :language: kotlin :start-after: DOCSTART 46 :end-before: DOCEND 46 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -317,7 +317,7 @@ Here's an example usage of ``TransactionBuilder.withItems``: :language: kotlin :start-after: DOCSTART 27 :end-before: DOCEND 27 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -335,7 +335,7 @@ Here are the methods for adding inputs and attachments: :language: kotlin :start-after: DOCSTART 28 :end-before: DOCEND 28 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -351,7 +351,7 @@ An output state can be added as a ``ContractState``, contract class name and not :language: kotlin :start-after: DOCSTART 49 :end-before: DOCEND 49 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -367,7 +367,7 @@ We can also leave the notary field blank, in which case the transaction's defaul :language: kotlin :start-after: DOCSTART 50 :end-before: DOCEND 50 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -383,7 +383,7 @@ Or we can add the output state as a ``TransactionState``, which already specifie :language: kotlin :start-after: DOCSTART 51 :end-before: DOCEND 51 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -399,7 +399,7 @@ Commands can be added as a ``Command``: :language: kotlin :start-after: DOCSTART 52 :end-before: DOCEND 52 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -415,7 +415,7 @@ Or as ``CommandData`` and a ``vararg PublicKey``: :language: kotlin :start-after: DOCSTART 53 :end-before: DOCEND 53 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -431,7 +431,7 @@ For the time-window, we can set a time-window directly: :language: kotlin :start-after: DOCSTART 44 :end-before: DOCEND 44 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -447,7 +447,7 @@ Or define the time-window as a time plus a duration (e.g. 45 seconds): :language: kotlin :start-after: DOCSTART 45 :end-before: DOCEND 45 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -467,7 +467,7 @@ We can either sign with our legal identity key: :language: kotlin :start-after: DOCSTART 29 :end-before: DOCEND 29 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -483,7 +483,7 @@ Or we can also choose to use another one of our public keys: :language: kotlin :start-after: DOCSTART 30 :end-before: DOCEND 30 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -527,13 +527,13 @@ and output states: :language: kotlin :start-after: DOCSTART 33 :end-before: DOCEND 33 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java :start-after: DOCSTART 33 :end-before: DOCEND 33 - :dedent: 12 + :dedent: 16 Checking that the transaction meets the contract constraints is only part of verifying the transaction's contents. We will usually also want to perform our own additional validation of the transaction contents before signing, to ensure @@ -552,13 +552,13 @@ We achieve this by using the ``ServiceHub`` to convert the ``SignedTransaction`` :language: kotlin :start-after: DOCSTART 32 :end-before: DOCEND 32 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java :start-after: DOCSTART 32 :end-before: DOCEND 32 - :dedent: 12 + :dedent: 16 We can now perform our additional verification. Here's a simple example: @@ -568,13 +568,13 @@ We can now perform our additional verification. Here's a simple example: :language: kotlin :start-after: DOCSTART 34 :end-before: DOCEND 34 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java :start-after: DOCSTART 34 :end-before: DOCEND 34 - :dedent: 12 + :dedent: 16 Verifying the transaction's signatures ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -589,13 +589,13 @@ We can verify that all the transaction's required signatures are present and val :language: kotlin :start-after: DOCSTART 35 :end-before: DOCEND 35 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java :start-after: DOCSTART 35 :end-before: DOCEND 35 - :dedent: 12 + :dedent: 16 However, we'll often want to verify the transaction's existing signatures before all of them have been collected. For this we can use ``SignedTransaction.verifySignaturesExcept``, which takes a ``vararg`` of the public keys for @@ -607,13 +607,13 @@ which the signatures are allowed to be missing: :language: kotlin :start-after: DOCSTART 36 :end-before: DOCEND 36 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java :start-after: DOCSTART 36 :end-before: DOCEND 36 - :dedent: 12 + :dedent: 16 If the transaction is missing any signatures without the corresponding public keys being passed in, a ``SignaturesMissingException`` is thrown. @@ -626,13 +626,13 @@ We can also choose to simply verify the signatures that are present: :language: kotlin :start-after: DOCSTART 37 :end-before: DOCEND 37 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java :start-after: DOCSTART 37 :end-before: DOCEND 37 - :dedent: 12 + :dedent: 16 Be very careful, however - this function neither guarantees that the signatures that are present are required, nor checks whether any signatures are missing. @@ -650,7 +650,7 @@ We can sign using our legal identity key, as follows: :language: kotlin :start-after: DOCSTART 38 :end-before: DOCEND 38 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -666,7 +666,7 @@ Or we can choose to sign using another one of our public keys: :language: kotlin :start-after: DOCSTART 39 :end-before: DOCEND 39 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -684,7 +684,7 @@ We can do this with our legal identity key: :language: kotlin :start-after: DOCSTART 40 :end-before: DOCEND 40 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java @@ -700,7 +700,7 @@ Or using another one of our public keys: :language: kotlin :start-after: DOCSTART 41 :end-before: DOCEND 41 - :dedent: 12 + :dedent: 8 .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java :language: java 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/FlowCookbookJava.java index 1946a2501d..e007e3af6f 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/FlowCookbookJava.java @@ -122,7 +122,7 @@ public class FlowCookbookJava { // - To serve as a timestamping authority if the transaction has a // time-window // We retrieve a notary from the network map. - // DOCSTART 1 + // DOCSTART 01 CordaX500Name notaryName = new CordaX500Name("Notary Service", "London", "GB"); Party specificNotary = getServiceHub().getNetworkMapCache().getNotary(notaryName); // Alternatively, we can pick an arbitrary notary from the notary @@ -130,15 +130,15 @@ public class FlowCookbookJava { // explicitly, as the notary list might change when new notaries are // introduced, or old ones decommissioned. Party firstNotary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0); - // DOCEND 1 + // DOCEND 01 // We may also need to identify a specific counterparty. We do so // using the identity service. - // DOCSTART 2 + // DOCSTART 02 CordaX500Name counterPartyName = new CordaX500Name("NodeA", "London", "GB"); Party namedCounterparty = getServiceHub().getIdentityService().wellKnownPartyFromX500Name(counterPartyName); Party keyedCounterparty = getServiceHub().getIdentityService().partyFromKey(dummyPubKey); - // DOCEND 2 + // DOCEND 02 /*------------------------------ * SENDING AND RECEIVING DATA * @@ -162,9 +162,9 @@ public class FlowCookbookJava { // In other words, we are assuming that the counterparty is // registered to respond to this flow, and has a corresponding // ``receive`` call. - // DOCSTART 4 + // DOCSTART 04 counterpartySession.send(new Object()); - // DOCEND 4 + // DOCEND 04 // We can wait to receive arbitrary data of a specific type from a // counterparty. Again, this implies a corresponding ``send`` call @@ -185,7 +185,7 @@ public class FlowCookbookJava { // instance. This is a reminder that the data we receive may not // be what it appears to be! We must unwrap the // ``UntrustworthyData`` using a lambda. - // DOCSTART 5 + // DOCSTART 05 UntrustworthyData packet1 = counterpartySession.receive(Integer.class); Integer integer = packet1.unwrap(data -> { // Perform checking on the object received. @@ -193,13 +193,13 @@ public class FlowCookbookJava { // Return the object. return data; }); - // DOCEND 5 + // DOCEND 05 // We can also use a single call to send data to a counterparty // and wait to receive data of a specific type back. The type of // data sent doesn't need to match the type of the data received // back. - // DOCSTART 7 + // DOCSTART 07 UntrustworthyData packet2 = counterpartySession.sendAndReceive(Boolean.class, "You can send and receive any class!"); Boolean bool = packet2.unwrap(data -> { // Perform checking on the object received. @@ -207,16 +207,16 @@ public class FlowCookbookJava { // Return the object. return data; }); - // DOCEND 7 + // DOCEND 07 // We're not limited to sending to and receiving from a single // counterparty. A flow can send messages to as many parties as it // likes, and each party can invoke a different response flow. - // DOCSTART 6 + // DOCSTART 06 FlowSession regulatorSession = initiateFlow(regulator); regulatorSession.send(new Object()); UntrustworthyData packet3 = regulatorSession.receive(Object.class); - // DOCEND 6 + // DOCEND 06 /*------------------------------------ * EXTRACTING STATES FROM THE VAULT * @@ -564,9 +564,9 @@ public class FlowCookbookJava { // We notarise the transaction and get it recorded in the vault of // the participants of all the transaction's states. - // DOCSTART 9 + // DOCSTART 09 SignedTransaction notarisedTx1 = subFlow(new FinalityFlow(fullySignedTx, FINALISATION.childProgressTracker())); - // DOCEND 9 + // DOCEND 09 // We can also choose to send it to additional parties who aren't one // of the state's participants. // DOCSTART 10 @@ -629,11 +629,11 @@ public class FlowCookbookJava { // 3. They sent a ``String`` instance and waited to receive a // ``Boolean`` instance back // Our side of the flow must mirror these calls. - // DOCSTART 8 + // DOCSTART 08 Object obj = counterpartySession.receive(Object.class).unwrap(data -> data); String string = counterpartySession.sendAndReceive(String.class, 99).unwrap(data -> data); counterpartySession.send(true); - // DOCEND 8 + // DOCEND 08 /*----------------------------------------- * RESPONDING TO COLLECT_SIGNATURES_FLOW * 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/FlowCookbook.kt index 1fbee1e1be..1ef82f8a1b 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/FlowCookbook.kt @@ -102,7 +102,7 @@ class InitiatorFlow(val arg1: Boolean, val arg2: Int, private val counterparty: // - To serve as a timestamping authority if the transaction has a // time-window // We retrieve the notary from the network map. - // DOCSTART 1 + // DOCSTART 01 val notaryName: CordaX500Name = CordaX500Name( organisation = "Notary Service", locality = "London", @@ -113,11 +113,11 @@ class InitiatorFlow(val arg1: Boolean, val arg2: Int, private val counterparty: // explicitly, as the notary list might change when new notaries are // introduced, or old ones decommissioned. val firstNotary: Party = serviceHub.networkMapCache.notaryIdentities.first() - // DOCEND 1 + // DOCEND 01 // We may also need to identify a specific counterparty. We do so // using the identity service. - // DOCSTART 2 + // DOCSTART 02 val counterpartyName: CordaX500Name = CordaX500Name( organisation = "NodeA", locality = "London", @@ -126,7 +126,7 @@ class InitiatorFlow(val arg1: Boolean, val arg2: Int, private val counterparty: throw IllegalArgumentException("Couldn't find counterparty for NodeA in identity service") val keyedCounterparty: Party = serviceHub.identityService.partyFromKey(dummyPubKey) ?: throw IllegalArgumentException("Couldn't find counterparty with key: $dummyPubKey in identity service") - // DOCEND 2 + // DOCEND 02 /**----------------------------- * SENDING AND RECEIVING DATA * @@ -150,9 +150,9 @@ class InitiatorFlow(val arg1: Boolean, val arg2: Int, private val counterparty: // In other words, we are assuming that the counterparty is // registered to respond to this flow, and has a corresponding // ``receive`` call. - // DOCSTART 4 + // DOCSTART 04 counterpartySession.send(Any()) - // DOCEND 4 + // DOCEND 04 // We can wait to receive arbitrary data of a specific type from a // counterparty. Again, this implies a corresponding ``send`` call @@ -173,7 +173,7 @@ class InitiatorFlow(val arg1: Boolean, val arg2: Int, private val counterparty: // instance. This is a reminder that the data we receive may not // be what it appears to be! We must unwrap the // ``UntrustworthyData`` using a lambda. - // DOCSTART 5 + // DOCSTART 05 val packet1: UntrustworthyData = counterpartySession.receive() val int: Int = packet1.unwrap { data -> // Perform checking on the object received. @@ -181,13 +181,13 @@ class InitiatorFlow(val arg1: Boolean, val arg2: Int, private val counterparty: // Return the object. data } - // DOCEND 5 + // DOCEND 05 // We can also use a single call to send data to a counterparty // and wait to receive data of a specific type back. The type of // data sent doesn't need to match the type of the data received // back. - // DOCSTART 7 + // DOCSTART 07 val packet2: UntrustworthyData = counterpartySession.sendAndReceive("You can send and receive any class!") val boolean: Boolean = packet2.unwrap { data -> // Perform checking on the object received. @@ -195,16 +195,16 @@ class InitiatorFlow(val arg1: Boolean, val arg2: Int, private val counterparty: // Return the object. data } - // DOCEND 7 + // DOCEND 07 // We're not limited to sending to and receiving from a single // counterparty. A flow can send messages to as many parties as it // likes, and each party can invoke a different response flow. - // DOCSTART 6 + // DOCSTART 06 val regulatorSession: FlowSession = initiateFlow(regulator) regulatorSession.send(Any()) val packet3: UntrustworthyData = regulatorSession.receive() - // DOCEND 6 + // DOCEND 06 /**----------------------------------- * EXTRACTING STATES FROM THE VAULT * @@ -543,9 +543,9 @@ class InitiatorFlow(val arg1: Boolean, val arg2: Int, private val counterparty: // We notarise the transaction and get it recorded in the vault of // the participants of all the transaction's states. - // DOCSTART 9 + // DOCSTART 09 val notarisedTx1: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, FINALISATION.childProgressTracker())) - // DOCEND 9 + // DOCEND 09 // We can also choose to send it to additional parties who aren't one // of the state's participants. // DOCSTART 10 @@ -603,11 +603,11 @@ class ResponderFlow(val counterpartySession: FlowSession) : FlowLogic() { // 3. They sent a ``String`` instance and waited to receive a // ``Boolean`` instance back // Our side of the flow must mirror these calls. - // DOCSTART 8 + // DOCSTART 08 val any: Any = counterpartySession.receive().unwrap { data -> data } val string: String = counterpartySession.sendAndReceive(99).unwrap { data -> data } counterpartySession.send(true) - // DOCEND 8 + // DOCEND 08 /**---------------------------------------- * RESPONDING TO COLLECT_SIGNATURES_FLOW * From 727cd0e55c9c992f2e337b2e26f3e8472ac9d8c1 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Thu, 5 Oct 2017 12:27:45 +0100 Subject: [PATCH 109/185] Cleaned up notary configuration by introducing a notary config option. extraAdvertisedServiceIds is no longer used for this. --- build.gradle | 3 +- .../corda/client/jfx/NodeMonitorModelTest.kt | 4 +- .../client/rpc/CordaRPCJavaClientTest.java | 6 +- .../corda/client/rpc/CordaRPCClientTest.kt | 4 +- .../rpc/StandaloneCordaRPCJavaClientTest.java | 12 +- .../kotlin/rpc/StandaloneCordaRPClientTest.kt | 2 +- config/dev/nameservernode.conf | 4 +- .../kotlin/net/corda/core/flows/NotaryFlow.kt | 6 +- .../core/node/services/NetworkMapCache.kt | 6 +- .../corda/core/node/services/NotaryService.kt | 12 ++ .../net/corda/core/flows/AttachmentTests.kt | 5 +- docs/source/changelog.rst | 5 + docs/source/corda-configuration-file.rst | 34 ++++-- docs/source/deploying-a-node.rst | 3 +- docs/source/example-code/build.gradle | 3 +- .../corda/docs/IntegrationTestingTutorial.kt | 6 +- .../net/corda/docs/ClientRpcTutorial.kt | 4 +- .../net/corda/docs/CustomVaultQueryTest.kt | 8 +- .../docs/FxTransactionBuildTutorialTest.kt | 8 +- .../WorkflowTransactionBuildTutorialTest.kt | 13 +- docs/source/hello-world-running.rst | 3 +- docs/source/running-a-notary.rst | 8 +- docs/source/tutorial-custom-notary.rst | 6 - .../java/net/corda/cordform/CordformNode.java | 31 ++--- .../main/groovy/net/corda/plugins/Node.groovy | 16 +-- .../src/main/kotlin/net/corda/nodeapi/User.kt | 8 +- .../corda/nodeapi/config/ConfigUtilities.kt | 68 ++++++++++- .../corda/nodeapi/config/ConfigParsingTest.kt | 52 +++++--- .../net/corda/node/NodePerformanceTests.kt | 12 +- .../node/services/AttachmentLoadingTests.kt | 4 +- .../node/services/BFTNotaryServiceTests.kt | 23 ++-- .../node/services/DistributedServiceTests.kt | 5 +- .../node/services/RaftNotaryServiceTests.kt | 4 +- .../services/messaging/P2PMessagingTest.kt | 46 +------ .../test/node/NodeStatePersistenceTests.kt | 5 +- .../net/corda/node/internal/AbstractNode.kt | 113 ++++++++++-------- .../net/corda/node/internal/NodeStartup.kt | 11 +- .../node/services/config/NodeConfiguration.kt | 41 ++++--- .../network/PersistentNetworkMapCache.kt | 7 +- .../BFTNonValidatingNotaryService.kt | 62 +++++----- .../RaftNonValidatingNotaryService.kt | 13 +- .../transactions/RaftUniquenessProvider.kt | 26 ++-- .../RaftValidatingNotaryService.kt | 13 +- .../transactions/SimpleNotaryService.kt | 5 - .../transactions/ValidatingNotaryService.kt | 4 +- node/src/main/resources/reference.conf | 4 - .../kotlin/net/corda/node/CordappSmokeTest.kt | 2 +- .../net/corda/node/CordaRPCOpsImplTest.kt | 4 +- .../corda/node/internal/CordaServiceTest.kt | 11 +- .../corda/node/services/NotaryChangeTests.kt | 8 +- .../config/FullNodeConfigurationTest.kt | 4 +- .../services/events/ScheduledFlowTests.kt | 6 +- .../statemachine/FlowFrameworkTests.kt | 87 ++------------ .../transactions/NotaryServiceTests.kt | 5 +- .../ValidatingNotaryServiceTests.kt | 6 +- samples/attachment-demo/build.gradle | 3 +- .../attachmentdemo/AttachmentDemoTest.kt | 4 +- .../kotlin/net/corda/attachmentdemo/Main.kt | 6 +- samples/bank-of-corda-demo/build.gradle | 3 +- .../net/corda/bank/BankOfCordaHttpAPITest.kt | 4 +- .../corda/bank/BankOfCordaRPCClientTest.kt | 4 +- .../net/corda/bank/BankOfCordaDriver.kt | 8 +- samples/irs-demo/build.gradle | 3 +- .../kotlin/net/corda/irs/IRSDemoTest.kt | 6 +- .../src/test/kotlin/net/corda/irs/Main.kt | 6 +- .../net/corda/netmap/simulation/Simulation.kt | 14 +-- .../net/corda/notarydemo/BFTNotaryCordform.kt | 15 +-- .../corda/notarydemo/RaftNotaryCordform.kt | 24 ++-- .../corda/notarydemo/SingleNotaryCordform.kt | 15 ++- samples/simm-valuation-demo/build.gradle | 5 +- .../net/corda/vega/SimmValuationTest.kt | 11 +- .../src/test/kotlin/net/corda/vega/Main.kt | 4 +- samples/trader-demo/build.gradle | 3 +- .../net/corda/traderdemo/TraderDemoTest.kt | 4 +- .../test/kotlin/net/corda/traderdemo/Main.kt | 4 +- .../corda/testing/FlowStackSnapshotTest.kt | 8 +- .../net/corda/testing/driver/DriverTests.kt | 6 +- .../net/corda/testing/DriverConstants.kt | 18 +-- .../kotlin/net/corda/testing/NodeTestUtils.kt | 6 +- .../kotlin/net/corda/testing/driver/Driver.kt | 62 +++++++--- .../testing/internal/demorun/CordformUtils.kt | 13 +- .../kotlin/net/corda/testing/node/MockNode.kt | 56 ++++----- .../net/corda/testing/node/NodeBasedTest.kt | 49 +++++--- .../net/corda/smoketesting/NodeConfig.kt | 33 +++-- .../net/corda/demobench/model/NodeConfig.kt | 32 ++--- .../net/corda/demobench/model/UserTest.kt | 4 +- .../net/corda/explorer/ExplorerSimulation.kt | 6 +- .../net/corda/verifier/VerifierTests.kt | 6 +- 88 files changed, 600 insertions(+), 716 deletions(-) diff --git a/build.gradle b/build.gradle index 6cfd34fed9..bf12b2ff1f 100644 --- a/build.gradle +++ b/build.gradle @@ -233,7 +233,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { networkMap "O=Controller,OU=corda,L=London,C=GB" node { name "O=Controller,OU=corda,L=London,C=GB" - advertisedServices = ["corda.notary.validating"] + notary = [validating : true] + advertisedServices = [] p2pPort 10002 cordapps = [] } diff --git a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt index ea0c88189c..e0131e6e99 100644 --- a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt +++ b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt @@ -27,9 +27,7 @@ import net.corda.finance.flows.CashExitFlow import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.driver.driver import net.corda.testing.node.DriverBasedTest @@ -59,7 +57,7 @@ class NodeMonitorModelTest : DriverBasedTest() { startFlowPermission()) ) val aliceNodeFuture = startNode(providedName = ALICE.name, rpcUsers = listOf(cashUser)) - val notaryHandle = startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))).getOrThrow() + val notaryHandle = startNotaryNode(DUMMY_NOTARY.name, validating = false).getOrThrow() val aliceNodeHandle = aliceNodeFuture.getOrThrow() aliceNode = aliceNodeHandle.nodeInfo newNode = { nodeName -> startNode(providedName = nodeName).getOrThrow().nodeInfo } 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 1862324c4f..e596a61418 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 @@ -11,9 +11,7 @@ import net.corda.finance.flows.CashPaymentFlow; import net.corda.finance.schemas.CashSchemaV1; import net.corda.node.internal.Node; import net.corda.node.internal.StartedNode; -import net.corda.node.services.transactions.ValidatingNotaryService; import net.corda.nodeapi.User; -import net.corda.nodeapi.internal.ServiceInfo; import net.corda.testing.CoreTestUtils; import net.corda.testing.node.NodeBasedTest; import org.junit.After; @@ -24,7 +22,6 @@ import java.io.IOException; import java.util.*; import java.util.concurrent.ExecutionException; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.Objects.requireNonNull; import static kotlin.test.AssertionsKt.assertEquals; @@ -53,8 +50,7 @@ public class CordaRPCJavaClientTest extends NodeBasedTest { @Before public void setUp() throws ExecutionException, InterruptedException { setCordappPackages("net.corda.finance.contracts"); - Set services = new HashSet<>(singletonList(new ServiceInfo(ValidatingNotaryService.Companion.getType(), null))); - CordaFuture> nodeFuture = startNode(getALICE().getName(), 1, services, singletonList(rpcUser), emptyMap()); + CordaFuture> nodeFuture = startNotaryNode(getALICE().getName(), singletonList(rpcUser), true); node = nodeFuture.get(); node.getInternals().registerCustomSchemas(Collections.singleton(CashSchemaV1.INSTANCE)); client = new CordaRPCClient(requireNonNull(node.getInternals().getConfiguration().getRpcAddress())); diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt index 32284a970f..422f08f8f3 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt @@ -19,9 +19,7 @@ import net.corda.finance.schemas.CashSchemaV1 import net.corda.node.internal.Node import net.corda.node.internal.StartedNode import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.User -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.ALICE import net.corda.testing.chooseIdentity import net.corda.testing.node.NodeBasedTest @@ -52,7 +50,7 @@ class CordaRPCClientTest : NodeBasedTest() { @Before fun setUp() { setCordappPackages("net.corda.finance.contracts") - node = startNode(ALICE.name, rpcUsers = listOf(rpcUser), advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))).getOrThrow() + node = startNotaryNode(ALICE.name, rpcUsers = listOf(rpcUser)).getOrThrow() node.internals.registerCustomSchemas(setOf(CashSchemaV1)) client = CordaRPCClient(node.internals.configuration.rpcAddress!!) } diff --git a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java index bc522e42f0..e9ebce530f 100644 --- a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java +++ b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java @@ -6,7 +6,6 @@ import net.corda.core.identity.CordaX500Name; import net.corda.core.identity.Party; import net.corda.core.messaging.CordaRPCOps; import net.corda.core.messaging.FlowHandle; -import net.corda.core.node.NodeInfo; import net.corda.core.utilities.OpaqueBytes; import net.corda.finance.flows.AbstractCashFlow; import net.corda.finance.flows.CashIssueFlow; @@ -41,7 +40,6 @@ public class StandaloneCordaRPCJavaClientTest { private NodeProcess notary; private CordaRPCOps rpcProxy; private CordaRPCConnection connection; - private NodeInfo notaryNode; private Party notaryNodeIdentity; private NodeConfig notaryConfig = new NodeConfig( @@ -49,8 +47,8 @@ public class StandaloneCordaRPCJavaClientTest { port.getAndIncrement(), port.getAndIncrement(), port.getAndIncrement(), - Collections.singletonList("corda.notary.validating"), - Arrays.asList(rpcUser), + true, + Collections.singletonList(rpcUser), null ); @@ -61,7 +59,6 @@ public class StandaloneCordaRPCJavaClientTest { notary = factory.create(notaryConfig); connection = notary.connect(); rpcProxy = connection.getProxy(); - notaryNode = fetchNotaryIdentity(); notaryNodeIdentity = rpcProxy.nodeInfo().getLegalIdentities().get(0); } @@ -98,11 +95,6 @@ public class StandaloneCordaRPCJavaClientTest { } } - private NodeInfo fetchNotaryIdentity() { - List nodeDataSnapshot = rpcProxy.networkMapSnapshot(); - return nodeDataSnapshot.get(0); - } - @Test public void testCashBalances() throws NoSuchFieldException, ExecutionException, InterruptedException { Amount dollars123 = new Amount<>(123, Currency.getInstance("USD")); diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt index 6da6aa3dc4..40ae8c5f16 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt @@ -64,7 +64,7 @@ class StandaloneCordaRPClientTest { p2pPort = port.andIncrement, rpcPort = port.andIncrement, webPort = port.andIncrement, - extraServices = listOf("corda.notary.validating"), + isNotary = true, users = listOf(user) ) diff --git a/config/dev/nameservernode.conf b/config/dev/nameservernode.conf index 664f2b6816..0519e56872 100644 --- a/config/dev/nameservernode.conf +++ b/config/dev/nameservernode.conf @@ -3,5 +3,7 @@ keyStorePassword : "cordacadevpass" trustStorePassword : "trustpass" p2pAddress : "localhost:10000" webAddress : "localhost:10001" -extraAdvertisedServiceIds : [ "corda.notary.validating" ] +notary : { + validating : true +} useHTTPS : false diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt index f738df62be..c4d122c649 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt @@ -13,7 +13,6 @@ import net.corda.core.node.services.NotaryService import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.node.services.UniquenessProvider import net.corda.core.serialization.CordaSerializable -import net.corda.core.transactions.FilteredTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.UntrustworthyData @@ -138,8 +137,11 @@ class NotaryFlow { // Check if transaction is intended to be signed by this notary. @Suspendable protected fun checkNotary(notary: Party?) { - if (notary !in serviceHub.myInfo.legalIdentities) + // TODO This check implies that it's OK to use the node's main identity. Shouldn't it be just limited to the + // notary identities? + if (notary !in serviceHub.myInfo.legalIdentities) { throw NotaryException(NotaryError.WrongNotary) + } } @Suspendable diff --git a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt index 8217eb69b4..792baadb39 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt @@ -116,10 +116,8 @@ interface NetworkMapCache { fun isNotary(party: Party): Boolean = party in notaryIdentities /** Checks whether a given party is an validating notary identity. */ - fun isValidatingNotary(party: Party): Boolean { - require(isNotary(party)) { "No notary found with identity $party." } - return !party.name.toString().contains("corda.notary.simple", true) // TODO This implementation will change after introducing of NetworkParameters. - } + // TODO This implementation will change after introducing of NetworkParameters. + fun isValidatingNotary(party: Party): Boolean = isNotary(party) && "validating" in party.name.commonName!! /** Clear all network map data from local node cache. */ fun clearNetworkMapCache() diff --git a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt index 71e24c506d..574008d6d8 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt @@ -13,6 +13,18 @@ import org.slf4j.Logger import java.security.PublicKey abstract class NotaryService : SingletonSerializeAsToken() { + companion object { + const val ID_PREFIX = "corda.notary." + fun constructId(validating: Boolean, raft: Boolean = false, bft: Boolean = false): String { + require(!raft || !bft) + return StringBuffer(ID_PREFIX).apply { + append(if (validating) "validating" else "simple") + if (raft) append(".raft") + if (bft) append(".bft") + }.toString() + } + } + abstract val services: ServiceHub abstract val notaryIdentityKey: PublicKey diff --git a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt index 5da5583af6..034c28c834 100644 --- a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt @@ -12,7 +12,6 @@ import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.persistence.NodeAttachmentService -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.utilities.DatabaseTransactionManager import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.ALICE @@ -117,7 +116,7 @@ class AttachmentTests { @Test fun `malicious response`() { // Make a node that doesn't do sanity checking at load time. - val aliceNode = mockNet.createNode(legalName = ALICE.name, nodeFactory = object : MockNetwork.Factory { + val aliceNode = mockNet.createNotaryNode(legalName = ALICE.name, nodeFactory = object : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, advertisedServices: Set, id: Int, notaryIdentity: Pair?, @@ -126,7 +125,7 @@ class AttachmentTests { override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false } } } - }, advertisedServices = *arrayOf(ServiceInfo(SimpleNotaryService.type))) + }, validating = false) val bobNode = mockNet.createNode(legalName = BOB.name) // Ensure that registration was successful before progressing any further diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index c996b4254b..f07f2e964e 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -20,6 +20,11 @@ UNRELEASED either annotated with the @CordaSerializable annotation or explicitly whitelisted then a NotSerializableException is thrown. +* ``extraAdvertisedServiceIds`` config has been removed as part of the previous work to retire the concept of advertised + services. The remaining use of this config was for notaries, the configuring of which has been cleaned up and simplified. + ``notaryNodeAddress``, ``notaryClusterAddresses`` and ``bftSMaRt`` have also been removed and replaced by a single + ``notary`` config object. See :doc:`corda-configuration-file` for more details. + .. _changelog_v1: Release 1.0 diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index e2c2365c92..8511b2d486 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -30,7 +30,7 @@ General node configuration file for hosting the IRSDemo services. .. literalinclude:: example-code/src/main/resources/example-node.conf :language: javascript -NetworkMapService plus Simple Notary configuration file. +Simple Notary configuration file. .. parsed-literal:: @@ -40,7 +40,9 @@ NetworkMapService plus Simple Notary configuration file. p2pAddress : "localhost:12345" rpcAddress : "localhost:12346" webAddress : "localhost:12347" - extraAdvertisedServiceIds : ["corda.notary.simple"] + notary : { + validating : false + } useHTTPS : false devMode : true // Certificate signing service will be hosted by R3 in the near future. @@ -92,19 +94,25 @@ path to the node's base directory. .. note:: The driver will not automatically create a webserver instance, but the Cordformation will. If this field is present the web server will start. -:extraAdvertisedServiceIds: A list of ServiceType id strings to be advertised to the NetworkMapService and thus be available - when other nodes query the NetworkMapCache for supporting nodes. This can also include plugin services loaded from .jar - files in the plugins folder. Optionally, a custom advertised service name can be provided by appending it to the service - type id: ``"corda.notary.validating|Notary A"`` +:notary: Optional config object which if present configures the node to run as a notary. If part of a Raft or BFT SMaRt + cluster then specify ``raft`` or ``bftSMaRt`` respectively as described below. If a single node notary then omit both. -:notaryNodeAddress: The host and port to which to bind the embedded Raft server. Required only when running a distributed - notary service. A group of Corda nodes can run a distributed notary service by each running an embedded Raft server and - joining them to the same cluster to replicate the committed state log. Note that the Raft cluster uses a separate transport - layer for communication that does not integrate with ArtemisMQ messaging services. + :validating: Boolean to determine whether the notary is a validating or non-validating one. -:notaryClusterAddresses: List of Raft cluster member addresses used to join the cluster. At least one of the specified - members must be active and be able to communicate with the cluster leader for joining. If empty, a new cluster will be - bootstrapped. Required only when running a distributed notary service. + :raft: If part of a distributed Raft cluster specify this config object, with the following settings: + + :nodeAddress: The host and port to which to bind the embedded Raft server. Note that the Raft cluster uses a + separate transport layer for communication that does not integrate with ArtemisMQ messaging services. + + :clusterAddresses: List of Raft cluster member addresses used to join the cluster. At least one of the specified + members must be active and be able to communicate with the cluster leader for joining. If empty, a new + cluster will be bootstrapped. + + :bftSMaRt: If part of a distributed BFT SMaRt cluster specify this config object, with the following settings: + + :replicaId: + + :clusterAddresses: :networkMapService: If `null`, or missing the node is declaring itself as the NetworkMapService host. Otherwise this is a config object with the details of the network map service: diff --git a/docs/source/deploying-a-node.rst b/docs/source/deploying-a-node.rst index 562df5952f..ec15b75d8b 100644 --- a/docs/source/deploying-a-node.rst +++ b/docs/source/deploying-a-node.rst @@ -20,7 +20,8 @@ notary/network map node: networkMap "O=Controller,OU=corda,L=London,C=UK" node { name "O=Controller,OU=corda,L=London,C=UK" - advertisedServices = ["corda.notary.validating"] + notary = [validating : true] + advertisedServices = [] p2pPort 10002 rpcPort 10003 webPort 10004 diff --git a/docs/source/example-code/build.gradle b/docs/source/example-code/build.gradle index cf0c3046e8..6709d9fcbe 100644 --- a/docs/source/example-code/build.gradle +++ b/docs/source/example-code/build.gradle @@ -75,7 +75,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { networkMap "O=Notary Service,OU=corda,L=London,C=GB" node { name "O=Notary Service,OU=corda,L=London,C=GB" - advertisedServices = ["corda.notary.validating"] + notary = [validating : true] + advertisedServices = [] p2pPort 10002 rpcPort 10003 webPort 10004 diff --git a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt index 07ef6d6983..c665a8cb67 100644 --- a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt +++ b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt @@ -11,8 +11,6 @@ import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.User import net.corda.testing.* import net.corda.testing.driver.driver @@ -32,10 +30,10 @@ class IntegrationTestingTutorial { val bobUser = User("bobUser", "testPassword2", permissions = setOf( startFlowPermission() )) - val (alice, bob, notary) = listOf( + val (alice, bob) = listOf( startNode(providedName = ALICE.name, rpcUsers = listOf(aliceUser)), startNode(providedName = BOB.name, rpcUsers = listOf(bobUser)), - startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))) + startNotaryNode(DUMMY_NOTARY.name) ).transpose().getOrThrow() // END 1 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/ClientRpcTutorial.kt index bf6b46be6e..5b11d79549 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/ClientRpcTutorial.kt @@ -15,9 +15,7 @@ import net.corda.finance.flows.CashExitFlow import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.User -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.ALICE import net.corda.testing.DUMMY_NOTARY import net.corda.testing.driver.driver @@ -49,7 +47,7 @@ fun main(args: Array) { startFlowPermission())) driver(driverDirectory = baseDirectory, extraCordappPackagesToScan = listOf("net.corda.finance")) { - startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))) + startNotaryNode(DUMMY_NOTARY.name) val node = startNode(providedName = ALICE.name, rpcUsers = listOf(user)).get() // END 1 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 36a98ebe8b..d313217f40 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 @@ -9,8 +9,6 @@ import net.corda.finance.contracts.getCashBalances import net.corda.finance.flows.CashIssueFlow import net.corda.finance.schemas.CashSchemaV1 import net.corda.node.internal.StartedNode -import net.corda.node.services.transactions.ValidatingNotaryService -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.node.MockNetwork import org.junit.After @@ -30,11 +28,7 @@ class CustomVaultQueryTest { fun setup() { setCordappPackages("net.corda.finance.contracts.asset") mockNet = MockNetwork(threadPerNode = true) - val notaryService = ServiceInfo(ValidatingNotaryService.type) - mockNet.createNode( - legalName = DUMMY_NOTARY.name, - notaryIdentity = notaryService to DUMMY_NOTARY_KEY, - advertisedServices = *arrayOf(notaryService)) + mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) nodeA = mockNet.createPartyNode() nodeB = mockNet.createPartyNode() 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 3152da5650..be28ddbec0 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 @@ -9,8 +9,6 @@ import net.corda.finance.contracts.getCashBalances import net.corda.finance.flows.CashIssueFlow import net.corda.finance.schemas.CashSchemaV1 import net.corda.node.internal.StartedNode -import net.corda.node.services.transactions.ValidatingNotaryService -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.node.MockNetwork import org.junit.After @@ -28,11 +26,7 @@ class FxTransactionBuildTutorialTest { fun setup() { setCordappPackages("net.corda.finance.contracts.asset") mockNet = MockNetwork(threadPerNode = true) - val notaryService = ServiceInfo(ValidatingNotaryService.type) - mockNet.createNode( - legalName = DUMMY_NOTARY.name, - notaryIdentity = notaryService to DUMMY_NOTARY_KEY, - advertisedServices = *arrayOf(notaryService)) + mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) nodeA = mockNet.createPartyNode() nodeB = mockNet.createPartyNode() nodeA.internals.registerCustomSchemas(setOf(CashSchemaV1)) 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 eed9b53759..6b9f0d279d 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,10 +9,11 @@ import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.toFuture import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode -import net.corda.node.services.transactions.ValidatingNotaryService -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.testing.* +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.chooseIdentity import net.corda.testing.node.MockNetwork +import net.corda.testing.setCordappPackages +import net.corda.testing.unsetCordappPackages import org.junit.After import org.junit.Before import org.junit.Test @@ -33,11 +34,7 @@ class WorkflowTransactionBuildTutorialTest { fun setup() { setCordappPackages("net.corda.docs") mockNet = MockNetwork(threadPerNode = true) - val notaryService = ServiceInfo(ValidatingNotaryService.type) - mockNet.createNode( - legalName = DUMMY_NOTARY.name, - notaryIdentity = Pair(notaryService, DUMMY_NOTARY_KEY), - advertisedServices = *arrayOf(notaryService)) + mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) nodeA = mockNet.createPartyNode() nodeB = mockNet.createPartyNode() nodeA.internals.registerInitiatedFlow(RecordCompletionFlow::class.java) diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index 7bf00e4771..1bfc2be2df 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -28,7 +28,8 @@ Let's take a look at the nodes we're going to deploy. Open the project's ``build networkMap "O=Controller,L=London,C=GB" node { name "O=Controller,L=London,C=GB" - advertisedServices = ["corda.notary.validating"] + notary = [validating : true] + advertisedServices = [] p2pPort 10002 rpcPort 10003 cordapps = ["net.corda:corda-finance:$corda_release_version"] diff --git a/docs/source/running-a-notary.rst b/docs/source/running-a-notary.rst index 32b62359a8..57ce2d0964 100644 --- a/docs/source/running-a-notary.rst +++ b/docs/source/running-a-notary.rst @@ -11,20 +11,20 @@ At present we have several prototype notary implementations: we are using the `Copycat `_ framework 4. ``RaftValidatingNotaryService`` (distributed) -- as above, but performs validation on the transactions received -To have a node run a notary service, you need to set appropriate configuration values before starting it +To have a node run a notary service, you need to set appropriate ``notary`` configuration before starting it (see :doc:`corda-configuration-file` for reference). -For ``SimpleNotaryService``, simply add the following service id to the list of advertised services: +For ``SimpleNotaryService`` the config is simply: .. parsed-literal:: - extraAdvertisedServiceIds : [ "net.corda.notary.simple" ] + notary : { validating : false } For ``ValidatingNotaryService``, it is: .. parsed-literal:: - extraAdvertisedServiceIds : [ "net.corda.notary.validating" ] + notary : { validating : true } Setting up a Raft notary is currently slightly more involved and is not recommended for prototyping purposes. There is work in progress to simplify it. To see it in action, however, you can try out the :ref:`notary-demo`. diff --git a/docs/source/tutorial-custom-notary.rst b/docs/source/tutorial-custom-notary.rst index 190d51533a..01e5da1b71 100644 --- a/docs/source/tutorial-custom-notary.rst +++ b/docs/source/tutorial-custom-notary.rst @@ -24,9 +24,3 @@ as ``ValidatingNotaryFlow``, ``NonValidatingNotaryFlow``, or implement your own :language: kotlin :start-after: START 2 :end-before: END 2 - -To ensure the custom notary is installed and advertised by the node, specify it in the configuration file: - -.. parsed-literal:: - - extraAdvertisedServiceIds : ["corda.notary.validating.mycustom"] diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java index d219c52281..7ecaa9fce9 100644 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java +++ b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java @@ -1,9 +1,7 @@ package net.corda.cordform; import static java.util.Collections.emptyList; -import com.typesafe.config.Config; -import com.typesafe.config.ConfigFactory; -import com.typesafe.config.ConfigValueFactory; +import com.typesafe.config.*; import java.util.Collections; import java.util.List; import java.util.Map; @@ -30,11 +28,6 @@ public class CordformNode implements NodeDefinition { */ public List advertisedServices = emptyList(); - /** - * If running a Raft notary cluster, the address of at least one node in the cluster, or leave blank to start a new cluster. - * If running a BFT notary cluster, the addresses of all nodes in the cluster. - */ - public List notaryClusterAddresses = emptyList(); /** * Set the RPC users for this node. This configuration block allows arbitrary configuration. * The recommended current structure is: @@ -45,6 +38,12 @@ public class CordformNode implements NodeDefinition { */ public List> rpcUsers = emptyList(); + /** + * Apply the notary configuration if this node is a notary. The map is the config structure of + * net.corda.node.services.config.NotaryConfig + */ + public Map notary = null; + protected Config config = ConfigFactory.empty(); public Config getConfig() { @@ -78,20 +77,4 @@ public class CordformNode implements NodeDefinition { public void rpcPort(Integer rpcPort) { config = config.withValue("rpcAddress", ConfigValueFactory.fromAnyRef(DEFAULT_HOST + ':' + rpcPort)); } - - /** - * Set the port which to bind the Copycat (Raft) node to. - * - * @param notaryPort The Raft port. - */ - public void notaryNodePort(Integer notaryPort) { - config = config.withValue("notaryNodeAddress", ConfigValueFactory.fromAnyRef(DEFAULT_HOST + ':' + notaryPort)); - } - - /** - * @param id The (0-based) BFT replica ID. - */ - public void bftReplicaId(Integer id) { - config = config.withValue("bftSMaRt", ConfigValueFactory.fromMap(Collections.singletonMap("replicaId", id))); - } } diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy index 19e3a6b9b3..262f4df38c 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy @@ -99,7 +99,7 @@ class Node extends CordformNode { } protected void build() { - configureRpcUsers() + configureProperties() installCordaJar() if (config.hasPath("webAddress")) { installWebserverJar() @@ -118,11 +118,12 @@ class Node extends CordformNode { return config.getString("p2pAddress") } - /** - * Write the RPC users to the config - */ - private void configureRpcUsers() { + private void configureProperties() { config = config.withValue("rpcUsers", ConfigValueFactory.fromIterable(rpcUsers)) + if (notary) { + config = config.withValue("notary", ConfigValueFactory.fromMap(notary)) + } + config = config.withValue('extraAdvertisedServiceIds', ConfigValueFactory.fromIterable(advertisedServices*.toString())) } /** @@ -177,11 +178,6 @@ class Node extends CordformNode { * Installs the configuration file to this node's directory and detokenises it. */ private void installConfig() { - // Adding required default values - config = config.withValue('extraAdvertisedServiceIds', ConfigValueFactory.fromIterable(advertisedServices*.toString())) - if (notaryClusterAddresses.size() > 0) { - config = config.withValue('notaryClusterAddresses', ConfigValueFactory.fromIterable(notaryClusterAddresses*.toString())) - } def configFileText = config.root().render(new ConfigRenderOptions(false, false, true, false)).split("\n").toList() // Need to write a temporary file first to use the project.copy, which resolves directories correctly. diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/User.kt b/node-api/src/main/kotlin/net/corda/nodeapi/User.kt index 0c0d259ab2..aa86e64414 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/User.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/User.kt @@ -1,6 +1,7 @@ package net.corda.nodeapi import net.corda.nodeapi.config.OldConfig +import net.corda.nodeapi.config.toConfig data class User( @OldConfig("user") @@ -8,9 +9,6 @@ data class User( val password: String, val permissions: Set) { override fun toString(): String = "${javaClass.simpleName}($username, permissions=$permissions)" - fun toMap() = mapOf( - "username" to username, - "password" to password, - "permissions" to permissions - ) + @Deprecated("Use toConfig().root().unwrapped() instead") + fun toMap(): Map = toConfig().root().unwrapped() } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt index 1463229b13..78283ea654 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt @@ -2,18 +2,23 @@ package net.corda.nodeapi.config import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigUtil +import com.typesafe.config.ConfigValueFactory import net.corda.core.identity.CordaX500Name import net.corda.core.internal.noneOrSingle import net.corda.core.internal.uncheckedCast import net.corda.core.utilities.NetworkHostAndPort import org.slf4j.LoggerFactory +import java.lang.reflect.Field +import java.lang.reflect.ParameterizedType import java.net.Proxy import java.net.URL import java.nio.file.Path import java.nio.file.Paths import java.time.Instant import java.time.LocalDate +import java.time.temporal.Temporal import java.util.* import kotlin.reflect.KClass import kotlin.reflect.KProperty @@ -71,8 +76,8 @@ private fun Config.getSingleValue(path: String, type: KType): Any? { NetworkHostAndPort::class -> NetworkHostAndPort.parse(getString(path)) Path::class -> Paths.get(getString(path)) URL::class -> URL(getString(path)) - Properties::class -> getConfig(path).toProperties() CordaX500Name::class -> CordaX500Name.parse(getString(path)) + Properties::class -> getConfig(path).toProperties() else -> if (typeClass.java.isEnum) { parseEnum(typeClass.java, getString(path)) } else { @@ -126,4 +131,65 @@ private fun parseEnum(enumType: Class<*>, name: String): Enum<*> = enumBridge> enumBridge(clazz: Class, name: String): T = java.lang.Enum.valueOf(clazz, name) +/** + * Convert the receiver object into a [Config]. This does the inverse action of [parseAs]. + */ +fun Any.toConfig(): Config = ConfigValueFactory.fromMap(toConfigMap()).toConfig() + +@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") +// Reflect over the fields of the receiver and generate a value Map that can use to create Config object. +private fun Any.toConfigMap(): Map { + val values = HashMap() + for (field in javaClass.declaredFields) { + if (field.isSynthetic) continue + field.isAccessible = true + val value = field.get(this) ?: continue + val configValue = if (value is String || value is Boolean || value is Number) { + // These types are supported by Config as use as is + value + } else if (value is Temporal || value is NetworkHostAndPort || value is CordaX500Name || value is Path || value is URL) { + // These types make sense to be represented as Strings and the exact inverse parsing function for use in parseAs + value.toString() + } else if (value is Enum<*>) { + // Expicitly use the Enum's name in case the toString is overridden, which would make parsing problematic. + value.name + } else if (value is Properties) { + // For Properties we treat keys with . as nested configs + ConfigFactory.parseMap(uncheckedCast(value)).root() + } else if (value is Iterable<*>) { + value.toConfigIterable(field) + } else { + // Else this is a custom object recursed over + value.toConfigMap() + } + values[field.name] = configValue + } + return values +} + +// For Iterables figure out the type parameter and apply the same logic as above on the individual elements. +private fun Iterable<*>.toConfigIterable(field: Field): Iterable { + val elementType = (field.genericType as ParameterizedType).actualTypeArguments[0] as Class<*> + return when (elementType) { + // For the types already supported by Config we can use the Iterable as is + String::class.java -> this + Integer::class.java -> this + java.lang.Long::class.java -> this + java.lang.Double::class.java -> this + java.lang.Boolean::class.java -> this + LocalDate::class.java -> map(Any?::toString) + Instant::class.java -> map(Any?::toString) + NetworkHostAndPort::class.java -> map(Any?::toString) + Path::class.java -> map(Any?::toString) + URL::class.java -> map(Any?::toString) + CordaX500Name::class.java -> map(Any?::toString) + Properties::class.java -> map { ConfigFactory.parseMap(uncheckedCast(it)).root() } + else -> if (elementType.isEnum) { + map { (it as Enum<*>).name } + } else { + map { it?.toConfigMap() } + } + } +} + private val logger = LoggerFactory.getLogger("net.corda.nodeapi.config") diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt index e13c47ce05..8737ac9160 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt @@ -76,54 +76,66 @@ class ConfigParsingTest { testPropertyType(URL("http://localhost:1234"), URL("http://localhost:1235"), valuesToString = true) } + @Test + fun CordaX500Name() { + testPropertyType( + CordaX500Name(organisation = "Mock Party", locality = "London", country = "GB"), + CordaX500Name(organisation = "Mock Party 2", locality = "London", country = "GB"), + valuesToString = true) + } + @Test fun `flat Properties`() { val config = config("value" to mapOf("key" to "prop")) - assertThat(config.parseAs().value).isEqualTo(Properties().apply { this["key"] = "prop" }) + val data = PropertiesData(Properties().apply { this["key"] = "prop" }) + assertThat(config.parseAs()).isEqualTo(data) + assertThat(data.toConfig()).isEqualTo(config) } @Test fun `Properties key with dot`() { val config = config("value" to mapOf("key.key2" to "prop")) - assertThat(config.parseAs().value).isEqualTo(Properties().apply { this["key.key2"] = "prop" }) + val data = PropertiesData(Properties().apply { this["key.key2"] = "prop" }) + assertThat(config.parseAs().value).isEqualTo(data.value) } @Test fun `nested Properties`() { val config = config("value" to mapOf("first" to mapOf("second" to "prop"))) - assertThat(config.parseAs().value).isEqualTo(Properties().apply { this["first.second"] = "prop" }) + val data = PropertiesData(Properties().apply { this["first.second"] = "prop" }) + assertThat(config.parseAs().value).isEqualTo(data.value) + assertThat(data.toConfig()).isEqualTo(config) } @Test fun `List of Properties`() { val config = config("values" to listOf(emptyMap(), mapOf("key" to "prop"))) - assertThat(config.parseAs().values).containsExactly( + val data = PropertiesListData(listOf( Properties(), - Properties().apply { this["key"] = "prop" }) + Properties().apply { this["key"] = "prop" })) + assertThat(config.parseAs().values).isEqualTo(data.values) + assertThat(data.toConfig()).isEqualTo(config) } @Test fun `Set`() { - val config = config("values" to listOf("a", "a", "b")) - assertThat(config.parseAs().values).containsOnly("a", "b") + val data = StringSetData(setOf("a", "b")) + assertThat(config("values" to listOf("a", "a", "b")).parseAs()).isEqualTo(data) + assertThat(data.toConfig()).isEqualTo(config("values" to listOf("a", "b"))) assertThat(empty().parseAs().values).isEmpty() } - @Test - fun x500Name() { - testPropertyType(CordaX500Name(organisation = "Mock Party", locality = "London", country = "GB"), CordaX500Name(organisation = "Mock Party 2", locality = "London", country = "GB"), valuesToString = true) - } - @Test fun `multi property data class`() { - val data = config( + val config = config( "b" to true, "i" to 123, "l" to listOf("a", "b")) - .parseAs() + val data = config.parseAs() assertThat(data.i).isEqualTo(123) assertThat(data.b).isTrue() assertThat(data.l).containsExactly("a", "b") + assertThat(data.toConfig()).isEqualTo(config) } @Test @@ -133,6 +145,7 @@ class ConfigParsingTest { "value" to "nested")) val data = NestedData(StringData("nested")) assertThat(config.parseAs()).isEqualTo(data) + assertThat(data.toConfig()).isEqualTo(config) } @Test @@ -143,12 +156,14 @@ class ConfigParsingTest { mapOf("value" to "2"))) val data = DataListData(listOf(StringData("1"), StringData("2"))) assertThat(config.parseAs()).isEqualTo(data) + assertThat(data.toConfig()).isEqualTo(config) } @Test fun `default value property`() { assertThat(config("a" to 3).parseAs()).isEqualTo(DefaultData(3, 2)) assertThat(config("a" to 3, "defaultOfTwo" to 3).parseAs()).isEqualTo(DefaultData(3, 3)) + assertThat(DefaultData(3).toConfig()).isEqualTo(config("a" to 3, "defaultOfTwo" to 2)) } @Test @@ -156,12 +171,14 @@ class ConfigParsingTest { assertThat(empty().parseAs().nullable).isNull() assertThat(config("nullable" to null).parseAs().nullable).isNull() assertThat(config("nullable" to "not null").parseAs().nullable).isEqualTo("not null") + assertThat(NullableData(null).toConfig()).isEqualTo(empty()) } @Test fun `old config property`() { assertThat(config("oldValue" to "old").parseAs().newValue).isEqualTo("old") assertThat(config("newValue" to "new").parseAs().newValue).isEqualTo("new") + assertThat(OldData("old").toConfig()).isEqualTo(config("newValue" to "old")) } private inline fun , reified L : ListData, V : Any> testPropertyType( @@ -177,6 +194,7 @@ class ConfigParsingTest { val config = config("value" to if (valueToString) value.toString() else value) val data = constructor.call(value) assertThat(config.parseAs().value).isEqualTo(data.value) + assertThat(data.toConfig()).isEqualTo(config) } private inline fun , V : Any> testListProperty(value1: V, value2: V, valuesToString: Boolean) { @@ -187,6 +205,7 @@ class ConfigParsingTest { val config = config("values" to configValues.take(n)) val data = constructor.call(rawValues.take(n)) assertThat(config.parseAs().values).isEqualTo(data.values) + assertThat(data.toConfig()).isEqualTo(config) } assertThat(empty().parseAs().values).isEmpty() } @@ -228,8 +247,8 @@ class ConfigParsingTest { data class PathListData(override val values: List) : ListData data class URLData(override val value: URL) : SingleData data class URLListData(override val values: List) : ListData - data class X500NameData(override val value: CordaX500Name) : SingleData - data class X500NameListData(override val values: List) : ListData + data class CordaX500NameData(override val value: CordaX500Name) : SingleData + data class CordaX500NameListData(override val values: List) : ListData data class PropertiesData(override val value: Properties) : SingleData data class PropertiesListData(override val values: List) : ListData data class MultiPropertyData(val i: Int, val b: Boolean, val l: List) @@ -242,5 +261,4 @@ class ConfigParsingTest { val newValue: String) enum class TestEnum { Value1, Value2 } - } \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt index e2d9648556..211f0e1f15 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt @@ -7,14 +7,14 @@ import net.corda.core.flows.StartableByRPC import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.startFlow import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.minutes import net.corda.finance.DOLLARS import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.chooseIdentity import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver @@ -103,10 +103,10 @@ class NodePerformanceTests { @Test fun `self pay rate`() { driver(startNodesInProcess = true) { - val a = startNode( - rpcUsers = listOf(User("A", "A", setOf(startFlowPermission(), startFlowPermission()))), - advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)) - ).get() + val a = startNotaryNode( + DUMMY_NOTARY.name, + rpcUsers = listOf(User("A", "A", setOf(startFlowPermission(), startFlowPermission()))) + ).getOrThrow() a as NodeHandle.InProcess val metricRegistry = startReporter(shutdownManager, a.node.services.monitoringService.metrics) a.rpcClientToNode().use("A", "A") { connection -> 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 fd803ee9ef..bb177abfd9 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 @@ -18,9 +18,7 @@ import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.loggerFor import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_NOTARY import net.corda.testing.TestDependencyInjectionBase @@ -120,7 +118,7 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() { val nodes = listOf( startNode(providedName = bankAName, rpcUsers = listOf(adminUser)), startNode(providedName = bankBName, rpcUsers = listOf(adminUser)), - startNode(providedName = notaryName, rpcUsers = listOf(adminUser), advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) + startNotaryNode(providedName = notaryName, rpcUsers = listOf(adminUser), validating = false) ).transpose().getOrThrow() // Wait for all nodes to start up. nodes.forEach { it.rpc.waitUntilNetworkReady().getOrThrow() } return nodes diff --git a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt index c014788530..9a6a5c3112 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt @@ -10,6 +10,7 @@ import net.corda.core.flows.NotaryException import net.corda.core.flows.NotaryFlow import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.internal.deleteIfExists import net.corda.core.internal.div import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder @@ -18,11 +19,11 @@ import net.corda.core.utilities.Try import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode import net.corda.node.services.config.BFTSMaRtConfiguration +import net.corda.node.services.config.NotaryConfig import net.corda.node.services.transactions.BFTNonValidatingNotaryService import net.corda.node.services.transactions.minClusterSize import net.corda.node.services.transactions.minCorrectReplicas import net.corda.node.utilities.ServiceIdentityGenerator -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.chooseIdentity import net.corda.testing.contracts.DummyContract import net.corda.testing.dummyCommand @@ -30,14 +31,13 @@ import net.corda.testing.getDefaultNotary import net.corda.testing.node.MockNetwork import org.junit.After import org.junit.Test -import java.nio.file.Files +import java.nio.file.Paths import kotlin.test.assertEquals import kotlin.test.assertTrue class BFTNotaryServiceTests { companion object { - private val serviceType = BFTNonValidatingNotaryService.type - private val clusterName = CordaX500Name(serviceType.id, "BFT", "Zurich", "CH") + private val clusterName = CordaX500Name(BFTNonValidatingNotaryService.id, "BFT", "Zurich", "CH") } private val mockNet = MockNetwork() @@ -49,20 +49,17 @@ class BFTNotaryServiceTests { } private fun bftNotaryCluster(clusterSize: Int, exposeRaces: Boolean = false) { - Files.deleteIfExists("config" / "currentView") // XXX: Make config object warn if this exists? + (Paths.get("config") / "currentView").deleteIfExists() // XXX: Make config object warn if this exists? val replicaIds = (0 until clusterSize) ServiceIdentityGenerator.generateToDisk( replicaIds.map { mockNet.baseDirectory(mockNet.nextNodeId + it) }, clusterName) - val bftNotaryService = ServiceInfo(serviceType, clusterName) - val notaryClusterAddresses = replicaIds.map { NetworkHostAndPort("localhost", 11000 + it * 10) } + val clusterAddresses = replicaIds.map { NetworkHostAndPort("localhost", 11000 + it * 10) } replicaIds.forEach { replicaId -> - mockNet.createNode( - advertisedServices = bftNotaryService, - configOverrides = { - whenever(it.bftSMaRt).thenReturn(BFTSMaRtConfiguration(replicaId, false, exposeRaces)) - whenever(it.notaryClusterAddresses).thenReturn(notaryClusterAddresses) - }) + mockNet.createNode(configOverrides = { + val notary = NotaryConfig(validating = false, bftSMaRt = BFTSMaRtConfiguration(replicaId, clusterAddresses, exposeRaces = exposeRaces)) + whenever(it.notary).thenReturn(notary) + }) } mockNet.runNetwork() // Exchange initial network map registration messages. } diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt index 69915138dd..06ed089e16 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt @@ -39,10 +39,9 @@ class DistributedServiceTests : DriverBasedTest() { ) val aliceFuture = startNode(providedName = ALICE.name, rpcUsers = listOf(testUser)) val notariesFuture = startNotaryCluster( - DUMMY_NOTARY.name.copy(commonName = RaftValidatingNotaryService.type.id), + DUMMY_NOTARY.name.copy(commonName = RaftValidatingNotaryService.id), rpcUsers = listOf(testUser), - clusterSize = clusterSize, - type = RaftValidatingNotaryService.type + clusterSize = clusterSize ) alice = aliceFuture.get() diff --git a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt index c81b86195c..0b5d72d858 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt @@ -14,8 +14,6 @@ import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.testing.* -import net.corda.testing.DUMMY_BANK_A -import net.corda.testing.chooseIdentity import net.corda.testing.contracts.DummyContract import net.corda.testing.node.NodeBasedTest import org.junit.After @@ -26,7 +24,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith class RaftNotaryServiceTests : NodeBasedTest() { - private val notaryName = CordaX500Name(RaftValidatingNotaryService.type.id, "RAFT Notary Service", "London", "GB") + private val notaryName = CordaX500Name(RaftValidatingNotaryService.id, "RAFT Notary Service", "London", "GB") @Before fun setup() { 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 786e5c0af8..5503d5ca19 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 @@ -17,9 +17,6 @@ import net.corda.core.utilities.seconds import net.corda.node.internal.StartedNode import net.corda.node.services.messaging.* import net.corda.node.services.transactions.RaftValidatingNotaryService -import net.corda.node.services.transactions.SimpleNotaryService -import net.corda.node.utilities.ServiceIdentityGenerator -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.node.NodeBasedTest import org.assertj.core.api.Assertions.assertThat @@ -32,8 +29,7 @@ import java.util.concurrent.atomic.AtomicInteger class P2PMessagingTest : NodeBasedTest() { private companion object { - val DISTRIBUTED_SERVICE_NAME = CordaX500Name(RaftValidatingNotaryService.type.id, "DistributedService", "London", "GB") - val SERVICE_2_NAME = CordaX500Name(organisation = "Service 2", locality = "London", country = "GB") + val DISTRIBUTED_SERVICE_NAME = CordaX500Name(RaftValidatingNotaryService.id, "DistributedService", "London", "GB") } @Test @@ -49,46 +45,6 @@ class P2PMessagingTest : NodeBasedTest() { startNodes().getOrThrow(timeout = startUpDuration * 3) } - // https://github.com/corda/corda/issues/71 - @Test - fun `communicating with a service running on the network map node`() { - startNetworkMapNode(advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) - networkMapNode.respondWith("Hello") - val alice = startNode(ALICE.name).getOrThrow() - val serviceAddress = alice.services.networkMapCache.run { - val notaryParty = notaryIdentities.randomOrNull()!! - alice.network.getAddressOfParty(getPartyInfo(notaryParty)!!) - } - val received = alice.receiveFrom(serviceAddress).getOrThrow(10.seconds) - assertThat(received).isEqualTo("Hello") - } - - // TODO Use a dummy distributed service - @Test - fun `communicating with a distributed service which the network map node is part of`() { - ServiceIdentityGenerator.generateToDisk( - listOf(DUMMY_MAP.name, SERVICE_2_NAME).map { baseDirectory(it) }, - DISTRIBUTED_SERVICE_NAME) - - val distributedService = ServiceInfo(RaftValidatingNotaryService.type, DISTRIBUTED_SERVICE_NAME) - val notaryClusterAddress = freeLocalHostAndPort() - startNetworkMapNode( - DUMMY_MAP.name, - advertisedServices = setOf(distributedService), - configOverrides = mapOf("notaryNodeAddress" to notaryClusterAddress.toString())) - val (serviceNode2, alice) = listOf( - startNode( - SERVICE_2_NAME, - advertisedServices = setOf(distributedService), - configOverrides = mapOf( - "notaryNodeAddress" to freeLocalHostAndPort().toString(), - "notaryClusterAddresses" to listOf(notaryClusterAddress.toString()))), - startNode(ALICE.name) - ).transpose().getOrThrow() - - assertAllNodesAreUsed(listOf(networkMapNode, serviceNode2), DISTRIBUTED_SERVICE_NAME, alice) - } - @Ignore @Test fun `communicating with a distributed service which we're part of`() { diff --git a/node/src/integration-test/kotlin/net/corda/test/node/NodeStatePersistenceTests.kt b/node/src/integration-test/kotlin/net/corda/test/node/NodeStatePersistenceTests.kt index efd07f1a55..0e8f062c9c 100644 --- a/node/src/integration-test/kotlin/net/corda/test/node/NodeStatePersistenceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/test/node/NodeStatePersistenceTests.kt @@ -18,8 +18,6 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.getOrThrow import net.corda.node.services.FlowPermissions -import net.corda.node.services.transactions.SimpleNotaryService -import net.corda.nodeapi.internal.ServiceInfo import net.corda.nodeapi.User import net.corda.testing.DUMMY_NOTARY import net.corda.testing.chooseIdentity @@ -38,8 +36,7 @@ class NodeStatePersistenceTests { val user = User("mark", "dadada", setOf(FlowPermissions.startFlowPermission())) val message = Message("Hello world!") driver(isDebug = true, startNodesInProcess = isQuasarAgentSpecified()) { - - startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))).getOrThrow() + startNotaryNode(DUMMY_NOTARY.name, validating = false).getOrThrow() var nodeHandle = startNode(rpcUsers = listOf(user)).getOrThrow() val nodeName = nodeHandle.nodeInfo.chooseIdentity().name nodeHandle.rpcClientToNode().start(user.username, user.password).use { 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 ba3f1d06b6..8e9d219d66 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -1,7 +1,6 @@ package net.corda.node.internal import com.codahale.metrics.MetricRegistry -import com.google.common.collect.Lists import com.google.common.collect.MutableClassToInstanceMap import com.google.common.util.concurrent.MoreExecutors import net.corda.confidential.SwapIdentitiesFlow @@ -41,7 +40,9 @@ 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.BFTSMaRtConfiguration import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.NotaryConfig import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.events.NodeSchedulerService import net.corda.node.services.events.ScheduledActivityObserver @@ -66,7 +67,6 @@ import net.corda.node.services.vault.VaultSoftLockManager import net.corda.node.utilities.* import net.corda.node.utilities.AddOrRemove.ADD import net.corda.nodeapi.internal.ServiceInfo -import net.corda.nodeapi.internal.ServiceType import org.apache.activemq.artemis.utils.ReusableLatch import org.slf4j.Logger import rx.Observable @@ -456,7 +456,7 @@ abstract class AbstractNode(config: NodeConfiguration, _services = ServiceHubInternalImpl(schemaService) attachments = NodeAttachmentService(services.monitoringService.metrics) cordappProvider.start(attachments) - legalIdentity = obtainIdentity() + legalIdentity = obtainIdentity(notaryConfig = null) network = makeMessagingService(legalIdentity) info = makeInfo(legalIdentity) val networkMapCache = services.networkMapCache @@ -500,19 +500,10 @@ abstract class AbstractNode(config: NodeConfiguration, } /** - * A service entry contains the advertised [ServiceInfo] along with the service identity. The identity *name* is - * taken from the configuration or, if non specified, generated by combining the node's legal name and the service id. - * Used only for notary identities. + * Obtain the node's notary identity if it's configured to be one. If part of a distributed notary then this will be + * the distributed identity shared across all the nodes of the cluster. */ - protected open fun getNotaryIdentity(): PartyAndCertificate? { - return advertisedServices.singleOrNull { it.type.isNotary() }?.let { - it.name?.let { - require(it.commonName != null) {"Common name in '$it' must not be null for notary service, use service type id as common name."} - require(ServiceType.parse(it.commonName!!).isNotary()) {"Common name for notary service in '$it' must be the notary service type id."} - } - obtainIdentity(it) - } - } + protected fun getNotaryIdentity(): PartyAndCertificate? = configuration.notary?.let { obtainIdentity(it) } @VisibleForTesting protected open fun acceptableLiveFiberCountOnStop(): Int = 0 @@ -558,22 +549,14 @@ abstract class AbstractNode(config: NodeConfiguration, } private fun makeNetworkServices(network: MessagingService, networkMapCache: NetworkMapCacheInternal, tokenizableServices: MutableList) { - val serviceTypes = advertisedServices.map { it.type } inNodeNetworkMapService = if (configuration.networkMapService == null) makeNetworkMapService(network, networkMapCache) else NullNetworkMapService - val notaryServiceType = serviceTypes.singleOrNull { it.isNotary() } - if (notaryServiceType != null) { - val service = makeCoreNotaryService(notaryServiceType) - if (service != null) { - service.apply { - tokenizableServices.add(this) - runOnStop += this::stop - start() - } - installCoreFlow(NotaryFlow.Client::class, service::createServiceFlow) - } else { - log.info("Notary type ${notaryServiceType.id} does not match any built-in notary types. " + - "It is expected to be loaded via a CorDapp") - } + configuration.notary?.let { + val notaryService = makeCoreNotaryService(it) + tokenizableServices.add(notaryService) + runOnStop += notaryService::stop + installCoreFlow(NotaryFlow.Client::class, notaryService::createServiceFlow) + log.info("Running core notary: ${notaryService.javaClass.name}") + notaryService.start() } } @@ -640,15 +623,33 @@ abstract class AbstractNode(config: NodeConfiguration, abstract protected fun makeNetworkMapService(network: MessagingService, networkMapCache: NetworkMapCacheInternal): NetworkMapService - open protected fun makeCoreNotaryService(type: ServiceType): NotaryService? { - check(myNotaryIdentity != null) { "No notary identity initialized when creating a notary service" } - return when (type) { - SimpleNotaryService.type -> SimpleNotaryService(services, myNotaryIdentity!!.owningKey) - ValidatingNotaryService.type -> ValidatingNotaryService(services, myNotaryIdentity!!.owningKey) - RaftNonValidatingNotaryService.type -> RaftNonValidatingNotaryService(services, myNotaryIdentity!!.owningKey) - RaftValidatingNotaryService.type -> RaftValidatingNotaryService(services, myNotaryIdentity!!.owningKey) - BFTNonValidatingNotaryService.type -> BFTNonValidatingNotaryService(services, myNotaryIdentity!!.owningKey) - else -> null + private fun makeCoreNotaryService(notaryConfig: NotaryConfig): NotaryService { + val notaryKey = myNotaryIdentity?.owningKey ?: throw IllegalArgumentException("No notary identity initialized when creating a notary service") + return if (notaryConfig.validating) { + if (notaryConfig.raft != null) { + RaftValidatingNotaryService(services, notaryKey, notaryConfig.raft) + } else if (notaryConfig.bftSMaRt != null) { + throw IllegalArgumentException("Validating BFTSMaRt notary not supported") + } else { + ValidatingNotaryService(services, notaryKey) + } + } else { + if (notaryConfig.raft != null) { + RaftNonValidatingNotaryService(services, notaryKey, notaryConfig.raft) + } else if (notaryConfig.bftSMaRt != null) { + val cluster = makeBFTCluster(notaryKey, notaryConfig.bftSMaRt) + BFTNonValidatingNotaryService(services, notaryKey, notaryConfig.bftSMaRt, cluster) + } else { + SimpleNotaryService(services, notaryKey) + } + } + } + + protected open fun makeBFTCluster(notaryKey: PublicKey, bftSMaRtConfig: BFTSMaRtConfiguration): BFTSMaRt.Cluster { + return object : BFTSMaRt.Cluster { + override fun waitUntilAllReplicasHaveInitialized() { + log.warn("A BFT replica may still be initializing, in which case the upcoming consensus change may cause it to spin.") + } } } @@ -691,29 +692,32 @@ abstract class AbstractNode(config: NodeConfiguration, protected abstract fun startMessagingService(rpcOps: RPCOps) - private fun obtainIdentity(serviceInfo: ServiceInfo? = null): PartyAndCertificate { - // Load the private identity key, creating it if necessary. The identity key is a long term well known key that - // is distributed to other peers and we use it (or a key signed by it) when we need to do something - // "permissioned". The identity file is what gets distributed and contains the node's legal name along with - // the public key. Obviously in a real system this would need to be a certificate chain of some kind to ensure - // the legal name is actually validated in some way. + private fun obtainIdentity(notaryConfig: NotaryConfig?): PartyAndCertificate { val keyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword) - val (id, name) = if (serviceInfo == null) { - // Create node identity if service info = null + val (id, singleName) = if (notaryConfig == null) { + // Node's main identity Pair("identity", myLegalName) } else { - val name = serviceInfo.name ?: myLegalName.copy(commonName = serviceInfo.type.id) - Pair(serviceInfo.type.id, name) + val notaryId = notaryConfig.run { NotaryService.constructId(validating, raft != null, bftSMaRt != null) } + if (notaryConfig.bftSMaRt == null && notaryConfig.raft == null) { + // Node's notary identity + Pair(notaryId, myLegalName.copy(commonName = notaryId)) + } else { + // The node is part of a distributed notary whose identity must already be generated beforehand + Pair(notaryId, null) + } } // TODO: Integrate with Key management service? val privateKeyAlias = "$id-private-key" if (!keyStore.containsAlias(privateKeyAlias)) { + singleName ?: throw IllegalArgumentException( + "Unable to find in the key store the identity of the distributed notary ($id) the node is part of") // TODO: Remove use of [ServiceIdentityGenerator.generateToDisk]. log.info("$privateKeyAlias not found in key store ${configuration.nodeKeystore}, generating fresh key!") - keyStore.signAndSaveNewKeyPair(name, privateKeyAlias, generateKeyPair()) + keyStore.signAndSaveNewKeyPair(singleName, privateKeyAlias, generateKeyPair()) } val (x509Cert, keys) = keyStore.certificateAndKeyPair(privateKeyAlias) @@ -726,7 +730,7 @@ abstract class AbstractNode(config: NodeConfiguration, // 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. - Lists.asList(certificate, keyStore.getCertificateChain(privateKeyAlias).drop(1).toTypedArray()) + listOf(certificate) + keyStore.getCertificateChain(privateKeyAlias).drop(1) } else { keyStore.getCertificateChain(privateKeyAlias).let { check(it[0].toX509CertHolder() == x509Cert) { "Certificates from key store do not line up!" } @@ -736,8 +740,11 @@ abstract class AbstractNode(config: NodeConfiguration, val nodeCert = certificates[0] as? X509Certificate ?: throw ConfigurationException("Node certificate must be an X.509 certificate") val subject = CordaX500Name.build(nodeCert.subjectX500Principal) - if (subject != name) - throw ConfigurationException("The name '$name' for $id doesn't match what's in the key store: $subject") + // TODO Include the name of the distributed notary, which the node is part of, in the notary config so that we + // can cross-check the identity we get from the key store + if (singleName != null && subject != singleName) { + throw ConfigurationException("The name '$singleName' for $id doesn't match what's in the key store: $subject") + } partyKeys += keys return PartyAndCertificate(CertificateFactory.getInstance("X509").generateCertPath(certificates)) 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 6413daa3ee..631361cd04 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -99,7 +99,7 @@ open class NodeStartup(val args: Array) { return } val startedNode = node.start() - printPluginsAndServices(startedNode.internals) + Node.printBasicNodeInfo("Loaded CorDapps", startedNode.internals.cordappProvider.cordapps.joinToString { it.name }) startedNode.internals.nodeReadyFuture.thenMatch({ val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0 val name = startedNode.info.legalIdentitiesAndCerts.first().name.organisation @@ -165,7 +165,7 @@ open class NodeStartup(val args: Array) { } open protected fun banJavaSerialisation(conf: FullNodeConfiguration) { - SerialFilter.install(if (conf.bftSMaRt.isValid()) ::bftSMaRtSerialFilter else ::defaultSerialFilter) + SerialFilter.install(if (conf.notary?.bftSMaRt != null) ::bftSMaRtSerialFilter else ::defaultSerialFilter) } open protected fun getVersionInfo(): VersionInfo { @@ -263,13 +263,6 @@ open class NodeStartup(val args: Array) { } } - private fun printPluginsAndServices(node: Node) { - node.configuration.extraAdvertisedServiceIds.filter { it.startsWith("corda.notary.") }.let { - if (it.isNotEmpty()) Node.printBasicNodeInfo("Providing additional services", it.joinToString()) - } - Node.printBasicNodeInfo("Loaded CorDapps", node.cordappProvider.cordapps.joinToString { it.name }) - } - 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() 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 ba865ecaba..b15e768f19 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 @@ -6,17 +6,11 @@ import net.corda.node.internal.NetworkMapInfo import net.corda.node.services.messaging.CertificateChainCheckPolicy import net.corda.nodeapi.User import net.corda.nodeapi.config.NodeSSLConfiguration -import net.corda.nodeapi.config.OldConfig import net.corda.nodeapi.internal.ServiceInfo import java.net.URL import java.nio.file.Path import java.util.* -/** @param exposeRaces for testing only, so its default is not in reference.conf but here. */ -data class BFTSMaRtConfiguration(val replicaId: Int, val debug: Boolean, val exposeRaces: Boolean = false) { - fun isValid() = replicaId >= 0 -} - interface NodeConfiguration : NodeSSLConfiguration { // myLegalName should be only used in the initial network registration, we should use the name from the certificate instead of this. // TODO: Remove this so we don't accidentally use this identity in the code? @@ -37,17 +31,31 @@ interface NodeConfiguration : NodeSSLConfiguration { val certificateChainCheckPolicies: List val verifierType: VerifierType val messageRedeliveryDelaySeconds: Int - val bftSMaRt: BFTSMaRtConfiguration - val notaryNodeAddress: NetworkHostAndPort? - val notaryClusterAddresses: List + val notary: NotaryConfig? val activeMQServer: ActiveMqServerConfiguration } -data class BridgeConfiguration( - val retryIntervalMs: Long, - val maxRetryIntervalMin: Long, - val retryIntervalMultiplier: Double -) +data class NotaryConfig(val validating: Boolean, val raft: RaftConfig? = null, val bftSMaRt: BFTSMaRtConfiguration? = null) { + init { + require(raft == null || bftSMaRt == null) { "raft and bftSMaRt configs cannot be specified together" } + } +} + +data class RaftConfig(val nodeAddress: NetworkHostAndPort, val clusterAddresses: List) + +/** @param exposeRaces for testing only, so its default is not in reference.conf but here. */ +data class BFTSMaRtConfiguration constructor(val replicaId: Int, + val clusterAddresses: List, + val debug: Boolean = false, + val exposeRaces: Boolean = false) { + init { + require(replicaId >= 0) { "replicaId cannot be negative" } + } +} + +data class BridgeConfiguration(val retryIntervalMs: Long, + val maxRetryIntervalMin: Long, + val retryIntervalMultiplier: Double) data class ActiveMqServerConfiguration(val bridge: BridgeConfiguration) @@ -67,16 +75,13 @@ data class FullNodeConfiguration( override val verifierType: VerifierType, override val messageRedeliveryDelaySeconds: Int = 30, val useHTTPS: Boolean, - @OldConfig("artemisAddress") val p2pAddress: NetworkHostAndPort, val rpcAddress: NetworkHostAndPort?, // TODO This field is slightly redundant as p2pAddress is sufficient to hold the address of the node's MQ broker. // Instead this should be a Boolean indicating whether that broker is an internal one started by the node or an external one val messagingServerAddress: NetworkHostAndPort?, val extraAdvertisedServiceIds: List, - override val bftSMaRt: BFTSMaRtConfiguration, - override val notaryNodeAddress: NetworkHostAndPort?, - override val notaryClusterAddresses: List, + override val notary: NotaryConfig?, override val certificateChainCheckPolicies: List, override val devMode: Boolean = false, val useTestClock: Boolean = false, 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 8b27a9a396..322a2c9299 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 @@ -13,6 +13,7 @@ import net.corda.core.messaging.DataFeed import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache.MapChange +import net.corda.core.node.services.NotaryService import net.corda.core.node.services.PartyInfo import net.corda.core.schemas.NodeInfoSchemaV1 import net.corda.core.serialization.SingletonSerializeAsToken @@ -81,10 +82,8 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) // Notary certificates have to be signed by the doorman directly it.legalIdentities } - .filter { - it.name.toString().contains("corda.notary", true) - } - .distinct() // Distinct, because of distributed service nodes + .filter { it.name.commonName?.startsWith(NotaryService.ID_PREFIX) ?: false } + .toSet() // Distinct, because of distributed service nodes .sortedBy { it.name.toString() } } 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 807bcc9415..8ea6e6af09 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 @@ -20,10 +20,12 @@ import net.corda.core.serialization.serialize import net.corda.core.transactions.FilteredTransaction import net.corda.core.utilities.* import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.node.utilities.NODE_DATABASE_PREFIX import java.security.PublicKey import javax.persistence.Entity +import javax.persistence.Table import kotlin.concurrent.thread /** @@ -33,24 +35,19 @@ import kotlin.concurrent.thread */ class BFTNonValidatingNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey, - cluster: BFTSMaRt.Cluster = distributedCluster) : NotaryService() { + private val bftSMaRtConfig: BFTSMaRtConfiguration, + cluster: BFTSMaRt.Cluster) : NotaryService() { companion object { - val type = SimpleNotaryService.type.getSubType("bft") + val id = constructId(validating = false, bft = true) private val log = loggerFor() - private val distributedCluster = object : BFTSMaRt.Cluster { - override fun waitUntilAllReplicasHaveInitialized() { - log.warn("A replica may still be initializing, in which case the upcoming consensus change may cause it to spin.") - } - } } private val client: BFTSMaRt.Client private val replicaHolder = SettableFuture.create() init { - require(services.configuration.bftSMaRt.isValid()) { "bftSMaRt replicaId must be specified in the configuration" } - client = BFTSMaRtConfig(services.configuration.notaryClusterAddresses, services.configuration.bftSMaRt.debug, services.configuration.bftSMaRt.exposeRaces).use { - val replicaId = services.configuration.bftSMaRt.replicaId + client = BFTSMaRtConfig(bftSMaRtConfig.clusterAddresses, bftSMaRtConfig.debug, bftSMaRtConfig.exposeRaces).use { + val replicaId = bftSMaRtConfig.replicaId val configHandle = it.handle() // Replica startup must be in parallel with other replicas, otherwise the constructor may not return: thread(name = "BFT SMaRt replica $replicaId init", isDaemon = true) { @@ -66,7 +63,7 @@ class BFTNonValidatingNotaryService(override val services: ServiceHubInternal, } fun waitUntilReplicaHasInitialized() { - log.debug { "Waiting for replica ${services.configuration.bftSMaRt.replicaId} to initialize." } + log.debug { "Waiting for replica ${bftSMaRtConfig.replicaId} to initialize." } replicaHolder.getOrThrow() // It's enough to wait for the ServiceReplica constructor to return. } @@ -96,36 +93,37 @@ class BFTNonValidatingNotaryService(override val services: ServiceHubInternal, } @Entity - @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}bft_smart_notary_committed_states") + @Table(name = "${NODE_DATABASE_PREFIX}bft_smart_notary_committed_states") class PersistedCommittedState(id: PersistentStateRef, consumingTxHash: String, consumingIndex: Int, party: PersistentUniquenessProvider.PersistentParty) : PersistentUniquenessProvider.PersistentUniqueness(id, consumingTxHash, consumingIndex, party) - fun createMap(): AppendOnlyPersistentMap = - AppendOnlyPersistentMap( - toPersistentEntityKey = { PersistentStateRef(it.txhash.toString(), it.index) }, - fromPersistentEntity = { - //TODO null check will become obsolete after making DB/JPA columns not nullable - val txId = it.id.txId ?: throw IllegalStateException("DB returned null SecureHash transactionId") - val index = it.id.index ?: throw IllegalStateException("DB returned null SecureHash index") - Pair(StateRef(txhash = SecureHash.parse(txId), index = index), + private fun createMap(): AppendOnlyPersistentMap { + return AppendOnlyPersistentMap( + toPersistentEntityKey = { PersistentStateRef(it.txhash.toString(), it.index) }, + fromPersistentEntity = { + //TODO null check will become obsolete after making DB/JPA columns not nullable + val txId = it.id.txId ?: throw IllegalStateException("DB returned null SecureHash transactionId") + val index = it.id.index ?: throw IllegalStateException("DB returned null SecureHash index") + Pair(StateRef(txhash = SecureHash.parse(txId), index = index), UniquenessProvider.ConsumingTx( id = SecureHash.parse(it.consumingTxHash), inputIndex = it.consumingIndex, requestingParty = Party( name = CordaX500Name.parse(it.party.name), owningKey = parsePublicKeyBase58(it.party.owningKey)))) - }, - toPersistentEntity = { (txHash, index) : StateRef, (id, inputIndex, requestingParty): UniquenessProvider.ConsumingTx -> - PersistedCommittedState( - id = PersistentStateRef(txHash.toString(), index), - consumingTxHash = id.toString(), - consumingIndex = inputIndex, - party = PersistentUniquenessProvider.PersistentParty(requestingParty.name.toString(), - requestingParty.owningKey.toBase58String()) - ) - }, - persistentEntityClass = PersistedCommittedState::class.java - ) + }, + toPersistentEntity = { (txHash, index) : StateRef, (id, inputIndex, requestingParty): UniquenessProvider.ConsumingTx -> + PersistedCommittedState( + id = PersistentStateRef(txHash.toString(), index), + consumingTxHash = id.toString(), + consumingIndex = inputIndex, + party = PersistentUniquenessProvider.PersistentParty(requestingParty.name.toString(), + requestingParty.owningKey.toBase58String()) + ) + }, + persistentEntityClass = PersistedCommittedState::class.java + ) + } private class Replica(config: BFTSMaRtConfig, replicaId: Int, diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt index 6e0916e3cc..797f9aa7de 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt @@ -5,18 +5,23 @@ import net.corda.core.flows.NotaryFlow import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.config.RaftConfig import java.security.PublicKey /** A non-validating notary service operated by a group of mutually trusting parties, uses the Raft algorithm to achieve consensus. */ -class RaftNonValidatingNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { +class RaftNonValidatingNotaryService(override val services: ServiceHubInternal, + override val notaryIdentityKey: PublicKey, + raftConfig: RaftConfig) : TrustedAuthorityNotaryService() { companion object { - val type = SimpleNotaryService.type.getSubType("raft") + val id = constructId(validating = false, raft = true) } override val timeWindowChecker: TimeWindowChecker = TimeWindowChecker(services.clock) - override val uniquenessProvider: RaftUniquenessProvider = RaftUniquenessProvider(services) + override val uniquenessProvider: RaftUniquenessProvider = RaftUniquenessProvider(services, raftConfig) - override fun createServiceFlow(otherPartySession: FlowSession): NotaryFlow.Service = NonValidatingNotaryFlow(otherPartySession, this) + override fun createServiceFlow(otherPartySession: FlowSession): NotaryFlow.Service { + return NonValidatingNotaryFlow(otherPartySession, this) + } override fun start() { uniquenessProvider.start() 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 ed349f1cc6..93daf5a23a 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 @@ -26,16 +26,14 @@ import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.loggerFor import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.config.RaftConfig import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.node.utilities.CordaPersistence import net.corda.nodeapi.config.SSLConfiguration import java.nio.file.Path import java.util.concurrent.CompletableFuture import javax.annotation.concurrent.ThreadSafe -import javax.persistence.Column -import javax.persistence.Entity -import javax.persistence.Id -import javax.persistence.Lob +import javax.persistence.* /** * A uniqueness provider that records committed input states in a distributed collection replicated and @@ -46,7 +44,7 @@ import javax.persistence.Lob * to the cluster leader to be actioned. */ @ThreadSafe -class RaftUniquenessProvider(private val services: ServiceHubInternal) : UniquenessProvider, SingletonSerializeAsToken() { +class RaftUniquenessProvider(private val services: ServiceHubInternal, private val raftConfig: RaftConfig) : UniquenessProvider, SingletonSerializeAsToken() { companion object { private val log = loggerFor() @@ -67,7 +65,7 @@ class RaftUniquenessProvider(private val services: ServiceHubInternal) : Uniquen } @Entity - @javax.persistence.Table(name = "notary_committed_states") + @Table(name = "notary_committed_states") class RaftState( @Id @Column @@ -81,13 +79,6 @@ class RaftUniquenessProvider(private val services: ServiceHubInternal) : Uniquen /** Directory storing the Raft log and state machine snapshots */ private val storagePath: Path = services.configuration.baseDirectory /** Address of the Copycat node run by this Corda node */ - private val myAddress = services.configuration.notaryNodeAddress - ?: throw IllegalArgumentException("notaryNodeAddress must be specified in configuration") - /** - * List of node addresses in the existing Copycat cluster. At least one active node must be - * provided to join the cluster. If empty, a new cluster will be bootstrapped. - */ - private val clusterAddresses = services.configuration.notaryClusterAddresses /** The database to store the state machine state in */ private val db: CordaPersistence = services.database /** SSL configuration */ @@ -96,7 +87,6 @@ class RaftUniquenessProvider(private val services: ServiceHubInternal) : Uniquen private lateinit var _clientFuture: CompletableFuture private lateinit var server: CopycatServer - /** * Copycat clients are responsible for connecting to the cluster and submitting commands and queries that operate * on the cluster's replicated state machine. @@ -108,7 +98,7 @@ class RaftUniquenessProvider(private val services: ServiceHubInternal) : Uniquen log.info("Creating Copycat server, log stored in: ${storagePath.toFile()}") val stateMachineFactory = { DistributedImmutableMap(db, RaftUniquenessProvider.Companion::createMap) } - val address = Address(myAddress.host, myAddress.port) + val address = raftConfig.nodeAddress.let { Address(it.host, it.port) } val storage = buildStorage(storagePath) val transport = buildTransport(transportConfiguration) val serializer = Serializer().apply { @@ -142,9 +132,9 @@ class RaftUniquenessProvider(private val services: ServiceHubInternal) : Uniquen .withSerializer(serializer) .build() - val serverFuture = if (clusterAddresses.isNotEmpty()) { - log.info("Joining an existing Copycat cluster at $clusterAddresses") - val cluster = clusterAddresses.map { Address(it.host, it.port) } + val serverFuture = if (raftConfig.clusterAddresses.isNotEmpty()) { + log.info("Joining an existing Copycat cluster at ${raftConfig.clusterAddresses}") + val cluster = raftConfig.clusterAddresses.map { Address(it.host, it.port) } server.join(cluster) } else { log.info("Bootstrapping a Copycat cluster at $address") diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt index 10da5581fe..4af9e2be74 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt @@ -5,18 +5,23 @@ import net.corda.core.flows.NotaryFlow import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.config.RaftConfig import java.security.PublicKey /** A validating notary service operated by a group of mutually trusting parties, uses the Raft algorithm to achieve consensus. */ -class RaftValidatingNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { +class RaftValidatingNotaryService(override val services: ServiceHubInternal, + override val notaryIdentityKey: PublicKey, + raftConfig: RaftConfig) : TrustedAuthorityNotaryService() { companion object { - val type = ValidatingNotaryService.type.getSubType("raft") + val id = constructId(validating = true, raft = true) } override val timeWindowChecker: TimeWindowChecker = TimeWindowChecker(services.clock) - override val uniquenessProvider: RaftUniquenessProvider = RaftUniquenessProvider(services) + override val uniquenessProvider: RaftUniquenessProvider = RaftUniquenessProvider(services, raftConfig) - override fun createServiceFlow(otherPartySession: FlowSession): NotaryFlow.Service = ValidatingNotaryFlow(otherPartySession, this) + override fun createServiceFlow(otherPartySession: FlowSession): NotaryFlow.Service { + return ValidatingNotaryFlow(otherPartySession, this) + } override fun start() { uniquenessProvider.start() 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 e62bcecb85..cb4401cae5 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 @@ -4,16 +4,11 @@ import net.corda.core.flows.FlowSession import net.corda.core.flows.NotaryFlow import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.TrustedAuthorityNotaryService -import net.corda.nodeapi.internal.ServiceType import net.corda.node.services.api.ServiceHubInternal 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() { - companion object { - val type = ServiceType.notary.getSubType("simple") - } - override val timeWindowChecker = TimeWindowChecker(services.clock) override val uniquenessProvider = PersistentUniquenessProvider() 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 c13b9186f1..6c7e36046b 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 @@ -4,16 +4,14 @@ import net.corda.core.flows.FlowSession import net.corda.core.flows.NotaryFlow import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.TrustedAuthorityNotaryService -import net.corda.nodeapi.internal.ServiceType import net.corda.node.services.api.ServiceHubInternal 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() { companion object { - val type = ServiceType.notary.getSubType("validating") + val id = constructId(validating = true) } - override val timeWindowChecker = TimeWindowChecker(services.clock) override val uniquenessProvider = PersistentUniquenessProvider() diff --git a/node/src/main/resources/reference.conf b/node/src/main/resources/reference.conf index 37a91c5e5e..37ff984da6 100644 --- a/node/src/main/resources/reference.conf +++ b/node/src/main/resources/reference.conf @@ -19,10 +19,6 @@ useHTTPS = false h2port = 0 useTestClock = false verifierType = InMemory -bftSMaRt = { - replicaId = -1 - debug = false -} activeMQServer = { bridge = { retryIntervalMs = 5000 diff --git a/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt b/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt index a4bfc6d12b..dbb62d3bac 100644 --- a/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt +++ b/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt @@ -33,7 +33,7 @@ class CordappSmokeTest { p2pPort = port.andIncrement, rpcPort = port.andIncrement, webPort = port.andIncrement, - extraServices = emptyList(), + isNotary = false, users = listOf(user) ) diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index ce3f6f3a6b..a33dadfcea 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -28,9 +28,7 @@ import net.corda.node.internal.StartedNode import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT import net.corda.node.services.messaging.RpcContext -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode @@ -68,7 +66,7 @@ class CordaRPCOpsImplTest { mockNet = MockNetwork() aliceNode = mockNet.createNode() - notaryNode = mockNet.createNode(advertisedServices = ServiceInfo(SimpleNotaryService.type)) + notaryNode = mockNet.createNotaryNode(validating = false) rpc = CordaRPCOpsImpl(aliceNode.services, aliceNode.smm, aliceNode.database) CURRENT_RPC_CONTEXT.set(RpcContext(User("user", "pwd", permissions = setOf( startFlowPermission(), diff --git a/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt b/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt index bbe685017a..1a08a04e76 100644 --- a/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt @@ -13,8 +13,6 @@ import net.corda.core.utilities.ProgressTracker import net.corda.finance.DOLLARS import net.corda.finance.flows.CashIssueFlow import net.corda.node.internal.cordapp.DummyRPCFlow -import net.corda.node.services.transactions.ValidatingNotaryService -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.DUMMY_NOTARY import net.corda.testing.node.MockNetwork import net.corda.testing.setCordappPackages @@ -23,7 +21,10 @@ import org.junit.After import org.junit.Before import org.junit.Test import java.util.concurrent.atomic.AtomicInteger -import kotlin.test.* +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue @StartableByService class DummyServiceFlow : FlowLogic() { @@ -86,9 +87,7 @@ class CordaServiceTest { fun start() { setCordappPackages("net.corda.node.internal","net.corda.finance") mockNet = MockNetwork(threadPerNode = true) - notaryNode = mockNet.createNode( - legalName = DUMMY_NOTARY.name, - advertisedServices = *arrayOf(ServiceInfo(ValidatingNotaryService.type))) + notaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name, validating = true) nodeA = mockNet.createNode() mockNet.startNodes() } diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index 4bb4b6aa62..a758655ff8 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -12,8 +12,6 @@ import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds import net.corda.node.internal.StartedNode -import net.corda.node.services.transactions.ValidatingNotaryService -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.node.MockNetwork @@ -39,12 +37,10 @@ class NotaryChangeTests { fun setUp() { setCordappPackages("net.corda.testing.contracts") mockNet = MockNetwork() - oldNotaryNode = mockNet.createNode( - legalName = DUMMY_NOTARY.name, - advertisedServices = *arrayOf(ServiceInfo(ValidatingNotaryService.type))) + oldNotaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) clientNodeA = mockNet.createNode() clientNodeB = mockNet.createNode() - newNotaryNode = mockNet.createNode(advertisedServices = ServiceInfo(ValidatingNotaryService.type)) + newNotaryNode = mockNet.createNotaryNode() mockNet.runNetwork() // Clear network map registration messages oldNotaryNode.internals.ensureRegistered() newNotaryParty = newNotaryNode.info.legalIdentities[1] diff --git a/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt index b4cce7d46f..2935053699 100644 --- a/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt @@ -30,9 +30,7 @@ class FullNodeConfigurationTest { rpcAddress = NetworkHostAndPort("localhost", 1), messagingServerAddress = null, extraAdvertisedServiceIds = emptyList(), - bftSMaRt = BFTSMaRtConfiguration(-1, false), - notaryNodeAddress = null, - notaryClusterAddresses = emptyList(), + notary = null, certificateChainCheckPolicies = emptyList(), devMode = true, activeMQServer = ActiveMqServerConfiguration(BridgeConfiguration(0, 0, 0.0))) diff --git a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt index 399f58f196..bb7c0d3e92 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt @@ -16,8 +16,6 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode import net.corda.node.services.statemachine.StateMachineManager -import net.corda.node.services.transactions.ValidatingNotaryService -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.node.MockNetwork @@ -95,9 +93,7 @@ class ScheduledFlowTests { fun setup() { setCordappPackages("net.corda.testing.contracts") mockNet = MockNetwork(threadPerNode = true) - notaryNode = mockNet.createNode( - legalName = DUMMY_NOTARY.name, - advertisedServices = *arrayOf(ServiceInfo(ValidatingNotaryService.type))) + notaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) val a = mockNet.createUnstartedNode() val b = mockNet.createUnstartedNode() diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 34f5e256a5..42b504a65e 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -6,10 +6,8 @@ import co.paralleluniverse.strands.concurrent.Semaphore import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef -import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.random63BitValue import net.corda.core.flows.* -import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.concurrent.flatMap import net.corda.core.internal.concurrent.map @@ -21,23 +19,16 @@ import net.corda.core.serialization.serialize import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker.Change import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap -import net.corda.finance.DOLLARS -import net.corda.finance.flows.CashIssueFlow -import net.corda.finance.flows.CashPaymentFlow import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.internal.StartedNode import net.corda.node.services.persistence.checkpoints -import net.corda.node.services.transactions.ValidatingNotaryService -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState -import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.InMemoryMessagingNetwork.MessageTransfer import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork @@ -69,10 +60,8 @@ class FlowFrameworkTests { private val receivedSessionMessages = ArrayList() private lateinit var node1: StartedNode private lateinit var node2: StartedNode - private lateinit var notary1: StartedNode - private lateinit var notary2: StartedNode - private lateinit var notary1Identity: Party - private lateinit var notary2Identity: Party + private lateinit var notary: StartedNode + private lateinit var notaryIdentity: Party @Before fun start() { @@ -85,19 +74,13 @@ class FlowFrameworkTests { node1.internals.ensureRegistered() // We intentionally create our own notary and ignore the one provided by the network - val notaryKeyPair = generateKeyPair() - val notaryService = ServiceInfo(ValidatingNotaryService.type, CordaX500Name(commonName = ValidatingNotaryService.type.id, organisation = "Notary service 2000", locality = "London", country = "GB")) - val notaryIdentityOverride = Pair(notaryService, notaryKeyPair) // Note that these notaries don't operate correctly as they don't share their state. They are only used for testing // service addressing. - notary1 = mockNet.createNotaryNode(notaryIdentity = notaryIdentityOverride, serviceName = notaryService.name) - notary2 = mockNet.createNotaryNode(notaryIdentity = notaryIdentityOverride, serviceName = notaryService.name) + notary = mockNet.createNotaryNode() receivedSessionMessagesObservable().forEach { receivedSessionMessages += it } mockNet.runNetwork() - - notary1Identity = notary1.services.myInfo.legalIdentities[1] - notary2Identity = notary2.services.myInfo.legalIdentities[1] + notaryIdentity = notary.services.myInfo.legalIdentities[1] } @After @@ -335,62 +318,6 @@ class FlowFrameworkTests { ) } - @Test - fun `different notaries are picked when addressing shared notary identity`() { - assertEquals(notary1Identity, notary2Identity) - assertThat(node1.services.networkMapCache.notaryIdentities.size == 1) - node1.services.startFlow(CashIssueFlow( - 2000.DOLLARS, - OpaqueBytes.of(0x01), - notary1Identity)).resultFuture.getOrThrow() - // We pay a couple of times, the notary picking should go round robin - for (i in 1..3) { - val flow = node1.services.startFlow(CashPaymentFlow(500.DOLLARS, node2.info.chooseIdentity())) - mockNet.runNetwork() - flow.resultFuture.getOrThrow() - } - val endpoint = mockNet.messagingNetwork.endpoint(notary1.network.myAddress as InMemoryMessagingNetwork.PeerHandle)!! - val party1Info = notary1.services.networkMapCache.getPartyInfo(notary1Identity)!! - assertTrue(party1Info is PartyInfo.DistributedNode) - val notary1Address: MessageRecipients = endpoint.getAddressOfParty(notary1.services.networkMapCache.getPartyInfo(notary1Identity)!!) - assertThat(notary1Address).isInstanceOf(InMemoryMessagingNetwork.ServiceHandle::class.java) - assertEquals(notary1Address, endpoint.getAddressOfParty(notary2.services.networkMapCache.getPartyInfo(notary2Identity)!!)) - receivedSessionMessages.expectEvents(isStrict = false) { - sequence( - // First Pay - expect(match = { it.message is SessionInit && it.message.initiatingFlowClass == NotaryFlow.Client::class.java.name }) { - it.message as SessionInit - assertEquals(node1.internals.id, it.from) - assertEquals(notary1Address, it.to) - }, - expect(match = { it.message is SessionConfirm }) { - it.message as SessionConfirm - assertEquals(notary1.internals.id, it.from) - }, - // Second pay - expect(match = { it.message is SessionInit && it.message.initiatingFlowClass == NotaryFlow.Client::class.java.name }) { - it.message as SessionInit - assertEquals(node1.internals.id, it.from) - assertEquals(notary1Address, it.to) - }, - expect(match = { it.message is SessionConfirm }) { - it.message as SessionConfirm - assertEquals(notary2.internals.id, it.from) - }, - // Third pay - expect(match = { it.message is SessionInit && it.message.initiatingFlowClass == NotaryFlow.Client::class.java.name }) { - it.message as SessionInit - assertEquals(node1.internals.id, it.from) - assertEquals(notary1Address, it.to) - }, - expect(match = { it.message is SessionConfirm }) { - it.message as SessionConfirm - assertEquals(it.from, notary1.internals.id) - } - ) - } - } - @Test fun `other side ends before doing expected send`() { node2.registerFlowFactory(ReceiveFlow::class) { NoOpFlow() } @@ -604,7 +531,7 @@ class FlowFrameworkTests { @Test fun `wait for transaction`() { - val ptx = TransactionBuilder(notary = notary1Identity) + val ptx = TransactionBuilder(notary = notaryIdentity) .addOutputState(DummyState(), DummyContract.PROGRAM_ID) .addCommand(dummyCommand(node1.info.chooseIdentity().owningKey)) val stx = node1.services.signInitialTransaction(ptx) @@ -619,7 +546,7 @@ class FlowFrameworkTests { @Test fun `committer throws exception before calling the finality flow`() { - val ptx = TransactionBuilder(notary = notary1Identity) + val ptx = TransactionBuilder(notary = notaryIdentity) .addOutputState(DummyState(), DummyContract.PROGRAM_ID) .addCommand(dummyCommand()) val stx = node1.services.signInitialTransaction(ptx) @@ -636,7 +563,7 @@ class FlowFrameworkTests { @Test fun `verify vault query service is tokenizable by force checkpointing within a flow`() { - val ptx = TransactionBuilder(notary = notary1Identity) + val ptx = TransactionBuilder(notary = notaryIdentity) .addOutputState(DummyState(), DummyContract.PROGRAM_ID) .addCommand(dummyCommand(node1.info.chooseIdentity().owningKey)) val stx = node1.services.signInitialTransaction(ptx) diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt index e395ce739e..579f653018 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt @@ -13,7 +13,6 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds import net.corda.node.internal.StartedNode -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.node.MockNetwork @@ -36,9 +35,7 @@ class NotaryServiceTests { fun setup() { setCordappPackages("net.corda.testing.contracts") mockNet = MockNetwork() - notaryNode = mockNet.createNode( - legalName = DUMMY_NOTARY.name, - advertisedServices = *arrayOf(ServiceInfo(SimpleNotaryService.type))) + notaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name, validating = false) clientNode = mockNet.createNode() mockNet.runNetwork() // Clear network map registration messages notaryNode.internals.ensureRegistered() diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt index fb4741e373..87410fecb8 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt @@ -14,7 +14,6 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode import net.corda.node.services.issueInvalidState -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.node.MockNetwork @@ -36,10 +35,7 @@ class ValidatingNotaryServiceTests { fun setup() { setCordappPackages("net.corda.testing.contracts") mockNet = MockNetwork() - notaryNode = mockNet.createNode( - legalName = DUMMY_NOTARY.name, - advertisedServices = *arrayOf(ServiceInfo(ValidatingNotaryService.type)) - ) + notaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) clientNode = mockNet.createNode() mockNet.runNetwork() // Clear network map registration messages notaryNode.internals.ensureRegistered() diff --git a/samples/attachment-demo/build.gradle b/samples/attachment-demo/build.gradle index 1ca4d85d3d..379fcf6af8 100644 --- a/samples/attachment-demo/build.gradle +++ b/samples/attachment-demo/build.gradle @@ -40,7 +40,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { networkMap "O=Notary Service,L=Zurich,C=CH" node { name "O=Notary Service,L=Zurich,C=CH" - advertisedServices["corda.notary.validating"] + notary = [validating : true] + advertisedServices = [] p2pPort 10002 rpcPort 10003 cordapps = [] 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 3f3458997b..d0cdb64932 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 @@ -2,9 +2,7 @@ package net.corda.attachmentdemo import net.corda.core.utilities.getOrThrow import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_BANK_B import net.corda.testing.DUMMY_NOTARY @@ -22,7 +20,7 @@ class AttachmentDemoTest { val (nodeA, nodeB) = listOf( startNode(providedName = DUMMY_BANK_A.name, rpcUsers = demoUser, maximumHeapSize = "1g"), startNode(providedName = DUMMY_BANK_B.name, rpcUsers = demoUser, maximumHeapSize = "1g"), - startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))) + startNotaryNode(DUMMY_NOTARY.name, validating = false)) .map { it.getOrThrow() } startWebserver(nodeB).getOrThrow() 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 b331dbacc7..f63d1c68a7 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 @@ -1,12 +1,10 @@ package net.corda.attachmentdemo import net.corda.core.internal.div -import net.corda.nodeapi.internal.ServiceInfo +import net.corda.nodeapi.User import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_BANK_B import net.corda.testing.DUMMY_NOTARY -import net.corda.node.services.transactions.SimpleNotaryService -import net.corda.nodeapi.User import net.corda.testing.driver.driver /** @@ -16,7 +14,7 @@ import net.corda.testing.driver.driver fun main(args: Array) { val demoUser = listOf(User("demo", "demo", setOf("StartFlow.net.corda.flows.FinalityFlow"))) driver(isDebug = true, driverDirectory = "build" / "attachment-demo-nodes") { - startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) + startNotaryNode(DUMMY_NOTARY.name, validating = false) startNode(providedName = DUMMY_BANK_A.name, rpcUsers = demoUser) startNode(providedName = DUMMY_BANK_B.name, rpcUsers = demoUser) waitForAllNodesToFinish() diff --git a/samples/bank-of-corda-demo/build.gradle b/samples/bank-of-corda-demo/build.gradle index dcdbe408d0..c673a10da1 100644 --- a/samples/bank-of-corda-demo/build.gradle +++ b/samples/bank-of-corda-demo/build.gradle @@ -53,7 +53,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { networkMap "O=Notary Service,L=Zurich,C=CH" node { name "O=Notary Service,L=Zurich,C=CH" - advertisedServices = ["corda.notary.validating"] + notary = [validating : true] + advertisedServices = [] p2pPort 10002 rpcPort 10003 cordapps = ["net.corda:finance:$corda_release_version"] diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt index 22e33c41b9..8077105e41 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt @@ -3,8 +3,6 @@ package net.corda.bank import net.corda.bank.api.BankOfCordaClientApi import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams import net.corda.core.utilities.getOrThrow -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.testing.BOC import net.corda.testing.driver.driver import net.corda.testing.notary @@ -16,7 +14,7 @@ class BankOfCordaHttpAPITest { fun `issuer flow via Http`() { driver(extraCordappPackagesToScan = listOf("net.corda.finance"), dsl = { val bigCorpNodeFuture = startNode(providedName = BIGCORP_LEGAL_NAME) - val nodeBankOfCordaFuture = startNode(providedName = BOC.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) + val nodeBankOfCordaFuture = startNotaryNode(BOC.name, validating = false) val (nodeBankOfCorda) = listOf(nodeBankOfCordaFuture, bigCorpNodeFuture).map { it.getOrThrow() } val nodeBankOfCordaApiAddr = startWebserver(nodeBankOfCorda).getOrThrow().listenAddress val notaryName = notary().node.nodeInfo.legalIdentities[1].name diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt index 157b2c0fd6..d379e3b98a 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt @@ -8,8 +8,6 @@ import net.corda.finance.DOLLARS import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.CashIssueAndPaymentFlow import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User import net.corda.testing.* import net.corda.testing.driver.driver @@ -22,7 +20,7 @@ class BankOfCordaRPCClientTest { val bocManager = User("bocManager", "password1", permissions = setOf( startFlowPermission())) val bigCorpCFO = User("bigCorpCFO", "password2", permissions = emptySet()) - val nodeBankOfCordaFuture = startNode(providedName = BOC.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)), rpcUsers = listOf(bocManager)) + val nodeBankOfCordaFuture = startNotaryNode(BOC.name, rpcUsers = listOf(bocManager), validating = false) val nodeBigCorporationFuture = startNode(providedName = BIGCORP_LEGAL_NAME, rpcUsers = listOf(bigCorpCFO)) val (nodeBankOfCorda, nodeBigCorporation) = listOf(nodeBankOfCordaFuture, nodeBigCorporationFuture).map { it.getOrThrow() } diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt index e1b75e3f1b..ac2e890f09 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt @@ -9,8 +9,7 @@ import net.corda.finance.flows.CashExitFlow import net.corda.finance.flows.CashIssueAndPaymentFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.transactions.SimpleNotaryService +import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.User import net.corda.testing.BOC import net.corda.testing.DUMMY_NOTARY @@ -55,7 +54,7 @@ private class BankOfCordaDriver { val role = options.valueOf(roleArg)!! val requestParams = IssueRequestParams(options.valueOf(quantity), options.valueOf(currency), BIGCORP_LEGAL_NAME, - "1", BOC.name, DUMMY_NOTARY.name.copy(commonName = "corda.notary.validating")) + "1", BOC.name, DUMMY_NOTARY.name.copy(commonName = ValidatingNotaryService.id)) try { when (role) { @@ -70,8 +69,7 @@ private class BankOfCordaDriver { val bigCorpUser = User(BIGCORP_USERNAME, "test", permissions = setOf( startFlowPermission())) - startNode(providedName = DUMMY_NOTARY.name, - advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) + startNotaryNode(DUMMY_NOTARY.name, validating = false) val bankOfCorda = startNode( providedName = BOC.name, rpcUsers = listOf(bankUser)) diff --git a/samples/irs-demo/build.gradle b/samples/irs-demo/build.gradle index 6d9b908823..bc482b26d7 100644 --- a/samples/irs-demo/build.gradle +++ b/samples/irs-demo/build.gradle @@ -53,7 +53,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { networkMap "O=Notary Service,L=Zurich,C=CH" node { name "O=Notary Service,L=Zurich,C=CH" - advertisedServices = ["corda.notary.validating", "corda.interest_rates"] + notary = [validating : true] + advertisedServices = ["corda.interest_rates"] p2pPort 10002 rpcPort 10003 webPort 10004 diff --git a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt index 4d3c92b7dc..a598595f0b 100644 --- a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt +++ b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt @@ -21,9 +21,7 @@ import net.corda.finance.plugin.registerFinanceJSONMappers import net.corda.irs.contract.InterestRateSwap import net.corda.irs.utilities.uploadFile import net.corda.node.services.config.FullNodeConfiguration -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.driver.driver import net.corda.testing.http.HttpApi @@ -50,9 +48,7 @@ class IRSDemoTest : IntegrationTestCategory { fun `runs IRS demo`() { driver(useTestClock = true, isDebug = true) { val (controller, nodeA, nodeB) = listOf( - startNode( - providedName = DUMMY_NOTARY.name, - advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))), + startNotaryNode(DUMMY_NOTARY.name, validating = false), startNode(providedName = DUMMY_BANK_A.name, rpcUsers = listOf(rpcUser)), startNode(providedName = DUMMY_BANK_B.name)) .map { it.getOrThrow() } diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/Main.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/Main.kt index 6cd3425e58..d1e770b257 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/Main.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/Main.kt @@ -1,8 +1,6 @@ package net.corda.irs import net.corda.core.utilities.getOrThrow -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_BANK_B import net.corda.testing.DUMMY_NOTARY @@ -15,9 +13,7 @@ import net.corda.testing.driver.driver fun main(args: Array) { driver(dsl = { val (controller, nodeA, nodeB) = listOf( - startNode( - providedName = DUMMY_NOTARY.name, - advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))), + startNotaryNode(DUMMY_NOTARY.name, validating = false), startNode(providedName = DUMMY_BANK_A.name), startNode(providedName = DUMMY_BANK_B.name)) .map { it.getOrThrow() } diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt index ea09fa73c0..46a38d7f5e 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt @@ -11,18 +11,13 @@ import net.corda.irs.api.NodeInterestRates import net.corda.node.internal.StartedNode import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.statemachine.StateMachineManager -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.internal.ServiceInfo import net.corda.nodeapi.internal.ServiceType -import net.corda.testing.DUMMY_MAP -import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.DUMMY_REGULATOR +import net.corda.testing.* import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.MockNetwork import net.corda.testing.node.TestClock import net.corda.testing.node.setTo -import net.corda.testing.setCordappPackages -import net.corda.testing.testNodeConfiguration import rx.Observable import rx.subjects.PublishSubject import java.math.BigInteger @@ -103,10 +98,11 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, advertisedServices: Set, id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): SimulatedNode { - require(advertisedServices.containsType(SimpleNotaryService.type)) + requireNotNull(config.notary) val cfg = testNodeConfiguration( baseDirectory = config.baseDirectory, - myLegalName = DUMMY_NOTARY.name) + myLegalName = DUMMY_NOTARY.name, + notaryConfig = config.notary) return SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, notaryIdentity, entropyRoot) } } @@ -153,7 +149,7 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, val mockNet = MockNetwork(networkSendManuallyPumped, runAsync) // This one must come first. val networkMap = mockNet.startNetworkMapNode(nodeFactory = NetworkMapNodeFactory) - val notary = mockNet.createNode(nodeFactory = NotaryNodeFactory, advertisedServices = ServiceInfo(SimpleNotaryService.type)) + val notary = mockNet.createNotaryNode(validating = false, nodeFactory = NotaryNodeFactory) val regulators = listOf(mockNet.createUnstartedNode(nodeFactory = RegulatorFactory)) val ratesOracle = mockNet.createUnstartedNode(nodeFactory = RatesOracleFactory) diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt index 87c12cd663..477adecd20 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt @@ -5,10 +5,9 @@ import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformNode import net.corda.core.identity.CordaX500Name import net.corda.core.internal.div -import net.corda.core.internal.stream -import net.corda.core.internal.toTypedArray import net.corda.core.utilities.NetworkHostAndPort -import net.corda.nodeapi.internal.ServiceInfo +import net.corda.node.services.config.BFTSMaRtConfiguration +import net.corda.node.services.config.NotaryConfig import net.corda.node.services.transactions.BFTNonValidatingNotaryService import net.corda.node.services.transactions.minCorrectReplicas import net.corda.node.utilities.ServiceIdentityGenerator @@ -24,9 +23,7 @@ private val notaryNames = createNotaryNames(clusterSize) // This is not the intended final design for how to use CordformDefinition, please treat this as experimental and DO // NOT use this as a design to copy. object BFTNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", notaryNames[0].toString()) { - private val serviceType = BFTNonValidatingNotaryService.type - private val clusterName = CordaX500Name(serviceType.id, "BFT", "Zurich", "CH") - private val advertisedService = ServiceInfo(serviceType, clusterName) + private val clusterName = CordaX500Name(BFTNonValidatingNotaryService.id, "BFT", "Zurich", "CH") init { node { @@ -40,12 +37,10 @@ object BFTNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", not p2pPort(10005) rpcPort(10006) } - val clusterAddresses = (0 until clusterSize).stream().mapToObj { NetworkHostAndPort("localhost", 11000 + it * 10) }.toTypedArray() + val clusterAddresses = (0 until clusterSize).map { NetworkHostAndPort("localhost", 11000 + it * 10) } fun notaryNode(replicaId: Int, configure: CordformNode.() -> Unit) = node { name(notaryNames[replicaId]) - advertisedServices(advertisedService) - notaryClusterAddresses(*clusterAddresses) - bftReplicaId(replicaId) + notary(NotaryConfig(validating = false, bftSMaRt = BFTSMaRtConfiguration(replicaId, clusterAddresses))) configure() } notaryNode(0) { diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt index 2aa043364a..1d78c51266 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt @@ -6,7 +6,8 @@ import net.corda.cordform.CordformNode import net.corda.core.identity.CordaX500Name import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort -import net.corda.nodeapi.internal.ServiceInfo +import net.corda.node.services.config.NotaryConfig +import net.corda.node.services.config.RaftConfig import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.testing.ALICE @@ -22,9 +23,7 @@ private val notaryNames = createNotaryNames(3) // This is not the intended final design for how to use CordformDefinition, please treat this as experimental and DO // NOT use this as a design to copy. object RaftNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", notaryNames[0].toString()) { - private val serviceType = RaftValidatingNotaryService.type - private val clusterName = CordaX500Name(serviceType.id, "Raft", "Zurich", "CH") - private val advertisedService = ServiceInfo(serviceType, clusterName) + private val clusterName = CordaX500Name(RaftValidatingNotaryService.id, "Raft", "Zurich", "CH") init { node { @@ -38,28 +37,23 @@ object RaftNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", no p2pPort(10005) rpcPort(10006) } - fun notaryNode(index: Int, configure: CordformNode.() -> Unit) = node { + fun notaryNode(index: Int, nodePort: Int, clusterPort: Int? = null, configure: CordformNode.() -> Unit) = node { name(notaryNames[index]) - advertisedServices(advertisedService) + val clusterAddresses = if (clusterPort != null ) listOf(NetworkHostAndPort("localhost", clusterPort)) else emptyList() + notary(NotaryConfig(validating = true, raft = RaftConfig(NetworkHostAndPort("localhost", nodePort), clusterAddresses))) configure() } - notaryNode(0) { - notaryNodePort(10008) + notaryNode(0, 10008) { p2pPort(10009) rpcPort(10010) } - val clusterAddress = NetworkHostAndPort("localhost", 10008) // Otherwise each notary forms its own cluster. - notaryNode(1) { - notaryNodePort(10012) + notaryNode(1, 10012, 10008) { p2pPort(10013) rpcPort(10014) - notaryClusterAddresses(clusterAddress) } - notaryNode(2) { - notaryNodePort(10016) + notaryNode(2, 10016, 10008) { p2pPort(10017) rpcPort(10018) - notaryClusterAddresses(clusterAddress) } } diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt index e9c45fd7e4..beec66a8fc 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt @@ -1,17 +1,16 @@ package net.corda.notarydemo +import net.corda.cordform.CordformContext +import net.corda.cordform.CordformDefinition import net.corda.core.internal.div -import net.corda.testing.ALICE -import net.corda.testing.BOB -import net.corda.testing.DUMMY_NOTARY import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.node.services.transactions.ValidatingNotaryService +import net.corda.node.services.config.NotaryConfig import net.corda.nodeapi.User import net.corda.notarydemo.flows.DummyIssueAndMove import net.corda.notarydemo.flows.RPCStartableNotaryFlowClient -import net.corda.cordform.CordformDefinition -import net.corda.cordform.CordformContext -import net.corda.nodeapi.internal.ServiceInfo +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.internal.demorun.* fun main(args: Array) = SingleNotaryCordform.runNodes() @@ -37,7 +36,7 @@ object SingleNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", name(DUMMY_NOTARY.name) p2pPort(10009) rpcPort(10010) - advertisedServices(ServiceInfo(ValidatingNotaryService.type)) + notary(NotaryConfig(validating = true)) } } diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index 278608bd77..ffce26eb4f 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -1,5 +1,3 @@ -import org.apache.tools.ant.filters.FixCrLfFilter - buildscript { ext.strata_version = '1.1.2' } @@ -68,7 +66,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { networkMap "O=Notary Service,L=Zurich,C=CH" node { name "O=Notary Service,L=Zurich,C=CH" - advertisedServices = ["corda.notary.validating"] + notary = [validating : true] + advertisedServices = [] p2pPort 10002 cordapps = ["net.corda:finance:$corda_release_version"] } diff --git a/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt b/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt index a81b898920..e113f235b7 100644 --- a/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt +++ b/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt @@ -3,16 +3,9 @@ package net.corda.vega import com.opengamma.strata.product.common.BuySell import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.getOrThrow -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.transactions.SimpleNotaryService -import net.corda.testing.DUMMY_BANK_A -import net.corda.testing.DUMMY_BANK_B -import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.IntegrationTestCategory +import net.corda.testing.* import net.corda.testing.driver.driver import net.corda.testing.http.HttpApi -import net.corda.testing.setCordappPackages -import net.corda.testing.unsetCordappPackages import net.corda.vega.api.PortfolioApi import net.corda.vega.api.PortfolioApiUtils import net.corda.vega.api.SwapDataModel @@ -46,7 +39,7 @@ class SimmValuationTest : IntegrationTestCategory { @Test fun `runs SIMM valuation demo`() { driver(isDebug = true) { - startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))).getOrThrow() + startNotaryNode(DUMMY_NOTARY.name, validating = false).getOrThrow() val nodeAFuture = startNode(providedName = nodeALegalName) val nodeBFuture = startNode(providedName = nodeBLegalName) val (nodeA, nodeB) = listOf(nodeAFuture, nodeBFuture).map { it.getOrThrow() } diff --git a/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/Main.kt b/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/Main.kt index ee855f6ffc..9cc72b5af0 100644 --- a/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/Main.kt +++ b/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/Main.kt @@ -1,12 +1,10 @@ package net.corda.vega import net.corda.core.utilities.getOrThrow -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_BANK_B import net.corda.testing.DUMMY_BANK_C import net.corda.testing.DUMMY_NOTARY -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.testing.driver.driver /** @@ -16,7 +14,7 @@ import net.corda.testing.driver.driver */ fun main(args: Array) { driver(dsl = { - val notaryFuture = startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) + val notaryFuture = startNotaryNode(DUMMY_NOTARY.name, validating = false) val nodeAFuture = startNode(providedName = DUMMY_BANK_A.name) val nodeBFuture = startNode(providedName = DUMMY_BANK_B.name) val nodeCFuture = startNode(providedName = DUMMY_BANK_C.name) diff --git a/samples/trader-demo/build.gradle b/samples/trader-demo/build.gradle index 5d2a1750eb..5d6f861e8a 100644 --- a/samples/trader-demo/build.gradle +++ b/samples/trader-demo/build.gradle @@ -54,7 +54,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { networkMap "O=Notary Service,L=Zurich,C=CH" node { name "O=Notary Service,L=Zurich,C=CH" - advertisedServices = ["corda.notary.validating"] + notary = [validating : true] + advertisedServices = [] p2pPort 10002 cordapps = ["net.corda:finance:$corda_release_version"] } diff --git a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt index 93034badfe..0efa101ec7 100644 --- a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt +++ b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt @@ -9,9 +9,7 @@ import net.corda.finance.flows.CashPaymentFlow import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.schemas.CommercialPaperSchemaV1 import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.driver.poll import net.corda.testing.node.NodeBasedTest @@ -43,7 +41,7 @@ class TraderDemoTest : NodeBasedTest() { startFlowPermission(), startFlowPermission(), startFlowPermission())) - val notaryFuture = startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) + val notaryFuture = startNotaryNode(DUMMY_NOTARY.name, validating = false) val nodeAFuture = startNode(DUMMY_BANK_A.name, rpcUsers = listOf(demoUser)) val nodeBFuture = startNode(DUMMY_BANK_B.name, rpcUsers = listOf(demoUser)) val bankNodeFuture = startNode(BOC.name, rpcUsers = listOf(bankUser)) 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 5b41ae9ec3..4a032576f5 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 @@ -3,8 +3,6 @@ package net.corda.traderdemo import net.corda.core.internal.div import net.corda.finance.flows.CashIssueFlow import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User import net.corda.testing.BOC import net.corda.testing.DUMMY_BANK_A @@ -27,7 +25,7 @@ fun main(args: Array) { val user = User("user1", "test", permissions = setOf(startFlowPermission(), startFlowPermission(), startFlowPermission())) - startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) + startNotaryNode(DUMMY_NOTARY.name, validating = false) startNode(providedName = DUMMY_BANK_A.name, rpcUsers = demoUser) startNode(providedName = DUMMY_BANK_B.name, rpcUsers = demoUser) startNode(providedName = BOC.name, rpcUsers = listOf(user)) diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt index f9fc3f3002..ae31e3bbb5 100644 --- a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt @@ -9,9 +9,7 @@ import net.corda.core.internal.read import net.corda.core.messaging.startFlow import net.corda.core.serialization.CordaSerializable import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.User -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.driver.driver import net.corda.testing.node.MockNetwork import org.junit.Ignore @@ -292,11 +290,7 @@ class FlowStackSnapshotTest { @Test fun `flowStackSnapshot object is serializable`() { val mockNet = MockNetwork(threadPerNode = true) - val notaryService = ServiceInfo(ValidatingNotaryService.type) - val notaryNode = mockNet.createNode( - legalName = DUMMY_NOTARY.name, - notaryIdentity = notaryService to DUMMY_NOTARY_KEY, - advertisedServices = *arrayOf(notaryService)) + mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) val node = mockNet.createPartyNode() node.internals.registerInitiatedFlow(DummyFlow::class.java) node.services.startFlow(FlowStackSnapshotSerializationTestingFlow()).resultFuture.get() diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt index 43260d5500..c9cf4ae4a0 100644 --- a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt @@ -5,12 +5,10 @@ import net.corda.core.internal.div import net.corda.core.internal.list import net.corda.core.internal.readLines import net.corda.core.utilities.getOrThrow +import net.corda.node.internal.NodeStartup import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_NOTARY import net.corda.testing.DUMMY_REGULATOR -import net.corda.node.internal.NodeStartup -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.transactions.SimpleNotaryService import net.corda.testing.ProjectStructure.projectRootDir import org.assertj.core.api.Assertions.assertThat import org.junit.Test @@ -40,7 +38,7 @@ class DriverTests { @Test fun `simple node startup and shutdown`() { val handles = driver { - val notary = startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) + val notary = startNotaryNode(DUMMY_NOTARY.name, validating = false) val regulator = startNode(providedName = DUMMY_REGULATOR.name) listOf(nodeMustBeUp(notary), nodeMustBeUp(regulator)) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/DriverConstants.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/DriverConstants.kt index 398cc9262c..1a17d02f3a 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/DriverConstants.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/DriverConstants.kt @@ -5,8 +5,6 @@ package net.corda.testing import net.corda.core.identity.Party import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.CordaRPCOps -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.User import net.corda.testing.driver.DriverDSLExposedInterface @@ -19,9 +17,15 @@ import net.corda.testing.driver.DriverDSLExposedInterface * node construction won't start until you access the members. You can get one of these from the * [alice], [bob] and [aliceBobAndNotary] functions. */ -class PredefinedTestNode internal constructor(party: Party, driver: DriverDSLExposedInterface, services: Set) { +class PredefinedTestNode internal constructor(party: Party, driver: DriverDSLExposedInterface, ifNotaryIsValidating: Boolean?) { val rpcUsers = listOf(User("admin", "admin", setOf("ALL"))) // TODO: Randomize? - val nodeFuture by lazy { driver.startNode(providedName = party.name, rpcUsers = rpcUsers, advertisedServices = services) } + val nodeFuture by lazy { + if (ifNotaryIsValidating != null) { + driver.startNotaryNode(providedName = party.name, rpcUsers = rpcUsers, validating = ifNotaryIsValidating) + } else { + driver.startNode(providedName = party.name, rpcUsers = rpcUsers) + } + } val node by lazy { nodeFuture.get()!! } val rpc by lazy { node.rpcClientToNode() } @@ -34,17 +38,17 @@ class PredefinedTestNode internal constructor(party: Party, driver: DriverDSLExp * Returns a plain, entirely stock node pre-configured with the [ALICE] identity. Note that a random key will be generated * for it: you won't have [ALICE_KEY]. */ -fun DriverDSLExposedInterface.alice(): PredefinedTestNode = PredefinedTestNode(ALICE, this, emptySet()) +fun DriverDSLExposedInterface.alice(): PredefinedTestNode = PredefinedTestNode(ALICE, this, null) /** * Returns a plain, entirely stock node pre-configured with the [BOB] identity. Note that a random key will be generated * for it: you won't have [BOB_KEY]. */ -fun DriverDSLExposedInterface.bob(): PredefinedTestNode = PredefinedTestNode(BOB, this, emptySet()) +fun DriverDSLExposedInterface.bob(): PredefinedTestNode = PredefinedTestNode(BOB, this, null) /** * Returns a plain single node notary pre-configured with the [DUMMY_NOTARY] identity. Note that a random key will be generated * for it: you won't have [DUMMY_NOTARY_KEY]. */ -fun DriverDSLExposedInterface.notary(): PredefinedTestNode = PredefinedTestNode(DUMMY_NOTARY, this, setOf(ServiceInfo(ValidatingNotaryService.type))) +fun DriverDSLExposedInterface.notary(): PredefinedTestNode = PredefinedTestNode(DUMMY_NOTARY, this, true) /** * Returns plain, entirely stock nodes pre-configured with the [ALICE], [BOB] and [DUMMY_NOTARY] X.500 names in that diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt index 088eaaf6f0..7f8069e919 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt @@ -8,8 +8,8 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.node.ServiceHub import net.corda.core.transactions.TransactionBuilder import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.NotaryConfig import net.corda.node.services.config.VerifierType -import net.corda.testing.node.MockCordappProvider import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties @@ -51,7 +51,8 @@ import java.nio.file.Path fun testNodeConfiguration( baseDirectory: Path, - myLegalName: CordaX500Name): NodeConfiguration { + myLegalName: CordaX500Name, + notaryConfig: NotaryConfig? = null): NodeConfiguration { abstract class MockableNodeConfiguration : NodeConfiguration // Otherwise Mockito is defeated by val getters. val nc = spy() whenever(nc.baseDirectory).thenReturn(baseDirectory) @@ -60,6 +61,7 @@ fun testNodeConfiguration( whenever(nc.keyStorePassword).thenReturn("cordacadevpass") whenever(nc.trustStorePassword).thenReturn("trustpass") whenever(nc.rpcUsers).thenReturn(emptyList()) + whenever(nc.notary).thenReturn(notaryConfig) whenever(nc.dataSourceProperties).thenReturn(makeTestDataSourceProperties(myLegalName.organisation)) whenever(nc.database).thenReturn(makeTestDatabaseProperties()) whenever(nc.emailAddress).thenReturn("") diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index 344244bfa4..9382ce4b3a 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -26,12 +26,11 @@ import net.corda.node.internal.NodeStartup import net.corda.node.internal.StartedNode import net.corda.node.services.config.* import net.corda.node.services.network.NetworkMapService -import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.nodeapi.User import net.corda.nodeapi.config.parseAs +import net.corda.nodeapi.config.toConfig import net.corda.nodeapi.internal.ServiceInfo -import net.corda.nodeapi.internal.ServiceType import net.corda.nodeapi.internal.addShutdownHook import net.corda.testing.* import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO @@ -96,6 +95,14 @@ interface DriverDSLExposedInterface : CordformContext { startInSameProcess: Boolean? = defaultParameters.startInSameProcess, maximumHeapSize: String = defaultParameters.maximumHeapSize): CordaFuture + // TODO This method has been added temporarily, to be deleted once the set of notaries is defined at the network level. + fun startNotaryNode(providedName: CordaX500Name, + rpcUsers: List = emptyList(), + verifierType: VerifierType = VerifierType.InMemory, + customOverrides: Map = emptyMap(), + //TODO Switch the default value + validating: Boolean = true): CordaFuture + /** * Helper function for starting a [node] with custom parameters from Java. * @@ -118,7 +125,6 @@ interface DriverDSLExposedInterface : CordformContext { * * @param notaryName The legal name of the advertised distributed notary service. * @param clusterSize Number of nodes to create for the cluster. - * @param type The advertised notary service type. Currently the only supported type is [RaftValidatingNotaryService.type]. * @param verifierType The type of transaction verifier to use. See: [VerifierType] * @param rpcUsers List of users who are authorised to use the RPC system. Defaults to empty list. * @param startInSameProcess Determines if the node should be started inside the same process the Driver is running @@ -128,7 +134,6 @@ interface DriverDSLExposedInterface : CordformContext { fun startNotaryCluster( notaryName: CordaX500Name, clusterSize: Int = 3, - type: ServiceType = RaftValidatingNotaryService.type, verifierType: VerifierType = VerifierType.InMemory, rpcUsers: List = emptyList(), startInSameProcess: Boolean? = null): CordaFuture>> @@ -668,9 +673,9 @@ class DriverDSL( } } is NetworkMapStartStrategy.Nominated -> { - serviceConfig(networkMapCandidates.filter { + serviceConfig(networkMapCandidates.single { it.name == legalName.toString() - }.single().config.getString("p2pAddress").let(NetworkHostAndPort.Companion::parse)).let { + }.config.getString("p2pAddress").let(NetworkHostAndPort.Companion::parse)).let { { nodeName: CordaX500Name -> if (nodeName == legalName) null else it } } } @@ -715,6 +720,15 @@ class DriverDSL( return startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize) } + override fun startNotaryNode(providedName: CordaX500Name, + rpcUsers: List, + verifierType: VerifierType, + customOverrides: Map, + validating: Boolean): CordaFuture { + val config = customOverrides + NotaryConfig(validating).toConfigMap() + return startNode(providedName = providedName, rpcUsers = rpcUsers, verifierType = verifierType, customOverrides = config) + } + override fun startNodes(nodes: List, startInSameProcess: Boolean?, maximumHeapSize: String): List> { val networkMapServiceConfigLookup = networkMapServiceConfigLookup(nodes) return nodes.map { node -> @@ -722,50 +736,62 @@ class DriverDSL( val webAddress = portAllocation.nextHostAndPort() val name = CordaX500Name.parse(node.name) val rpcUsers = node.rpcUsers + val notary = if (node.notary != null) mapOf("notary" to node.notary) else emptyMap() val config = ConfigHelper.loadConfig( baseDirectory = baseDirectory(name), allowMissingConfig = true, - configOverrides = node.config + mapOf( + configOverrides = node.config + notary + mapOf( "extraAdvertisedServiceIds" to node.advertisedServices, "networkMapService" to networkMapServiceConfigLookup(name), - "rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers, - "notaryClusterAddresses" to node.notaryClusterAddresses + "rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers ) ) startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize) } } + // TODO This mapping is done is several plaecs including the gradle plugin. In general we need a better way of + // generating the configs for the nodes, probably making use of Any.toConfig() + private fun NotaryConfig.toConfigMap(): Map = mapOf("notary" to toConfig().root().unwrapped()) + override fun startNotaryCluster( notaryName: CordaX500Name, clusterSize: Int, - type: ServiceType, verifierType: VerifierType, rpcUsers: List, startInSameProcess: Boolean? ): CordaFuture>> { + fun notaryConfig(nodeAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): Map { + val clusterAddresses = if (clusterAddress != null) listOf(clusterAddress) else emptyList() + val config = NotaryConfig(validating = true, raft = RaftConfig(nodeAddress = nodeAddress, clusterAddresses = clusterAddresses)) + return config.toConfigMap() + } + val nodeNames = (0 until clusterSize).map { CordaX500Name("Notary Service $it", "Zurich", "CH") } val paths = nodeNames.map { baseDirectory(it) } ServiceIdentityGenerator.generateToDisk(paths, notaryName) - val advertisedServices = setOf(ServiceInfo(type, notaryName)) - val notaryClusterAddress = portAllocation.nextHostAndPort() + val clusterAddress = portAllocation.nextHostAndPort() // Start the first node that will bootstrap the cluster val firstNotaryFuture = startNode( providedName = nodeNames.first(), - advertisedServices = advertisedServices, rpcUsers = rpcUsers, verifierType = verifierType, - customOverrides = mapOf("notaryNodeAddress" to notaryClusterAddress.toString(), - "database.serverNameTablePrefix" to if (nodeNames.isNotEmpty()) nodeNames.first().toString().replace(Regex("[^0-9A-Za-z]+"), "") else ""), + customOverrides = notaryConfig(clusterAddress) + mapOf( + "database.serverNameTablePrefix" to if (nodeNames.isNotEmpty()) nodeNames.first().toString().replace(Regex("[^0-9A-Za-z]+"), "") else "" + ), startInSameProcess = startInSameProcess ) // All other nodes will join the cluster val restNotaryFutures = nodeNames.drop(1).map { val nodeAddress = portAllocation.nextHostAndPort() - val configOverride = mapOf("notaryNodeAddress" to nodeAddress.toString(), "notaryClusterAddresses" to listOf(notaryClusterAddress.toString()), - "database.serverNameTablePrefix" to it.toString().replace(Regex("[^0-9A-Za-z]+"), "")) - startNode(providedName = it, advertisedServices = advertisedServices, rpcUsers = rpcUsers, verifierType = verifierType, customOverrides = configOverride) + startNode( + providedName = it, + rpcUsers = rpcUsers, + verifierType = verifierType, + customOverrides = notaryConfig(nodeAddress, clusterAddress) + mapOf( + "database.serverNameTablePrefix" to it.toString().replace(Regex("[^0-9A-Za-z]+"), "") + )) } return firstNotaryFuture.flatMap { firstNotary -> diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/CordformUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/CordformUtils.kt index c7006d02f4..80f078d2bf 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/CordformUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/CordformUtils.kt @@ -5,9 +5,10 @@ package net.corda.testing.internal.demorun import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformNode import net.corda.core.identity.CordaX500Name -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.nodeapi.internal.ServiceInfo +import net.corda.node.services.config.NotaryConfig import net.corda.nodeapi.User +import net.corda.nodeapi.config.toConfig +import net.corda.nodeapi.internal.ServiceInfo fun CordformDefinition.node(configure: CordformNode.() -> Unit) { addNode { cordformNode -> cordformNode.configure() } @@ -19,10 +20,10 @@ fun CordformNode.rpcUsers(vararg users: User) { rpcUsers = users.map { it.toMap() } } +fun CordformNode.notary(notaryConfig: NotaryConfig) { + notary = notaryConfig.toConfig().root().unwrapped() +} + fun CordformNode.advertisedServices(vararg services: ServiceInfo) { advertisedServices = services.map { it.toString() } } - -fun CordformNode.notaryClusterAddresses(vararg addresses: NetworkHostAndPort) { - notaryClusterAddresses = addresses.map { it.toString() } -} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index e1d1f5c4c3..b1f02788c6 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -19,7 +19,6 @@ import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService import net.corda.core.node.services.NetworkMapCache -import net.corda.core.node.services.NotaryService import net.corda.core.serialization.SerializationWhitelist import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow @@ -28,10 +27,10 @@ import net.corda.finance.utils.WorldMapLocation import net.corda.node.internal.AbstractNode import net.corda.node.internal.StartedNode import net.corda.node.services.api.NetworkMapCacheInternal -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.nodeapi.internal.ServiceType import net.corda.node.services.api.SchemaService +import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.NotaryConfig import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.E2ETestKeyManagementService import net.corda.node.services.messaging.MessagingService @@ -40,18 +39,22 @@ import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.BFTNonValidatingNotaryService import net.corda.node.services.transactions.BFTSMaRt import net.corda.node.services.transactions.InMemoryTransactionVerifierService -import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.node.utilities.CertificateAndKeyPair -import net.corda.testing.* +import net.corda.nodeapi.internal.ServiceInfo +import net.corda.testing.DUMMY_KEY_1 +import net.corda.testing.initialiseTestSerialization import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.resetTestSerialization +import net.corda.testing.testNodeConfiguration import org.apache.activemq.artemis.utils.ReusableLatch import org.slf4j.Logger import java.math.BigInteger import java.nio.file.Path import java.security.KeyPair +import java.security.PublicKey import java.security.cert.X509Certificate import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger @@ -220,17 +223,6 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, return InMemoryNetworkMapService(network, networkMapCache, 1) } - override fun getNotaryIdentity(): PartyAndCertificate? { - val defaultIdentity = super.getNotaryIdentity() - return if (notaryIdentity == null || !notaryIdentity.first.type.isNotary() || defaultIdentity == null) - defaultIdentity - else { - // Ensure that we always have notary in name and type of it. TODO It is temporary solution until we will have proper handling of NetworkParameters - myNotaryIdentity = getTestPartyAndCertificate(defaultIdentity.name, notaryIdentity.second.public) - myNotaryIdentity - } - } - // This is not thread safe, but node construction is done on a single thread, so that should always be fine override fun generateKeyPair(): KeyPair { counter = counter.add(BigInteger.ONE) @@ -277,12 +269,11 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, override fun acceptableLiveFiberCountOnStop(): Int = acceptableLiveFiberCountOnStop - override fun makeCoreNotaryService(type: ServiceType): NotaryService? { - if (type != BFTNonValidatingNotaryService.type) return super.makeCoreNotaryService(type) - return BFTNonValidatingNotaryService(services, myNotaryIdentity!!.owningKey, object : BFTSMaRt.Cluster { + override fun makeBFTCluster(notaryKey: PublicKey, bftSMaRtConfig: BFTSMaRtConfiguration): BFTSMaRt.Cluster { + return object : BFTSMaRt.Cluster { override fun waitUntilAllReplicasHaveInitialized() { - val clusterNodes = mockNet.nodes.filter { myNotaryIdentity!!.owningKey in it.started!!.info.legalIdentities.map { it.owningKey } } - if (clusterNodes.size != configuration.notaryClusterAddresses.size) { + val clusterNodes = mockNet.nodes.filter { notaryKey in it.started!!.info.legalIdentities.map { it.owningKey } } + if (clusterNodes.size != bftSMaRtConfig.clusterAddresses.size) { throw IllegalStateException("Unable to enumerate all nodes in BFT cluster.") } clusterNodes.forEach { @@ -290,7 +281,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, notaryService.waitUntilReplicaHasInitialized() } } - }) + } } /** @@ -406,16 +397,19 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, } } - /** - * Construct a default notary node. - */ - fun createNotaryNode() = createNotaryNode(DUMMY_NOTARY.name, null, null) + @JvmOverloads + fun createNotaryNode(legalName: CordaX500Name? = null, validating: Boolean = true): StartedNode { + return createNode(legalName = legalName, configOverrides = { + whenever(it.notary).thenReturn(NotaryConfig(validating)) + }) + } - fun createNotaryNode(legalName: CordaX500Name? = null, - notaryIdentity: Pair? = null, - serviceName: CordaX500Name? = null): StartedNode { - return createNode(legalName = legalName, notaryIdentity = notaryIdentity, - advertisedServices = *arrayOf(ServiceInfo(ValidatingNotaryService.type, serviceName))) + fun createNotaryNode(legalName: CordaX500Name? = null, + validating: Boolean = true, + nodeFactory: Factory): StartedNode { + return createNode(legalName = legalName, nodeFactory = nodeFactory, configOverrides = { + whenever(it.notary).thenReturn(NotaryConfig(validating)) + }) } @JvmOverloads diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt index 2a126e5d21..3a8f838ffb 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt @@ -5,19 +5,16 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.internal.concurrent.* import net.corda.core.internal.createDirectories import net.corda.core.internal.div +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow import net.corda.node.internal.Node import net.corda.node.internal.StartedNode -import net.corda.node.services.config.ConfigHelper -import net.corda.node.services.config.FullNodeConfiguration -import net.corda.node.services.config.configOf -import net.corda.node.services.config.plus -import net.corda.node.services.transactions.RaftValidatingNotaryService +import net.corda.node.services.config.* import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.nodeapi.User import net.corda.nodeapi.config.parseAs +import net.corda.nodeapi.config.toConfig import net.corda.nodeapi.internal.ServiceInfo -import net.corda.nodeapi.internal.ServiceType import net.corda.testing.DUMMY_MAP import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.driver.addressMustNotBeBoundFuture @@ -129,30 +126,44 @@ abstract class NodeBasedTest : TestDependencyInjectionBase() { return if (waitForConnection) node.internals.nodeReadyFuture.map { node } else doneFuture(node) } - fun startNotaryCluster(notaryName: CordaX500Name, - clusterSize: Int, - serviceType: ServiceType = RaftValidatingNotaryService.type): CordaFuture>> { + // TODO This method has been added temporarily, to be deleted once the set of notaries is defined at the network level. + fun startNotaryNode(name: CordaX500Name, + rpcUsers: List = emptyList(), + validating: Boolean = true): CordaFuture> { + return startNode(name, rpcUsers = rpcUsers, configOverrides = mapOf("notary" to mapOf("validating" to validating))) + } + + fun startNotaryCluster(notaryName: CordaX500Name, clusterSize: Int): CordaFuture>> { + fun notaryConfig(nodeAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): Map { + val clusterAddresses = if (clusterAddress != null) listOf(clusterAddress) else emptyList() + val config = NotaryConfig(validating = true, raft = RaftConfig(nodeAddress = nodeAddress, clusterAddresses = clusterAddresses)) + return mapOf("notary" to config.toConfig().root().unwrapped()) + } + ServiceIdentityGenerator.generateToDisk( (0 until clusterSize).map { baseDirectory(notaryName.copy(organisation = "${notaryName.organisation}-$it")) }, notaryName) - val serviceInfo = ServiceInfo(serviceType, notaryName) - val nodeAddresses = getFreeLocalPorts("localhost", clusterSize).map { it.toString() } + val nodeAddresses = getFreeLocalPorts("localhost", clusterSize) val masterNodeFuture = startNode( CordaX500Name(organisation = "${notaryName.organisation}-0", locality = notaryName.locality, country = notaryName.country), - advertisedServices = setOf(serviceInfo), - configOverrides = mapOf("notaryNodeAddress" to nodeAddresses[0], - "database" to mapOf("serverNameTablePrefix" to if (clusterSize > 1) "${notaryName.organisation}0".replace(Regex("[^0-9A-Za-z]+"), "") else ""))) + configOverrides = notaryConfig(nodeAddresses[0]) + mapOf( + "database" to mapOf( + "serverNameTablePrefix" to if (clusterSize > 1) "${notaryName.organisation}0".replace(Regex("[^0-9A-Za-z]+"), "") else "" + ) + ) + ) val remainingNodesFutures = (1 until clusterSize).map { startNode( CordaX500Name(organisation = "${notaryName.organisation}-$it", locality = notaryName.locality, country = notaryName.country), - advertisedServices = setOf(serviceInfo), - configOverrides = mapOf( - "notaryNodeAddress" to nodeAddresses[it], - "notaryClusterAddresses" to listOf(nodeAddresses[0]), - "database" to mapOf("serverNameTablePrefix" to "${notaryName.organisation}$it".replace(Regex("[^0-9A-Za-z]+"), "")))) + configOverrides = notaryConfig(nodeAddresses[it], nodeAddresses[0]) + mapOf( + "database" to mapOf( + "serverNameTablePrefix" to "${notaryName.organisation}$it".replace(Regex("[^0-9A-Za-z]+"), "") + ) + ) + ) } return remainingNodesFutures.transpose().flatMap { remainingNodes -> diff --git a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt index ef0c1ff87b..cc003edc6b 100644 --- a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt +++ b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt @@ -13,7 +13,7 @@ class NodeConfig( val p2pPort: Int, val rpcPort: Int, val webPort: Int, - val extraServices: List, + val isNotary: Boolean, val users: List, var networkMap: NodeConfig? = null ) { @@ -27,18 +27,25 @@ class NodeConfig( * The configuration object depends upon the networkMap, * which is mutable. */ - fun toFileConfig(): Config = empty() - .withValue("myLegalName", valueFor(legalName.toString())) - .withValue("p2pAddress", addressValueFor(p2pPort)) - .withValue("extraAdvertisedServiceIds", valueFor(extraServices)) - .withFallback(optional("networkMapService", networkMap, { c, n -> - c.withValue("address", addressValueFor(n.p2pPort)) - .withValue("legalName", valueFor(n.legalName.toString())) - })) - .withValue("webAddress", addressValueFor(webPort)) - .withValue("rpcAddress", addressValueFor(rpcPort)) - .withValue("rpcUsers", valueFor(users.map(User::toMap).toList())) - .withValue("useTestClock", valueFor(true)) + //TODO Make use of Any.toConfig + private fun toFileConfig(): Config { + val config = empty() + .withValue("myLegalName", valueFor(legalName.toString())) + .withValue("p2pAddress", addressValueFor(p2pPort)) + .withFallback(optional("networkMapService", networkMap, { c, n -> + c.withValue("address", addressValueFor(n.p2pPort)) + .withValue("legalName", valueFor(n.legalName.toString())) + })) + .withValue("webAddress", addressValueFor(webPort)) + .withValue("rpcAddress", addressValueFor(rpcPort)) + .withValue("rpcUsers", valueFor(users.map(User::toMap).toList())) + .withValue("useTestClock", valueFor(true)) + return if (isNotary) { + config.withValue("notary", ConfigValueFactory.fromMap(mapOf("validating" to true))) + } else { + config + } + } fun toText(): String = toFileConfig().root().render(renderOptions) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt index 141a9e1d68..03dda19564 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt @@ -8,7 +8,7 @@ import java.nio.file.Files import java.nio.file.Path import java.nio.file.StandardCopyOption -class NodeConfig( +class NodeConfig constructor( baseDir: Path, legalName: CordaX500Name, p2pPort: Int, @@ -42,20 +42,22 @@ class NodeConfig( * The configuration object depends upon the networkMap, * which is mutable. */ - fun toFileConfig(): Config = ConfigFactory.empty() - .withValue("myLegalName", valueFor(legalName.toString())) - .withValue("p2pAddress", addressValueFor(p2pPort)) - .withValue("extraAdvertisedServiceIds", valueFor(extraServices)) - .withFallback(optional("networkMapService", networkMap, { c, n -> - c.withValue("address", addressValueFor(n.p2pPort)) - .withValue("legalName", valueFor(n.legalName.toString())) - })) - .withValue("webAddress", addressValueFor(webPort)) - .withValue("rpcAddress", addressValueFor(rpcPort)) - .withValue("rpcUsers", valueFor(users.map(User::toMap).toList())) - .withValue("h2port", valueFor(h2Port)) - .withValue("useTestClock", valueFor(true)) - .withValue("detectPublicIp", valueFor(false)) + fun toFileConfig(): Config { + return ConfigFactory.empty() + .withValue("myLegalName", valueFor(legalName.toString())) + .withValue("p2pAddress", addressValueFor(p2pPort)) + .withValue("extraAdvertisedServiceIds", valueFor(extraServices)) + .withFallback(optional("networkMapService", networkMap, { c, n -> + c.withValue("address", addressValueFor(n.p2pPort)) + .withValue("legalName", valueFor(n.legalName.toString())) + })) + .withValue("webAddress", addressValueFor(webPort)) + .withValue("rpcAddress", addressValueFor(rpcPort)) + .withValue("rpcUsers", valueFor(users.map(User::toMap).toList())) + .withValue("h2port", valueFor(h2Port)) + .withValue("useTestClock", valueFor(true)) + .withValue("detectPublicIp", valueFor(false)) + } fun toText(): String = toFileConfig().root().render(renderOptions) diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/UserTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/UserTest.kt index e902bcfec9..5376c0915e 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/UserTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/UserTest.kt @@ -11,7 +11,7 @@ class UserTest { val user = toUser(emptyMap()) assertEquals("none", user.username) assertEquals("none", user.password) - assertEquals(emptySet(), user.permissions) + assertEquals(emptySet(), user.permissions) } @Test @@ -33,7 +33,7 @@ class UserTest { val map = user.toMap() assertEquals("MyName", map["username"]) assertEquals("MyPassword", map["password"]) - assertEquals(setOf("Flow.MyFlow"), map["permissions"]) + assertEquals(listOf("Flow.MyFlow"), map["permissions"]) } @Test diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt index 706d5e3649..5dbb09753c 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -21,10 +21,9 @@ import net.corda.finance.flows.* import net.corda.finance.flows.CashExitFlow.ExitRequest import net.corda.finance.flows.CashIssueAndPaymentFlow.IssueAndPaymentRequest import net.corda.node.services.FlowPermissions.Companion.startFlowPermission -import net.corda.node.services.transactions.SimpleNotaryService +import net.corda.nodeapi.User import net.corda.nodeapi.internal.ServiceInfo import net.corda.nodeapi.internal.ServiceType -import net.corda.nodeapi.User import net.corda.testing.ALICE import net.corda.testing.BOB import net.corda.testing.DUMMY_NOTARY @@ -70,8 +69,7 @@ class ExplorerSimulation(val options: OptionSet) { val portAllocation = PortAllocation.Incremental(20000) driver(portAllocation = portAllocation, extraCordappPackagesToScan = listOf("net.corda.finance")) { // TODO : Supported flow should be exposed somehow from the node instead of set of ServiceInfo. - val notary = startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)), - customOverrides = mapOf("nearestCity" to "Zurich")) + val notary = startNotaryNode(DUMMY_NOTARY.name, customOverrides = mapOf("nearestCity" to "Zurich"), validating = false) val alice = startNode(providedName = ALICE.name, rpcUsers = arrayListOf(user), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))), customOverrides = mapOf("nearestCity" to "Milan")) diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt index 4ce985d0f5..6648fdb83d 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt @@ -10,12 +10,10 @@ import net.corda.finance.DOLLARS import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.config.VerifierType -import net.corda.node.services.transactions.ValidatingNotaryService -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.ALICE import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.driver.NetworkMapStartStrategy import net.corda.testing.chooseIdentity +import net.corda.testing.driver.NetworkMapStartStrategy import org.junit.Test import java.util.* import java.util.concurrent.atomic.AtomicInteger @@ -117,7 +115,7 @@ class VerifierTests { extraCordappPackagesToScan = listOf("net.corda.finance.contracts") ) { val aliceFuture = startNode(providedName = ALICE.name) - val notaryFuture = startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type)), verifierType = VerifierType.OutOfProcess) + val notaryFuture = startNotaryNode(DUMMY_NOTARY.name, verifierType = VerifierType.OutOfProcess) val alice = aliceFuture.get() val notary = notaryFuture.get() val notaryIdentity = notary.nodeInfo.legalIdentities[1] From 8d46b6bcbfffeb5c0b93957a0a2f70e95ece50e6 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Mon, 9 Oct 2017 09:34:08 +0100 Subject: [PATCH 110/185] Update gradle plugin version for previous commit --- constants.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.properties b/constants.properties index 1965e53715..dd42901d00 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=1.1.1 +gradlePluginsVersion=1.1.2 kotlinVersion=1.1.50 guavaVersion=21.0 bouncycastleVersion=1.57 From a425f82c773176a04bf1cba11f4a721c6bde0087 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Mon, 9 Oct 2017 10:19:37 +0100 Subject: [PATCH 111/185] CORDA-540: Change how standard mutable whitelists structured (#1814) --- .../serialization/CordaClassResolver.kt | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt index 91573ed24e..b614284139 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt @@ -152,14 +152,15 @@ object AllWhitelist : ClassWhitelist { override fun hasListed(type: Class<*>): Boolean = true } -// TODO: Need some concept of from which class loader -class GlobalTransientClassWhiteList(val delegate: ClassWhitelist) : MutableClassWhitelist, ClassWhitelist by delegate { - companion object { - val whitelist: MutableSet = Collections.synchronizedSet(mutableSetOf()) - } +sealed class AbstractMutableClassWhitelist(private val whitelist: MutableSet, private val delegate: ClassWhitelist) : MutableClassWhitelist { override fun hasListed(type: Class<*>): Boolean { - return (type.name in whitelist) || delegate.hasListed(type) + /** + * There are certain delegates like [net.corda.nodeapi.internal.serialization.AllButBlacklisted] + * which may throw when asked whether the type is listed. + * In such situations - it may be a good idea to ask [delegate] first before making a check against own [whitelist]. + */ + return delegate.hasListed(type) || (type.name in whitelist) } override fun add(entry: Class<*>) { @@ -167,21 +168,17 @@ class GlobalTransientClassWhiteList(val delegate: ClassWhitelist) : MutableClass } } +// TODO: Need some concept of from which class loader +class GlobalTransientClassWhiteList(delegate: ClassWhitelist) : AbstractMutableClassWhitelist(GlobalTransientClassWhiteList.whitelist, delegate) { + companion object { + private val whitelist: MutableSet = Collections.synchronizedSet(mutableSetOf()) + } +} + /** - * A whitelist that can be customised via the [net.corda.core.node.SerializationWhitelist], since implements [MutableClassWhitelist]. + * A whitelist that can be customised via the [net.corda.core.node.SerializationWhitelist], since it implements [MutableClassWhitelist]. */ -class TransientClassWhiteList(val delegate: ClassWhitelist) : MutableClassWhitelist, ClassWhitelist by delegate { - val whitelist: MutableSet = Collections.synchronizedSet(mutableSetOf()) - - override fun hasListed(type: Class<*>): Boolean { - return (type.name in whitelist) || delegate.hasListed(type) - } - - override fun add(entry: Class<*>) { - whitelist += entry.name - } -} - +class TransientClassWhiteList(delegate: ClassWhitelist) : AbstractMutableClassWhitelist(Collections.synchronizedSet(mutableSetOf()), delegate) /** * This class is not currently used, but can be installed to log a large number of missing entries from the whitelist From a9508b393cb12f6f15202c52e19f7495a1ea6a8d Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Mon, 9 Oct 2017 11:22:11 +0100 Subject: [PATCH 112/185] CORDA-540: AMQP Private key serializer (#1838) --- .../serialization/AMQPSerializationScheme.kt | 1 + .../amqp/custom/PrivateKeySerializer.kt | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/PrivateKeySerializer.kt diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt index bbc3be72a8..501840e3df 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt @@ -33,6 +33,7 @@ abstract class AbstractAMQPSerializationScheme : SerializationScheme { fun registerCustomSerializers(factory: SerializerFactory) { with(factory) { register(net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer) + register(net.corda.nodeapi.internal.serialization.amqp.custom.PrivateKeySerializer) register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(this)) register(net.corda.nodeapi.internal.serialization.amqp.custom.X500NameSerializer) register(net.corda.nodeapi.internal.serialization.amqp.custom.BigDecimalSerializer) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/PrivateKeySerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/PrivateKeySerializer.kt new file mode 100644 index 0000000000..f9a2d1817d --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/PrivateKeySerializer.kt @@ -0,0 +1,27 @@ +package net.corda.nodeapi.internal.serialization.amqp.custom + +import net.corda.core.crypto.Crypto +import net.corda.core.serialization.SerializationContext.UseCase.* +import net.corda.nodeapi.internal.serialization.amqp.* +import net.corda.nodeapi.internal.serialization.checkUseCase +import org.apache.qpid.proton.codec.Data +import java.lang.reflect.Type +import java.security.PrivateKey +import java.util.* + +object PrivateKeySerializer : CustomSerializer.Implements(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) { + checkUseCase(allowedUseCases) + output.writeObject(obj.encoded, data, clazz) + } + + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): PrivateKey { + val bits = input.readObject(obj, schema, ByteArray::class.java) as ByteArray + return Crypto.decodePrivateKey(bits) + } +} \ No newline at end of file From 0c2289de8c8a3c112d5769dc4d174b67949e00a8 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Mon, 9 Oct 2017 11:32:03 +0100 Subject: [PATCH 113/185] Joel move api docs --- docs/source/building-a-cordapp-index.rst | 1 + docs/source/{corda-modules.rst => corda-api.rst} | 6 +++--- docs/source/getting-set-up.rst | 2 +- docs/source/hello-world-running.rst | 10 ++-------- docs/source/other-index.rst | 1 - docs/source/release-notes.rst | 2 +- 6 files changed, 8 insertions(+), 14 deletions(-) rename docs/source/{corda-modules.rst => corda-api.rst} (97%) diff --git a/docs/source/building-a-cordapp-index.rst b/docs/source/building-a-cordapp-index.rst index 12466338ee..a5ea4e9bfe 100644 --- a/docs/source/building-a-cordapp-index.rst +++ b/docs/source/building-a-cordapp-index.rst @@ -7,5 +7,6 @@ Building a CorDapp cordapp-overview writing-cordapps cordapp-build-systems + corda-api flow-cookbook cheat-sheet \ No newline at end of file diff --git a/docs/source/corda-modules.rst b/docs/source/corda-api.rst similarity index 97% rename from docs/source/corda-modules.rst rename to docs/source/corda-api.rst index 0307db092c..57863be4b7 100644 --- a/docs/source/corda-modules.rst +++ b/docs/source/corda-api.rst @@ -1,7 +1,7 @@ -Corda modules -============= +Corda API +========= -This section describes the APIs that are available for the development of CorDapps: +The following are the core APIs that are used in the development of CorDapps: .. toctree:: :maxdepth: 1 diff --git a/docs/source/getting-set-up.rst b/docs/source/getting-set-up.rst index 1fbe7060b3..f6ce762df1 100644 --- a/docs/source/getting-set-up.rst +++ b/docs/source/getting-set-up.rst @@ -169,7 +169,7 @@ The best way to check that everything is working fine is by taking a deeper look Next, you should read through :doc:`Corda Key Concepts ` to understand how Corda works. By then, you'll be ready to start writing your own CorDapps. Learn how to do this in the -:doc:`Hello, World tutorial `. You may want to refer to the :doc:`Modules documentation ` along the +:doc:`Hello, World tutorial `. You may want to refer to the :doc:`API documentation ` along the way. If you encounter any issues, please see the :doc:`troubleshooting` page, or get in touch with us on the diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index 1bfc2be2df..ae91fe0907 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -117,15 +117,9 @@ commands. We want to create an IOU of 100 with PartyB. We start the ``IOUFlow`` by typing: -.. container:: codeset +.. code:: bash - .. code-block:: java - - start IOUFlow arg0: 99, arg1: "O=PartyB,L=New York,C=US" - - .. code-block:: kotlin - - start IOUFlow iouValue: 99, otherParty: "O=PartyB,L=New York,C=US" + start IOUFlow iouValue: 99, otherParty: "O=PartyB,L=New York,C=US" PartyA and PartyB will automatically agree an IOU. If the flow worked, it should have led to the recording of a new IOU in the vaults of both PartyA and PartyB. diff --git a/docs/source/other-index.rst b/docs/source/other-index.rst index 539608be94..7f6fa6ef66 100644 --- a/docs/source/other-index.rst +++ b/docs/source/other-index.rst @@ -7,6 +7,5 @@ Other json secure-coding-guidelines corda-repo-layout - corda-modules building-the-docs codestyle \ No newline at end of file diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 3552fc329e..72baf8cd6f 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -32,7 +32,7 @@ Our extensive testing frameworks will continue to evolve alongside future Corda we have introduced a new test node driver module to encapsulate all test functionality in support of building standalone node integration tests using our DSL driver. -Please read :doc:`corda-modules` for complete details. +Please read :doc:`corda-api` for complete details. .. note:: it may be necessary to recompile applications against future versions of the API until we begin offering `ABI (Application Binary Interface) `_ stability as well. From f83f1b7010e4f9a614add352eaccd581d8ea5968 Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Mon, 9 Oct 2017 12:49:07 +0100 Subject: [PATCH 114/185] CORDA-599 Fix circular dependency between vault and SH (#1630) Fix circular dependency between the 2 vault objects and SH. --- .../kotlin/net/corda/core/node/ServiceHub.kt | 40 ++++-------- .../transactions/NotaryChangeTransactions.kt | 6 +- .../core/transactions/SignedTransaction.kt | 7 +- .../contracts/LedgerTransactionQueryTests.kt | 2 +- .../net/corda/node/internal/AbstractNode.kt | 16 +++-- .../net/corda/node/internal/StartedNode.kt | 15 +++++ .../node/services/api/ServiceHubInternal.kt | 4 +- .../node/services/api/VaultServiceInternal.kt | 19 ++++++ .../node/services/vault/NodeVaultService.kt | 64 ++++++++----------- .../events/NodeSchedulerServiceTest.kt | 4 +- .../persistence/DBTransactionStorageTests.kt | 12 ++-- .../persistence/HibernateConfigurationTest.kt | 6 +- .../services/vault/NodeVaultServiceTest.kt | 10 +-- .../node/testing/MockServiceHubInternal.kt | 11 ++-- .../net/corda/testing/node/MockServices.kt | 25 +++++--- .../main/kotlin/net/corda/testing/TestDSL.kt | 6 +- .../corda/testing/node/MockCordappProvider.kt | 23 +++---- 17 files changed, 148 insertions(+), 122 deletions(-) create mode 100644 node/src/main/kotlin/net/corda/node/services/api/VaultServiceInternal.kt diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index 5d23e76dab..c278ac16a3 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -7,10 +7,6 @@ import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.ContractUpgradeFlow -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.AnonymousParty -import net.corda.core.identity.Party -import net.corda.core.internal.toMultiMap import net.corda.core.node.services.* import net.corda.core.serialization.SerializeAsToken import net.corda.core.transactions.FilteredTransaction @@ -20,11 +16,24 @@ import java.security.PublicKey import java.sql.Connection import java.time.Clock +/** + * Part of [ServiceHub]. + */ +interface StateLoader { + /** + * Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState]. + * + * @throws TransactionResolutionException if [stateRef] points to a non-existent transaction. + */ + @Throws(TransactionResolutionException::class) + fun loadState(stateRef: StateRef): TransactionState<*> +} + /** * Subset of node services that are used for loading transactions from the wire into fully resolved, looked up * forms ready for verification. */ -interface ServicesForResolution { +interface ServicesForResolution : StateLoader { /** * An identity service maintains a directory of parties by their associated distinguished name/public keys and thus * supports lookup of a party given its key, or name. The service also manages the certificates linking confidential @@ -37,14 +46,6 @@ interface ServicesForResolution { /** Provides access to anything relating to cordapps including contract attachment resolution and app context */ val cordappProvider: CordappProvider - - /** - * Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState]. - * - * @throws TransactionResolutionException if the [StateRef] points to a non-existent transaction. - */ - @Throws(TransactionResolutionException::class) - fun loadState(stateRef: StateRef): TransactionState<*> } /** @@ -155,19 +156,6 @@ interface ServiceHub : ServicesForResolution { recordTransactions(true, txs) } - /** - * Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState]. - * - * @throws TransactionResolutionException if [stateRef] points to a non-existent transaction. - */ - @Throws(TransactionResolutionException::class) - override fun loadState(stateRef: StateRef): TransactionState<*> { - val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) - return if (stx.isNotaryChangeTransaction()) { - stx.resolveNotaryChangeTransaction(this).outputs[stateRef.index] - } else stx.tx.outputs[stateRef.index] - } - /** * Converts the given [StateRef] into a [StateAndRef] object. * diff --git a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt index f592f998b7..332718fd94 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt @@ -7,6 +7,7 @@ import net.corda.core.crypto.serializedHash import net.corda.core.utilities.toBase58String import net.corda.core.identity.Party import net.corda.core.node.ServiceHub +import net.corda.core.node.StateLoader import net.corda.core.serialization.CordaSerializable import java.security.PublicKey @@ -39,9 +40,10 @@ data class NotaryChangeWireTransaction( */ override val id: SecureHash by lazy { serializedHash(inputs + notary + newNotary) } - fun resolve(services: ServiceHub, sigs: List): NotaryChangeLedgerTransaction { + fun resolve(services: ServiceHub, sigs: List) = resolve(services as StateLoader, sigs) + fun resolve(stateLoader: StateLoader, sigs: List): NotaryChangeLedgerTransaction { val resolvedInputs = inputs.map { ref -> - services.loadState(ref).let { StateAndRef(it, ref) } + stateLoader.loadState(ref).let { StateAndRef(it, ref) } } return NotaryChangeLedgerTransaction(resolvedInputs, notary, newNotary, id, sigs) } diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt index af811c25b3..4e8602daf6 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -7,6 +7,7 @@ import net.corda.core.crypto.* import net.corda.core.identity.Party import net.corda.core.internal.VisibleForTesting import net.corda.core.node.ServiceHub +import net.corda.core.node.StateLoader import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize @@ -181,10 +182,12 @@ data class SignedTransaction(val txBits: SerializedBytes, * If [transaction] is a [NotaryChangeWireTransaction], loads the input states and resolves it to a * [NotaryChangeLedgerTransaction] so the signatures can be verified. */ - fun resolveNotaryChangeTransaction(services: ServiceHub): NotaryChangeLedgerTransaction { + fun resolveNotaryChangeTransaction(services: ServiceHub) = resolveNotaryChangeTransaction(services as StateLoader) + + fun resolveNotaryChangeTransaction(stateLoader: StateLoader): NotaryChangeLedgerTransaction { val ntx = transaction as? NotaryChangeWireTransaction ?: throw IllegalStateException("Expected a ${NotaryChangeWireTransaction::class.simpleName} but found ${transaction::class.simpleName}") - return ntx.resolve(services, sigs) + return ntx.resolve(stateLoader, sigs) } override fun toString(): String = "${javaClass.simpleName}(id=$id)" diff --git a/core/src/test/kotlin/net/corda/core/contracts/LedgerTransactionQueryTests.kt b/core/src/test/kotlin/net/corda/core/contracts/LedgerTransactionQueryTests.kt index 8273cd9a80..aa8b071299 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/LedgerTransactionQueryTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/LedgerTransactionQueryTests.kt @@ -22,7 +22,7 @@ class LedgerTransactionQueryTests : TestDependencyInjectionBase() { @Before fun setup() { - services.mockCordappProvider.addMockCordapp(DummyContract.PROGRAM_ID, services) + services.mockCordappProvider.addMockCordapp(DummyContract.PROGRAM_ID, services.attachments) } interface Commands { 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 8e9d219d66..640ac65389 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -19,6 +19,10 @@ import net.corda.core.internal.concurrent.flatMap import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.toX509CertHolder import net.corda.core.internal.uncheckedCast +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.messaging.RPCOps +import net.corda.core.messaging.SingleMessageRecipient +import net.corda.core.node.* import net.corda.core.messaging.* import net.corda.core.node.AppServiceHub import net.corda.core.node.NodeInfo @@ -453,7 +457,8 @@ abstract class AbstractNode(config: NodeConfiguration, private fun makeServices(schemaService: SchemaService): MutableList { checkpointStorage = DBCheckpointStorage() cordappProvider = CordappProviderImpl(cordappLoader) - _services = ServiceHubInternalImpl(schemaService) + val transactionStorage = makeTransactionStorage() + _services = ServiceHubInternalImpl(schemaService, transactionStorage, StateLoaderImpl(transactionStorage)) attachments = NodeAttachmentService(services.monitoringService.metrics) cordappProvider.start(attachments) legalIdentity = obtainIdentity(notaryConfig = null) @@ -752,15 +757,18 @@ abstract class AbstractNode(config: NodeConfiguration, protected open fun generateKeyPair() = cryptoGenerateKeyPair() - private inner class ServiceHubInternalImpl(override val schemaService: SchemaService) : ServiceHubInternal, SingletonSerializeAsToken() { + private inner class ServiceHubInternalImpl( + override val schemaService: SchemaService, + override val validatedTransactions: WritableTransactionStorage, + private val stateLoader: StateLoader + ) : SingletonSerializeAsToken(), ServiceHubInternal, StateLoader by stateLoader { override val rpcFlows = ArrayList>>() override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage() override val auditService = DummyAuditService() override val monitoringService = MonitoringService(MetricRegistry()) - override val validatedTransactions = makeTransactionStorage() override val transactionVerifierService by lazy { makeTransactionVerifierService() } override val networkMapCache by lazy { PersistentNetworkMapCache(this) } - override val vaultService by lazy { NodeVaultService(this, database.hibernateConfig) } + override val vaultService by lazy { NodeVaultService(platformClock, keyManagementService, stateLoader, this@AbstractNode.database.hibernateConfig) } override val contractUpgradeService by lazy { ContractUpgradeServiceImpl() } // Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because diff --git a/node/src/main/kotlin/net/corda/node/internal/StartedNode.kt b/node/src/main/kotlin/net/corda/node/internal/StartedNode.kt index 03ad5b5a23..501b8f926a 100644 --- a/node/src/main/kotlin/net/corda/node/internal/StartedNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/StartedNode.kt @@ -1,8 +1,13 @@ package net.corda.node.internal +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionResolutionException +import net.corda.core.contracts.TransactionState import net.corda.core.flows.FlowLogic import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.NodeInfo +import net.corda.core.node.StateLoader +import net.corda.core.node.services.TransactionStorage import net.corda.node.services.api.CheckpointStorage import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.messaging.MessagingService @@ -25,3 +30,13 @@ interface StartedNode { fun dispose() = internals.stop() fun > registerInitiatedFlow(initiatedFlowClass: Class) = internals.registerInitiatedFlow(initiatedFlowClass) } + +class StateLoaderImpl(private val validatedTransactions: TransactionStorage) : StateLoader { + @Throws(TransactionResolutionException::class) + override fun loadState(stateRef: StateRef): TransactionState<*> { + val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) + return if (stx.isNotaryChangeTransaction()) { + stx.resolveNotaryChangeTransaction(this).outputs[stateRef.index] + } else stx.tx.outputs[stateRef.index] + } +} 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 c1fb4f0c1b..65bd38d843 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 @@ -25,7 +25,6 @@ import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.services.statemachine.FlowStateMachineImpl -import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.CordaPersistence interface NetworkMapCacheInternal : NetworkMapCache { @@ -73,6 +72,7 @@ interface ServiceHubInternal : ServiceHub { private val log = loggerFor() } + override val vaultService: VaultServiceInternal /** * A map of hash->tx where tx has been signature/contract validated and the states are known to be correct. * The signatures aren't technically needed after that point, but we keep them around so that we can relay @@ -104,7 +104,7 @@ interface ServiceHubInternal : ServiceHub { if (notifyVault) { val toNotify = recordedTransactions.map { if (it.isNotaryChangeTransaction()) it.notaryChangeTx else it.tx } - (vaultService as NodeVaultService).notifyAll(toNotify) + vaultService.notifyAll(toNotify) } } diff --git a/node/src/main/kotlin/net/corda/node/services/api/VaultServiceInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/VaultServiceInternal.kt new file mode 100644 index 0000000000..fde38dfe05 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/api/VaultServiceInternal.kt @@ -0,0 +1,19 @@ +package net.corda.node.services.api + +import net.corda.core.node.services.VaultService +import net.corda.core.transactions.CoreTransaction +import net.corda.core.transactions.NotaryChangeWireTransaction +import net.corda.core.transactions.WireTransaction + +interface VaultServiceInternal : VaultService { + /** + * Splits the provided [txns] into batches of [WireTransaction] and [NotaryChangeWireTransaction]. + * This is required because the batches get aggregated into single updates, and we want to be able to + * indicate whether an update consists entirely of regular or notary change transactions, which may require + * different processing logic. + */ + fun notifyAll(txns: Iterable) + + /** Same as notifyAll but with a single transaction. */ + fun notify(tx: CoreTransaction) = notifyAll(listOf(tx)) +} 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 15796f15eb..6564859865 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,16 +4,16 @@ 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.internal.ThreadBox -import net.corda.core.internal.VisibleForTesting -import net.corda.core.internal.bufferUntilSubscribed -import net.corda.core.internal.tee -import net.corda.core.messaging.DataFeed -import net.corda.core.node.ServiceHub +import net.corda.core.internal.* +import net.corda.core.node.StateLoader +import net.corda.core.node.services.* import net.corda.core.node.services.StatesNotAvailableException import net.corda.core.node.services.Vault +import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.node.services.vault.Sort +import net.corda.core.node.services.vault.SortAttribute +import net.corda.core.messaging.DataFeed import net.corda.core.node.services.VaultQueryException -import net.corda.core.node.services.VaultService import net.corda.core.node.services.vault.* import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.SerializationDefaults.STORAGE_CONTEXT @@ -24,6 +24,7 @@ import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.NotaryChangeWireTransaction import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.* +import net.corda.node.services.api.VaultServiceInternal import net.corda.node.services.persistence.HibernateConfiguration import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.utilities.DatabaseTransactionManager @@ -33,6 +34,7 @@ import org.hibernate.Session import rx.Observable import rx.subjects.PublishSubject import java.security.PublicKey +import java.time.Clock import java.time.Instant import java.util.* import javax.persistence.Tuple @@ -47,7 +49,7 @@ import javax.persistence.Tuple * TODO: keep an audit trail with time stamps of previously unconsumed states "as of" a particular point in time. * TODO: have transaction storage do some caching. */ -class NodeVaultService(private val services: ServiceHub, private val hibernateConfig: HibernateConfiguration) : SingletonSerializeAsToken(), VaultService { +class NodeVaultService(private val clock: Clock, private val keyManagementService: KeyManagementService, private val stateLoader: StateLoader, private val hibernateConfig: HibernateConfiguration) : SingletonSerializeAsToken(), VaultServiceInternal { private companion object { val log = loggerFor() @@ -78,7 +80,7 @@ class NodeVaultService(private val services: ServiceHub, private val hibernateCo contractStateClassName = stateAndRef.value.state.data.javaClass.name, contractState = stateAndRef.value.state.serialize(context = STORAGE_CONTEXT).bytes, stateStatus = Vault.StateStatus.UNCONSUMED, - recordedTime = services.clock.instant()) + recordedTime = clock.instant()) state.stateRef = PersistentStateRef(stateAndRef.key) session.save(state) } @@ -86,11 +88,11 @@ class NodeVaultService(private val services: ServiceHub, private val hibernateCo val state = session.get(VaultSchemaV1.VaultStates::class.java, PersistentStateRef(stateRef)) state?.run { stateStatus = Vault.StateStatus.CONSUMED - consumedTime = services.clock.instant() + consumedTime = clock.instant() // remove lock (if held) if (lockId != null) { lockId = null - lockUpdateTime = services.clock.instant() + lockUpdateTime = clock.instant() log.trace("Releasing soft lock on consumed state: $stateRef") } session.save(state) @@ -106,13 +108,7 @@ class NodeVaultService(private val services: ServiceHub, private val hibernateCo override val updates: Observable> get() = mutex.locked { _updatesInDbTx } - /** - * Splits the provided [txns] into batches of [WireTransaction] and [NotaryChangeWireTransaction]. - * This is required because the batches get aggregated into single updates, and we want to be able to - * indicate whether an update consists entirely of regular or notary change transactions, which may require - * different processing logic. - */ - fun notifyAll(txns: Iterable) { + override fun notifyAll(txns: Iterable) { // It'd be easier to just group by type, but then we'd lose ordering. val regularTxns = mutableListOf() val notaryChangeTxns = mutableListOf() @@ -140,12 +136,9 @@ class NodeVaultService(private val services: ServiceHub, private val hibernateCo if (notaryChangeTxns.isNotEmpty()) notifyNotaryChange(notaryChangeTxns.toList()) } - /** Same as notifyAll but with a single transaction. */ - fun notify(tx: CoreTransaction) = notifyAll(listOf(tx)) - private fun notifyRegular(txns: Iterable) { fun makeUpdate(tx: WireTransaction): Vault.Update { - val myKeys = services.keyManagementService.filterMyKeys(tx.outputs.flatMap { it.data.participants.map { it.owningKey } }) + val myKeys = keyManagementService.filterMyKeys(tx.outputs.flatMap { it.data.participants.map { it.owningKey } }) val ourNewStates = tx.outputs. filter { isRelevant(it.data, myKeys.toSet()) }. map { tx.outRef(it.data) } @@ -171,8 +164,8 @@ class NodeVaultService(private val services: ServiceHub, private val hibernateCo // We need to resolve the full transaction here because outputs are calculated from inputs // We also can't do filtering beforehand, since output encumbrance pointers get recalculated based on // input positions - val ltx = tx.resolve(services, emptyList()) - val myKeys = services.keyManagementService.filterMyKeys(ltx.outputs.flatMap { it.data.participants.map { it.owningKey } }) + val ltx = tx.resolve(stateLoader, emptyList()) + val myKeys = keyManagementService.filterMyKeys(ltx.outputs.flatMap { it.data.participants.map { it.owningKey } }) val (consumedStateAndRefs, producedStates) = ltx.inputs. zip(ltx.outputs). filter { (_, output) -> isRelevant(output.data, myKeys.toSet()) }. @@ -246,7 +239,7 @@ class NodeVaultService(private val services: ServiceHub, private val hibernateCo @Throws(StatesNotAvailableException::class) override fun softLockReserve(lockId: UUID, stateRefs: NonEmptySet) { - val softLockTimestamp = services.clock.instant() + val softLockTimestamp = clock.instant() try { val session = DatabaseTransactionManager.current().session val criteriaBuilder = session.criteriaBuilder @@ -292,7 +285,7 @@ class NodeVaultService(private val services: ServiceHub, private val hibernateCo } override fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet?) { - val softLockTimestamp = services.clock.instant() + val softLockTimestamp = clock.instant() val session = DatabaseTransactionManager.current().session val criteriaBuilder = session.criteriaBuilder if (stateRefs == null) { @@ -440,8 +433,8 @@ class NodeVaultService(private val services: ServiceHub, private val hibernateCo // pagination checks if (!paging.isDefault) { // pagination - if (paging.pageNumber < DEFAULT_PAGE_NUM) throw VaultQueryException("Page specification: invalid page number ${paging.pageNumber} [page numbers start from ${DEFAULT_PAGE_NUM}]") - if (paging.pageSize < 1) throw VaultQueryException("Page specification: invalid page size ${paging.pageSize} [must be a value between 1 and ${MAX_PAGE_SIZE}]") + if (paging.pageNumber < DEFAULT_PAGE_NUM) throw VaultQueryException("Page specification: invalid page number ${paging.pageNumber} [page numbers start from $DEFAULT_PAGE_NUM]") + if (paging.pageSize < 1) throw VaultQueryException("Page specification: invalid page size ${paging.pageSize} [must be a value between 1 and $MAX_PAGE_SIZE]") } query.firstResult = (paging.pageNumber - 1) * paging.pageSize @@ -452,7 +445,7 @@ class NodeVaultService(private val services: ServiceHub, private val hibernateCo // final pagination check (fail-fast on too many results when no pagination specified) if (paging.isDefault && results.size > DEFAULT_PAGE_SIZE) - throw VaultQueryException("Please specify a `PageSpecification` as there are more results [${results.size}] than the default page size [${DEFAULT_PAGE_SIZE}]") + throw VaultQueryException("Please specify a `PageSpecification` as there are more results [${results.size}] than the default page size [$DEFAULT_PAGE_SIZE]") val statesAndRefs: MutableList> = mutableListOf() val statesMeta: MutableList = mutableListOf() @@ -495,8 +488,7 @@ class NodeVaultService(private val services: ServiceHub, private val hibernateCo override fun _trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class): DataFeed, Vault.Update> { return mutex.locked { val snapshotResults = _queryBy(criteria, paging, sorting, contractStateType) - @Suppress("UNCHECKED_CAST") - val updates = _updatesPublisher.bufferUntilSubscribed().filter { it.containsType(contractStateType, snapshotResults.stateTypes) } as Observable> + val updates: Observable> = uncheckedCast(_updatesPublisher.bufferUntilSubscribed().filter { it.containsType(contractStateType, snapshotResults.stateTypes) }) DataFeed(snapshotResults, updates) } } @@ -522,8 +514,7 @@ class NodeVaultService(private val services: ServiceHub, private val hibernateCo val contractInterfaceToConcreteTypes = mutableMapOf>() distinctTypes.forEach { type -> - @Suppress("UNCHECKED_CAST") - val concreteType = Class.forName(type) as Class + val concreteType: Class = uncheckedCast(Class.forName(type)) val contractInterfaces = deriveContractInterfaces(concreteType) contractInterfaces.map { val contractInterface = contractInterfaceToConcreteTypes.getOrPut(it.name, { mutableSetOf() }) @@ -537,10 +528,9 @@ class NodeVaultService(private val services: ServiceHub, private val hibernateCo private fun deriveContractInterfaces(clazz: Class): Set> { val myInterfaces: MutableSet> = mutableSetOf() clazz.interfaces.forEach { - if (!it.equals(ContractState::class.java)) { - @Suppress("UNCHECKED_CAST") - myInterfaces.add(it as Class) - myInterfaces.addAll(deriveContractInterfaces(it)) + if (it != ContractState::class.java) { + myInterfaces.add(uncheckedCast(it)) + myInterfaces.addAll(deriveContractInterfaces(uncheckedCast(it))) } } return myInterfaces diff --git a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt index 0b7466dddf..ad05ba1ad2 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt @@ -9,12 +9,12 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.node.ServiceHub -import net.corda.core.node.services.VaultService import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.days import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl +import net.corda.node.services.api.VaultServiceInternal import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.persistence.DBCheckpointStorage import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl @@ -96,7 +96,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { overrideClock = testClock, keyManagement = kms, network = mockMessagingService), TestReference { - override val vaultService: VaultService = NodeVaultService(this, database.hibernateConfig) + override val vaultService: VaultServiceInternal = NodeVaultService(testClock, kms, stateLoader, database.hibernateConfig) override val testReference = this@NodeSchedulerServiceTest override val cordappProvider: CordappProviderImpl = CordappProviderImpl(CordappLoader.createWithTestPackages()).start(attachments) } 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 e13460aea9..f043a4c675 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 @@ -6,18 +6,14 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.TransactionSignature import net.corda.core.node.services.VaultService -import net.corda.core.schemas.MappedSchema import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction -import net.corda.finance.schemas.CashSchemaV1 -import net.corda.finance.schemas.SampleCashSchemaV2 -import net.corda.finance.schemas.SampleCashSchemaV3 +import net.corda.node.services.api.VaultServiceInternal import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.services.vault.NodeVaultService -import net.corda.node.services.vault.VaultSchemaV1 import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase import net.corda.testing.* @@ -46,8 +42,8 @@ class DBTransactionStorageTests : TestDependencyInjectionBase() { database.transaction { services = object : MockServices(BOB_KEY) { - override val vaultService: VaultService get() { - val vaultService = NodeVaultService(this, database.hibernateConfig) + override val vaultService: VaultServiceInternal get() { + val vaultService = NodeVaultService(clock, keyManagementService, stateLoader, database.hibernateConfig) hibernatePersister = HibernateObserver(vaultService.rawUpdates, database.hibernateConfig) return vaultService } @@ -57,7 +53,7 @@ class DBTransactionStorageTests : TestDependencyInjectionBase() { validatedTransactions.addTransaction(stx) } // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. - (vaultService as NodeVaultService).notifyAll(txs.map { it.tx }) + vaultService.notifyAll(txs.map { it.tx }) } } } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt index 54c7f933c8..7cb537575b 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt @@ -26,7 +26,6 @@ import net.corda.finance.schemas.SampleCashSchemaV3 import net.corda.finance.utils.sumCash import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService -import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.VaultSchemaV1 import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase @@ -82,14 +81,13 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { database.transaction { hibernateConfig = database.hibernateConfig services = object : MockServices(BOB_KEY, BOC_KEY, DUMMY_NOTARY_KEY) { - override val vaultService: VaultService = makeVaultService(database.hibernateConfig) - + override val vaultService = makeVaultService(database.hibernateConfig) override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { for (stx in txs) { validatedTransactions.addTransaction(stx) } // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. - (vaultService as NodeVaultService).notifyAll(txs.map { it.tx }) + vaultService.notifyAll(txs.map { it.tx }) } override fun jdbcSession() = database.createSession() } 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 2880435862..8b364eb12b 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 @@ -47,7 +47,7 @@ import kotlin.test.assertTrue class NodeVaultServiceTest : TestDependencyInjectionBase() { lateinit var services: MockServices lateinit var issuerServices: MockServices - val vaultService: VaultService get() = services.vaultService + val vaultService get() = services.vaultService as NodeVaultService lateinit var database: CordaPersistence @Before @@ -98,7 +98,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { val originalVault = vaultService val services2 = object : MockServices() { - override val vaultService: NodeVaultService get() = originalVault as NodeVaultService + override val vaultService: NodeVaultService get() = originalVault override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { for (stx in txs) { validatedTransactions.addTransaction(stx) @@ -473,7 +473,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { @Test fun `is ownable state relevant`() { - val service = (services.vaultService as NodeVaultService) + val service = vaultService val amount = Amount(1000, Issued(BOC.ref(1), GBP)) val wellKnownCash = Cash.State(amount, services.myInfo.chooseIdentity()) val myKeys = services.keyManagementService.filterMyKeys(listOf(wellKnownCash.owner.owningKey)) @@ -494,7 +494,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { @Test fun `correct updates are generated for general transactions`() { - val service = (services.vaultService as NodeVaultService) + val service = vaultService val vaultSubscriber = TestSubscriber>().apply { service.updates.subscribe(this) } @@ -527,7 +527,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { @Test fun `correct updates are generated when changing notaries`() { - val service = (services.vaultService as NodeVaultService) + val service = vaultService val notary = services.myInfo.chooseIdentity() val vaultSubscriber = TestSubscriber>().apply { diff --git a/testing/node-driver/src/main/kotlin/net/corda/node/testing/MockServiceHubInternal.kt b/testing/node-driver/src/main/kotlin/net/corda/node/testing/MockServiceHubInternal.kt index e3e54de6d4..c4ca2077cf 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/node/testing/MockServiceHubInternal.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/node/testing/MockServiceHubInternal.kt @@ -6,9 +6,11 @@ import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic import net.corda.core.identity.Party import net.corda.core.node.NodeInfo +import net.corda.core.node.StateLoader import net.corda.core.node.services.* import net.corda.core.serialization.SerializeAsToken import net.corda.node.internal.InitiatedFlowFactory +import net.corda.node.internal.StateLoaderImpl import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.node.serialization.NodeClock @@ -34,7 +36,7 @@ import java.time.Clock open class MockServiceHubInternal( override val database: CordaPersistence, override val configuration: NodeConfiguration, - val customVault: VaultService? = null, + val customVault: VaultServiceInternal? = null, val keyManagement: KeyManagementService? = null, val network: MessagingService? = null, val identity: IdentityService? = MOCK_IDENTITY_SERVICE, @@ -47,11 +49,12 @@ open class MockServiceHubInternal( val schemas: SchemaService? = NodeSchemaService(), val customContractUpgradeService: ContractUpgradeService? = null, val customTransactionVerifierService: TransactionVerifierService? = InMemoryTransactionVerifierService(2), - override val cordappProvider: CordappProvider = CordappProviderImpl(CordappLoader.createDefault(Paths.get("."))).start(attachments) -) : ServiceHubInternal { + override val cordappProvider: CordappProvider = CordappProviderImpl(CordappLoader.createDefault(Paths.get("."))).start(attachments), + protected val stateLoader: StateLoaderImpl = StateLoaderImpl(validatedTransactions) +) : ServiceHubInternal, StateLoader by stateLoader { override val transactionVerifierService: TransactionVerifierService get() = customTransactionVerifierService ?: throw UnsupportedOperationException() - override val vaultService: VaultService + override val vaultService: VaultServiceInternal get() = customVault ?: throw UnsupportedOperationException() override val contractUpgradeService: ContractUpgradeService get() = customContractUpgradeService ?: throw UnsupportedOperationException() 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 367dcad228..6263dfbc65 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 @@ -7,14 +7,17 @@ import net.corda.core.identity.PartyAndCertificate import net.corda.core.messaging.DataFeed import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub +import net.corda.core.node.StateLoader import net.corda.core.node.services.* import net.corda.core.schemas.MappedSchema import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction import net.corda.node.VersionInfo +import net.corda.node.internal.StateLoaderImpl import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.services.api.StateMachineRecordedTransactionMappingStorage +import net.corda.node.services.api.VaultServiceInternal import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.keys.freshCertificate @@ -42,7 +45,12 @@ import java.util.* * A singleton utility that only provides a mock identity, key and storage service. However, this is sufficient for * building chains of transactions and verifying them. It isn't sufficient for testing flows however. */ -open class MockServices(cordappPackages: List = emptyList(), vararg val keys: KeyPair) : ServiceHub { +open class MockServices( + cordappPackages: List, + override val validatedTransactions: WritableTransactionStorage, + protected val stateLoader: StateLoaderImpl = StateLoaderImpl(validatedTransactions), + vararg val keys: KeyPair +) : ServiceHub, StateLoader by stateLoader { companion object { @JvmStatic @@ -105,14 +113,14 @@ open class MockServices(cordappPackages: List = emptyList(), vararg val val mockService = database.transaction { object : MockServices(cordappPackages, *(keys.toTypedArray())) { override val identityService: IdentityService = database.transaction { identityServiceRef } - override val vaultService: VaultService = makeVaultService(database.hibernateConfig) + override val vaultService = makeVaultService(database.hibernateConfig) override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { for (stx in txs) { validatedTransactions.addTransaction(stx) } // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. - (vaultService as NodeVaultService).notifyAll(txs.map { it.tx }) + vaultService.notifyAll(txs.map { it.tx }) } override fun jdbcSession(): Connection = database.createSession() @@ -122,9 +130,9 @@ open class MockServices(cordappPackages: List = emptyList(), vararg val } } + constructor(cordappPackages: List, vararg keys: KeyPair) : this(cordappPackages, MockTransactionStorage(), keys = *keys) constructor(vararg keys: KeyPair) : this(emptyList(), *keys) - - constructor() : this(emptyList(), generateKeyPair()) + constructor() : this(generateKeyPair()) val key: KeyPair get() = keys.first() @@ -137,8 +145,7 @@ open class MockServices(cordappPackages: List = emptyList(), vararg val } } - final override val attachments: AttachmentStorage = MockAttachmentStorage() - override val validatedTransactions: WritableTransactionStorage = MockTransactionStorage() + final override val attachments = MockAttachmentStorage() val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage = MockStateMachineRecordedTransactionMappingStorage() override val identityService: IdentityService = InMemoryIdentityService(MOCK_IDENTITIES, trustRoot = DEV_TRUST_ROOT) override val keyManagementService: KeyManagementService by lazy { MockKeyManagementService(identityService, *keys) } @@ -157,8 +164,8 @@ open class MockServices(cordappPackages: List = emptyList(), vararg val lateinit var hibernatePersister: HibernateObserver - fun makeVaultService(hibernateConfig: HibernateConfiguration = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties(), { identityService })): VaultService { - val vaultService = NodeVaultService(this, hibernateConfig) + fun makeVaultService(hibernateConfig: HibernateConfiguration = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties(), { identityService })): VaultServiceInternal { + val vaultService = NodeVaultService(Clock.systemUTC(), keyManagementService, stateLoader, hibernateConfig) hibernatePersister = HibernateObserver(vaultService.rawUpdates, hibernateConfig) return vaultService } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt index d157583c2e..9b4e99b752 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt @@ -8,10 +8,12 @@ import net.corda.core.flows.FlowException import net.corda.core.identity.Party import net.corda.core.internal.uncheckedCast import net.corda.core.node.ServiceHub +import net.corda.core.node.ServicesForResolution import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import net.corda.testing.contracts.DummyContract +import net.corda.testing.node.MockAttachmentStorage import net.corda.testing.node.MockCordappProvider import java.io.InputStream import java.security.KeyPair @@ -72,7 +74,7 @@ data class TestTransactionDSLInterpreter private constructor( transactionBuilder: TransactionBuilder ) : this(ledgerInterpreter, transactionBuilder, HashMap()) - val services = object : ServiceHub by ledgerInterpreter.services { + val services = object : ServicesForResolution by ledgerInterpreter.services { override fun loadState(stateRef: StateRef) = ledgerInterpreter.resolveStateRef(stateRef) override val cordappProvider: CordappProvider = ledgerInterpreter.services.cordappProvider } @@ -136,7 +138,7 @@ data class TestTransactionDSLInterpreter private constructor( ) = dsl(TransactionDSL(copy())) override fun _attachment(contractClassName: ContractClassName) { - (services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services) + (services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage) } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt index 976d595ec6..dae72192a6 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt @@ -3,7 +3,6 @@ package net.corda.testing.node import net.corda.core.contracts.ContractClassName import net.corda.core.cordapp.Cordapp import net.corda.core.internal.cordapp.CordappImpl -import net.corda.core.node.ServiceHub import net.corda.core.node.services.AttachmentId import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl @@ -13,27 +12,23 @@ import java.util.* class MockCordappProvider(cordappLoader: CordappLoader) : CordappProviderImpl(cordappLoader) { val cordappRegistry = mutableListOf>() - fun addMockCordapp(contractClassName: ContractClassName, services: ServiceHub) { + fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage) { val cordapp = CordappImpl(listOf(contractClassName), emptyList(), emptyList(), emptyList(), emptyList(), emptyList(), emptyList(), emptySet(), Paths.get(".").toUri().toURL()) if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) }) { - cordappRegistry.add(Pair(cordapp, findOrImportAttachment(contractClassName.toByteArray(), services))) + cordappRegistry.add(Pair(cordapp, findOrImportAttachment(contractClassName.toByteArray(), attachments))) } } override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? = cordappRegistry.find { it.first.contractClassNames.contains(contractClassName) }?.second ?: super.getContractAttachmentID(contractClassName) - private fun findOrImportAttachment(data: ByteArray, services: ServiceHub): AttachmentId { - return if (services.attachments is MockAttachmentStorage) { - val existingAttachment = (services.attachments as MockAttachmentStorage).files.filter { - Arrays.equals(it.value, data) - } - if (!existingAttachment.isEmpty()) { - existingAttachment.keys.first() - } else { - services.attachments.importAttachment(data.inputStream()) - } + private fun findOrImportAttachment(data: ByteArray, attachments: MockAttachmentStorage): AttachmentId { + val existingAttachment = attachments.files.filter { + Arrays.equals(it.value, data) + } + return if (!existingAttachment.isEmpty()) { + existingAttachment.keys.first() } else { - throw Exception("MockCordappService only requires MockAttachmentStorage") + attachments.importAttachment(data.inputStream()) } } } From 689758a71cf934dcc0aa87be15b5a842687537dd Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Mon, 9 Oct 2017 13:02:40 +0100 Subject: [PATCH 115/185] CORDA-644: Only serialise Kotlin lambdas when checkpointing. (#1801) * Remove local function because it is serialised as a lambda. * Don't automatically whitelist Kotlin lambdas unless checkpointing. * Add comment to @CordaSerializable, warning not to allow AnnotationTarget.EXPRESSION. --- .../client/rpc/BlacklistKotlinClosureTest.kt | 92 +++++++++++++++++++ .../core/serialization/CordaSerializable.kt | 3 + .../corda/core/utilities/KotlinUtilsTest.kt | 48 +++++++++- .../serialization/CordaClassResolver.kt | 2 - .../statemachine/FlowStateMachineImpl.kt | 6 +- 5 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 client/rpc/src/integration-test/kotlin/net/corda/client/rpc/BlacklistKotlinClosureTest.kt diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/BlacklistKotlinClosureTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/BlacklistKotlinClosureTest.kt new file mode 100644 index 0000000000..776b96f87f --- /dev/null +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/BlacklistKotlinClosureTest.kt @@ -0,0 +1,92 @@ +package net.corda.client.rpc + +import co.paralleluniverse.fibers.Suspendable +import com.esotericsoftware.kryo.KryoException +import net.corda.core.flows.* +import net.corda.core.identity.Party +import net.corda.core.messaging.startFlow +import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.unwrap +import net.corda.node.internal.Node +import net.corda.node.internal.StartedNode +import net.corda.nodeapi.User +import net.corda.testing.* +import net.corda.testing.node.NodeBasedTest +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.ExpectedException + +@CordaSerializable +data class Packet(val x: () -> Long) + +class BlacklistKotlinClosureTest : NodeBasedTest() { + companion object { + @Suppress("UNUSED") val logger = loggerFor() + const val EVIL: Long = 666 + } + + @StartableByRPC + @InitiatingFlow + class FlowC(private val remoteParty: Party, private val data: Packet) : FlowLogic() { + @Suspendable + override fun call() { + val session = initiateFlow(remoteParty) + val x = session.sendAndReceive(data).unwrap { x -> x } + logger.info("FlowC: ${x.x()}") + } + } + + @InitiatedBy(FlowC::class) + class RemoteFlowC(private val session: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + val packet = session.receive().unwrap { x -> x } + logger.info("RemoteFlowC: ${packet.x() + 1}") + session.send(Packet({ packet.x() + 1 })) + } + } + + @JvmField + @Rule + val expectedEx: ExpectedException = ExpectedException.none() + + private val rpcUser = User("user1", "test", permissions = setOf("ALL")) + private lateinit var aliceNode: StartedNode + private lateinit var bobNode: StartedNode + private lateinit var aliceClient: CordaRPCClient + private var connection: CordaRPCConnection? = null + + private fun login(username: String, password: String) { + connection = aliceClient.start(username, password) + } + + @Before + fun setUp() { + setCordappPackages("net.corda.client.rpc") + aliceNode = startNode(ALICE.name, rpcUsers = listOf(rpcUser)).getOrThrow() + bobNode = startNode(BOB.name, rpcUsers = listOf(rpcUser)).getOrThrow() + bobNode.registerInitiatedFlow(RemoteFlowC::class.java) + aliceClient = CordaRPCClient(aliceNode.internals.configuration.rpcAddress!!) + } + + @After + fun done() { + connection?.close() + bobNode.internals.stop() + aliceNode.internals.stop() + unsetCordappPackages() + } + + @Test + fun `closure sent via RPC`() { + login(rpcUser.username, rpcUser.password) + val proxy = connection!!.proxy + expectedEx.expect(KryoException::class.java) + expectedEx.expectMessage("is not annotated or on the whitelist, so cannot be used in serialization") + proxy.startFlow(::FlowC, bobNode.info.chooseIdentity(), Packet{ EVIL }).returnValue.getOrThrow() + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/CordaSerializable.kt b/core/src/main/kotlin/net/corda/core/serialization/CordaSerializable.kt index ff90f2a462..ce80444256 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/CordaSerializable.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/CordaSerializable.kt @@ -11,6 +11,9 @@ import java.lang.annotation.Inherited * * It also makes it possible for a code reviewer to clearly identify the classes that can be passed on the wire. * + * Do NOT include [AnnotationTarget.EXPRESSION] as one of the @Target parameters, as this would allow any Lambda to + * be serialised. This would be a security hole. + * * TODO: As we approach a long term wire format, this annotation will only be permitted on classes that meet certain criteria. */ @Target(AnnotationTarget.CLASS) 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 60bfdd8369..22ef438a6b 100644 --- a/core/src/test/kotlin/net/corda/core/utilities/KotlinUtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/utilities/KotlinUtilsTest.kt @@ -1,14 +1,22 @@ package net.corda.core.utilities +import com.esotericsoftware.kryo.KryoException import net.corda.core.crypto.random63BitValue import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize +import net.corda.nodeapi.internal.serialization.KRYO_CHECKPOINT_CONTEXT import net.corda.testing.TestDependencyInjectionBase import org.assertj.core.api.Assertions.assertThat +import org.junit.Rule import org.junit.Test +import org.junit.rules.ExpectedException class KotlinUtilsTest : TestDependencyInjectionBase() { + @JvmField + @Rule + val expectedEx: ExpectedException = ExpectedException.none() + @Test fun `transient property which is null`() { val test = NullTransientProperty() @@ -18,26 +26,58 @@ class KotlinUtilsTest : TestDependencyInjectionBase() { } @Test - fun `transient property with non-capturing lamba`() { + fun `checkpointing a transient property with non-capturing lamba`() { val original = NonCapturingTransientProperty() val originalVal = original.transientVal - val copy = original.serialize().deserialize() + val copy = original.serialize(context = KRYO_CHECKPOINT_CONTEXT).deserialize(context = KRYO_CHECKPOINT_CONTEXT) val copyVal = copy.transientVal assertThat(copyVal).isNotEqualTo(originalVal) assertThat(copy.transientVal).isEqualTo(copyVal) } @Test - fun `transient property with capturing lamba`() { + fun `serialise transient property with non-capturing lamba`() { + 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() + } + + @Test + fun `deserialise transient property with non-capturing lamba`() { + 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() + } + + @Test + fun `checkpointing a transient property with capturing lamba`() { val original = CapturingTransientProperty("Hello") val originalVal = original.transientVal - val copy = original.serialize().deserialize() + val copy = original.serialize(context = KRYO_CHECKPOINT_CONTEXT).deserialize(context = KRYO_CHECKPOINT_CONTEXT) val copyVal = copy.transientVal assertThat(copyVal).isNotEqualTo(originalVal) assertThat(copy.transientVal).isEqualTo(copyVal) assertThat(copy.transientVal).startsWith("Hello") } + @Test + fun `serialise transient property with capturing lamba`() { + expectedEx.expect(KryoException::class.java) + expectedEx.expectMessage("is not annotated or on the whitelist, so cannot be used in serialization") + val original = CapturingTransientProperty("Hello") + original.serialize() + } + + @Test + fun `deserialise transient property with capturing lamba`() { + expectedEx.expect(KryoException::class.java) + expectedEx.expectMessage("is not annotated or on the whitelist, so cannot be used in serialization") + val original = CapturingTransientProperty("Hello") + original.serialize(context = KRYO_CHECKPOINT_CONTEXT).deserialize() + } + private class NullTransientProperty { var evalCount = 0 val transientValue by transient { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt index b614284139..afcdcbf0c8 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt @@ -63,8 +63,6 @@ class CordaClassResolver(serializationContext: SerializationContext) : DefaultCl if (type.isArray) return checkClass(type.componentType) // Specialised enum entry, so just resolve the parent Enum type since cannot annotate the specialised entry. if (!type.isEnum && Enum::class.java.isAssignableFrom(type)) return checkClass(type.superclass) - // Kotlin lambdas require some special treatment - if (kotlin.jvm.internal.Lambda::class.java.isAssignableFrom(type)) return null // It's safe to have the Class already, since Kryo loads it with initialisation off. // If we use a whitelist with blacklisting capabilities, whitelist.hasListed(type) may throw an IllegalStateException if input class is blacklisted. // Thus, blacklisting precedes annotation checking. 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 b3103f979e..59f40a8533 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 @@ -404,9 +404,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, @Suspendable private fun ReceiveRequest<*>.suspendAndExpectReceive(): ReceivedSessionMessage<*> { - fun pollForMessage() = session.receivedMessages.poll() - - val polledMessage = pollForMessage() + val polledMessage = session.receivedMessages.poll() return if (polledMessage != null) { if (this is SendAndReceive) { // Since we've already received the message, we downgrade to a send only to get the payload out and not @@ -417,7 +415,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, } else { // Suspend while we wait for a receive suspend(this) - pollForMessage() ?: + session.receivedMessages.poll() ?: throw IllegalStateException("Was expecting a ${receiveType.simpleName} but instead got nothing for $this") } } From 20a30b30da98ec4af386efce5e6386c3bd411f1c Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Mon, 9 Oct 2017 13:24:17 +0100 Subject: [PATCH 116/185] Initial stub for IntelliJ Code Style settings (#1832) * Initial stub for IntelliJ Code Style settings * Added exclusion to ".gitignore" --- .gitignore | 1 + .idea/codeStyleSettings.xml | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 .idea/codeStyleSettings.xml diff --git a/.gitignore b/.gitignore index ae3bd7f4de..645707dcfb 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ lib/quasar.jar # Include the -parameters compiler option by default in IntelliJ required for serialization. !.idea/compiler.xml +!.idea/codeStyleSettings.xml # if you remove the above rule, at least ignore the following: diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml new file mode 100644 index 0000000000..e7e9428374 --- /dev/null +++ b/.idea/codeStyleSettings.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file From 29a101c3780a7bde1ccec60ea6425796899058a1 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Mon, 9 Oct 2017 13:46:37 +0100 Subject: [PATCH 117/185] [CORDA-683] Enable `receiveAll()` from Flows. --- .gitignore | 1 + .../kotlin/net/corda/core/flows/FlowLogic.kt | 48 +++++++- .../corda/core/internal/FlowStateMachine.kt | 11 +- .../net/corda/core/flows/FlowTestsUtils.kt | 115 ++++++++++++++++++ .../corda/core/flows/ReceiveAllFlowTests.kt | 87 +++++++++++++ docs/source/changelog.rst | 1 + .../services/statemachine/FlowIORequest.kt | 66 +++++++++- .../statemachine/FlowSessionInternal.kt | 2 + .../statemachine/FlowStateMachineImpl.kt | 34 +++++- .../statemachine/StateMachineManager.kt | 10 +- .../kotlin/net/corda/testing/node/MockNode.kt | 17 ++- 11 files changed, 377 insertions(+), 15 deletions(-) create mode 100644 core/src/test/kotlin/net/corda/core/flows/FlowTestsUtils.kt create mode 100644 core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt diff --git a/.gitignore b/.gitignore index 645707dcfb..ad05b9958f 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ lib/quasar.jar .idea/shelf .idea/dataSources .idea/markdown-navigator +.idea/runConfigurations /gradle-plugins/.idea/ # Include the -parameters compiler option by default in IntelliJ required for serialization. diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt index fd2c144d9e..419c918753 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt @@ -6,6 +6,7 @@ import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.abbreviate +import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.DataFeed import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub @@ -177,6 +178,38 @@ abstract class FlowLogic { return stateMachine.receive(receiveType, otherParty, flowUsedForSessions) } + /** Suspends until a message has been received for each session in the specified [sessions]. + * + * Consider [receiveAll(receiveType: Class, sessions: List): List>] when the same type is expected from all sessions. + * + * Remember that when receiving data from other parties the data should not be trusted until it's been thoroughly + * verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly + * corrupted data in order to exploit your code. + * + * @returns a [Map] containing the objects received, wrapped in an [UntrustworthyData], by the [FlowSession]s who sent them. + */ + @Suspendable + open fun receiveAll(sessions: Map>): Map> { + return stateMachine.receiveAll(sessions, this) + } + + /** + * Suspends until a message has been received for each session in the specified [sessions]. + * + * Consider [sessions: Map>): Map>] when sessions are expected to receive different types. + * + * Remember that when receiving data from other parties the data should not be trusted until it's been thoroughly + * verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly + * corrupted data in order to exploit your code. + * + * @returns a [List] containing the objects received, wrapped in an [UntrustworthyData], with the same order of [sessions]. + */ + @Suspendable + open fun receiveAll(receiveType: Class, sessions: List): List> { + enforceNoDuplicates(sessions) + return castMapValuesToKnownType(receiveAll(associateSessionsToReceiveType(receiveType, sessions))) + } + /** * Queues the given [payload] for sending to the [otherParty] and continues without suspending. * @@ -231,7 +264,6 @@ abstract class FlowLogic { stateMachine.checkFlowPermission(permissionName, extraAuditData) } - /** * Flows can call this method to record application level flow audit events * @param eventType is a string representing the type of event. Each flow is given a distinct namespace for these names. @@ -334,6 +366,18 @@ abstract class FlowLogic { ours.setChildProgressTracker(ours.currentStep, theirs) } } + + private fun enforceNoDuplicates(sessions: List) { + require(sessions.size == sessions.toSet().size) { "A flow session can only appear once as argument." } + } + + private fun associateSessionsToReceiveType(receiveType: Class, sessions: List): Map> { + return sessions.associateByTo(LinkedHashMap(), { it }, { receiveType }) + } + + private fun castMapValuesToKnownType(map: Map>): List> { + return map.values.map { uncheckedCast>(it) } + } } /** @@ -351,4 +395,4 @@ data class FlowInfo( * to deduplicate it from other releases of the same CorDapp, typically a version string. See the * [CorDapp JAR format](https://docs.corda.net/cordapp-build-systems.html#cordapp-jar-format) for more details. */ - val appName: String) + val appName: String) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt b/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt index dc09d6da3f..261af49f02 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt @@ -30,20 +30,20 @@ interface FlowStateMachine { fun receive(receiveType: Class, otherParty: Party, sessionFlow: FlowLogic<*>): UntrustworthyData @Suspendable - fun send(otherParty: Party, payload: Any, sessionFlow: FlowLogic<*>): Unit + fun send(otherParty: Party, payload: Any, sessionFlow: FlowLogic<*>) @Suspendable fun waitForLedgerCommit(hash: SecureHash, sessionFlow: FlowLogic<*>): SignedTransaction - fun checkFlowPermission(permissionName: String, extraAuditData: Map): Unit + fun checkFlowPermission(permissionName: String, extraAuditData: Map) - fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map): Unit + fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map) @Suspendable fun flowStackSnapshot(flowClass: Class>): FlowStackSnapshot? @Suspendable - fun persistFlowStackSnapshot(flowClass: Class>): Unit + fun persistFlowStackSnapshot(flowClass: Class>) val serviceHub: ServiceHub val logger: Logger @@ -51,4 +51,7 @@ interface FlowStateMachine { val resultFuture: CordaFuture val flowInitiator: FlowInitiator val ourIdentityAndCert: PartyAndCertificate + + @Suspendable + fun receiveAll(sessions: Map>, sessionFlow: FlowLogic<*>): Map> } diff --git a/core/src/test/kotlin/net/corda/core/flows/FlowTestsUtils.kt b/core/src/test/kotlin/net/corda/core/flows/FlowTestsUtils.kt new file mode 100644 index 0000000000..6b6f0492b3 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/flows/FlowTestsUtils.kt @@ -0,0 +1,115 @@ +package net.corda.core.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.utilities.UntrustworthyData +import net.corda.core.utilities.unwrap +import net.corda.node.internal.InitiatedFlowFactory +import net.corda.node.internal.StartedNode +import kotlin.reflect.KClass + +/** + * Allows to simplify writing flows that simply rend a message back to an initiating flow. + */ +class Answer(session: FlowSession, override val answer: R, closure: (result: R) -> Unit = {}) : SimpleAnswer(session, closure) + +/** + * Allows to simplify writing flows that simply rend a message back to an initiating flow. + */ +abstract class SimpleAnswer(private val session: FlowSession, private val closure: (result: R) -> Unit = {}) : FlowLogic() { + @Suspendable + override fun call() { + val tmp = answer + closure(tmp) + session.send(tmp) + } + + protected abstract val answer: R +} + +/** + * A flow that does not do anything when triggered. + */ +class NoAnswer(private val closure: () -> Unit = {}) : FlowLogic() { + @Suspendable + override fun call() = closure() +} + +/** + * Allows to register a flow of type [R] against an initiating flow of type [I]. + */ +inline fun , reified R : FlowLogic<*>> StartedNode<*>.registerInitiatedFlow(initiatingFlowType: KClass, crossinline construct: (session: FlowSession) -> R) { + internals.internalRegisterFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> construct(session) }, R::class.javaObjectType, true) +} + +/** + * Allows to register a flow of type [Answer] against an initiating flow of type [I], returning a valure of type [R]. + */ +inline fun , reified R : Any> StartedNode<*>.registerAnswer(initiatingFlowType: KClass, value: R) { + internals.internalRegisterFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> Answer(session, value) }, Answer::class.javaObjectType, true) +} + +/** + * Extracts data from a [Map[FlowSession, UntrustworthyData]] without performing checks and casting to [R]. + */ +@Suppress("UNCHECKED_CAST") +infix fun Map>.from(session: FlowSession): R = this[session]!!.unwrap { it as R } + +/** + * Creates a [Pair([session], [Class])] from this [Class]. + */ +infix fun > T.from(session: FlowSession): Pair = session to this + +/** + * Creates a [Pair([session], [Class])] from this [KClass]. + */ +infix fun KClass.from(session: FlowSession): Pair> = session to this.javaObjectType + +/** + * Suspends until a message has been received for each session in the specified [sessions]. + * + * Consider [receiveAll(receiveType: Class, sessions: List): List>] when the same type is expected from all sessions. + * + * Remember that when receiving data from other parties the data should not be trusted until it's been thoroughly + * verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly + * corrupted data in order to exploit your code. + * + * @returns a [Map] containing the objects received, wrapped in an [UntrustworthyData], by the [FlowSession]s who sent them. + */ +@Suspendable +fun FlowLogic<*>.receiveAll(session: Pair>, vararg sessions: Pair>): Map> { + val allSessions = arrayOf(session, *sessions) + allSessions.enforceNoDuplicates() + return receiveAll(mapOf(*allSessions)) +} + +/** + * Suspends until a message has been received for each session in the specified [sessions]. + * + * Consider [sessions: Map>): Map>] when sessions are expected to receive different types. + * + * Remember that when receiving data from other parties the data should not be trusted until it's been thoroughly + * verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly + * corrupted data in order to exploit your code. + * + * @returns a [List] containing the objects received, wrapped in an [UntrustworthyData], with the same order of [sessions]. + */ +@Suspendable +fun FlowLogic<*>.receiveAll(receiveType: Class, session: FlowSession, vararg sessions: FlowSession): List> = receiveAll(receiveType, listOf(session, *sessions)) + +/** + * Suspends until a message has been received for each session in the specified [sessions]. + * + * Consider [sessions: Map>): Map>] when sessions are expected to receive different types. + * + * Remember that when receiving data from other parties the data should not be trusted until it's been thoroughly + * verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly + * corrupted data in order to exploit your code. + * + * @returns a [List] containing the objects received, wrapped in an [UntrustworthyData], with the same order of [sessions]. + */ +@Suspendable +inline fun FlowLogic<*>.receiveAll(session: FlowSession, vararg sessions: FlowSession): List> = receiveAll(R::class.javaObjectType, listOf(session, *sessions)) + +private fun Array>>.enforceNoDuplicates() { + require(this.size == this.toSet().size) { "A flow session can only appear once as argument." } +} \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt new file mode 100644 index 0000000000..cfadcf4bf5 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt @@ -0,0 +1,87 @@ +package net.corda.core.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.identity.Party +import net.corda.core.utilities.UntrustworthyData +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.unwrap +import net.corda.testing.chooseIdentity +import net.corda.testing.node.network +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class ReceiveMultipleFlowTests { + @Test + fun `receive all messages in parallel using map style`() { + network(3) { nodes, _ -> + val doubleValue = 5.0 + nodes[1].registerAnswer(AlgorithmDefinition::class, doubleValue) + val stringValue = "Thriller" + nodes[2].registerAnswer(AlgorithmDefinition::class, stringValue) + + val flow = nodes[0].services.startFlow(ParallelAlgorithmMap(nodes[1].info.chooseIdentity(), nodes[2].info.chooseIdentity())) + runNetwork() + + val result = flow.resultFuture.getOrThrow() + + assertThat(result).isEqualTo(doubleValue * stringValue.length) + } + } + + @Test + fun `receive all messages in parallel using list style`() { + network(3) { nodes, _ -> + val value1 = 5.0 + nodes[1].registerAnswer(ParallelAlgorithmList::class, value1) + val value2 = 6.0 + nodes[2].registerAnswer(ParallelAlgorithmList::class, value2) + + val flow = nodes[0].services.startFlow(ParallelAlgorithmList(nodes[1].info.chooseIdentity(), nodes[2].info.chooseIdentity())) + runNetwork() + val data = flow.resultFuture.getOrThrow() + + assertThat(data[0]).isEqualTo(value1) + assertThat(data[1]).isEqualTo(value2) + assertThat(data.fold(1.0) { a, b -> a * b }).isEqualTo(value1 * value2) + } + } + + class ParallelAlgorithmMap(doubleMember: Party, stringMember: Party) : AlgorithmDefinition(doubleMember, stringMember) { + @Suspendable + override fun askMembersForData(doubleMember: Party, stringMember: Party): Data { + val doubleSession = initiateFlow(doubleMember) + val stringSession = initiateFlow(stringMember) + val rawData = receiveAll(Double::class from doubleSession, String::class from stringSession) + return Data(rawData from doubleSession, rawData from stringSession) + } + } + + @InitiatingFlow + class ParallelAlgorithmList(private val member1: Party, private val member2: Party) : FlowLogic>() { + @Suspendable + override fun call(): List { + val session1 = initiateFlow(member1) + val session2 = initiateFlow(member2) + val data = receiveAll(session1, session2) + return computeAnswer(data) + } + + private fun computeAnswer(data: List>): List { + return data.map { element -> element.unwrap { it } } + } + } + + @InitiatingFlow + abstract class AlgorithmDefinition(private val doubleMember: Party, private val stringMember: Party) : FlowLogic() { + protected data class Data(val double: Double, val string: String) + + @Suspendable + protected abstract fun askMembersForData(doubleMember: Party, stringMember: Party): Data + + @Suspendable + override fun call(): Double { + val (double, string) = askMembersForData(doubleMember, stringMember) + return double * string.length + } + } +} \ No newline at end of file diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index f07f2e964e..0955876373 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,6 +6,7 @@ from the previous milestone release. UNRELEASED ---------- +* ``FlowLogic`` now exposes a series of function called ``receiveAll(...)`` allowing to join ``receive(...)`` instructions. * ``Cordform`` and node identity generation * Cordform may not specify a value for ``NetworkMap``, when that happens, during the task execution the following happens: diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowIORequest.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowIORequest.kt index 748cce9bd8..cd56d786ad 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowIORequest.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowIORequest.kt @@ -1,5 +1,6 @@ package net.corda.node.services.statemachine +import co.paralleluniverse.fibers.Suspendable import net.corda.core.crypto.SecureHash interface FlowIORequest { @@ -8,7 +9,9 @@ interface FlowIORequest { val stackTraceInCaseOfProblems: StackSnapshot } -interface WaitingRequest : FlowIORequest +interface WaitingRequest : FlowIORequest { + fun shouldResume(message: ExistingSessionMessage, session: FlowSessionInternal): Boolean +} interface SessionedFlowIORequest : FlowIORequest { val session: FlowSessionInternal @@ -21,6 +24,8 @@ interface SendRequest : SessionedFlowIORequest { interface ReceiveRequest : SessionedFlowIORequest, WaitingRequest { val receiveType: Class val userReceiveType: Class<*>? + + override fun shouldResume(message: ExistingSessionMessage, session: FlowSessionInternal): Boolean = this.session === session } data class SendAndReceive(override val session: FlowSessionInternal, @@ -38,6 +43,63 @@ data class ReceiveOnly(override val session: FlowSessionInte override val stackTraceInCaseOfProblems: StackSnapshot = StackSnapshot() } +class ReceiveAll(val requests: List>) : WaitingRequest { + @Transient + override val stackTraceInCaseOfProblems: StackSnapshot = StackSnapshot() + + private fun isComplete(received: LinkedHashMap): Boolean { + return received.keys == requests.map { it.session }.toSet() + } + private fun shouldResumeIfRelevant() = requests.all { hasSuccessfulEndMessage(it) } + + private fun hasSuccessfulEndMessage(it: ReceiveRequest): Boolean { + return it.session.receivedMessages.map { it.message }.any { it is SessionData || it is SessionEnd } + } + + @Suspendable + fun suspendAndExpectReceive(suspend: Suspend): Map { + val receivedMessages = LinkedHashMap() + + poll(receivedMessages) + return if (isComplete(receivedMessages)) { + receivedMessages + } else { + suspend(this) + poll(receivedMessages) + if (isComplete(receivedMessages)) { + receivedMessages + } else { + throw IllegalStateException(requests.filter { it.session !in receivedMessages.keys }.map { "Was expecting a ${it.receiveType.simpleName} but instead got nothing for $it." }.joinToString { "\n" }) + } + } + } + + interface Suspend { + @Suspendable + operator fun invoke(request: FlowIORequest) + } + + @Suspendable + private fun poll(receivedMessages: LinkedHashMap) { + return requests.filter { it.session !in receivedMessages.keys }.forEach { request -> + poll(request)?.let { + receivedMessages[request.session] = RequestMessage(request, it) + } + } + } + + @Suspendable + private fun poll(request: ReceiveRequest): ReceivedSessionMessage<*>? { + return request.session.receivedMessages.poll() + } + + override fun shouldResume(message: ExistingSessionMessage, session: FlowSessionInternal): Boolean = isRelevant(session) && shouldResumeIfRelevant() + + private fun isRelevant(session: FlowSessionInternal) = requests.any { it.session === session } + + data class RequestMessage(val request: ReceiveRequest, val message: ReceivedSessionMessage<*>) +} + data class SendOnly(override val session: FlowSessionInternal, override val message: SessionMessage) : SendRequest { @Transient override val stackTraceInCaseOfProblems: StackSnapshot = StackSnapshot() @@ -46,6 +108,8 @@ data class SendOnly(override val session: FlowSessionInternal, override val mess data class WaitForLedgerCommit(val hash: SecureHash, val fiber: FlowStateMachineImpl<*>) : WaitingRequest { @Transient override val stackTraceInCaseOfProblems: StackSnapshot = StackSnapshot() + + override fun shouldResume(message: ExistingSessionMessage, session: FlowSessionInternal): Boolean = message is ErrorSessionEnd } class StackSnapshot : Throwable("This is a stack trace to help identify the source of the underlying problem") diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowSessionInternal.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowSessionInternal.kt index 4f2d1ba5fc..dc5b39c6f5 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowSessionInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowSessionInternal.kt @@ -2,6 +2,7 @@ package net.corda.node.services.statemachine import net.corda.core.flows.FlowInfo import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession import net.corda.core.identity.Party import net.corda.node.services.statemachine.FlowSessionState.Initiated import net.corda.node.services.statemachine.FlowSessionState.Initiating @@ -15,6 +16,7 @@ import java.util.concurrent.ConcurrentLinkedQueue // TODO rename this class FlowSessionInternal( val flow: FlowLogic<*>, + val flowSession : FlowSession, val ourSessionId: Long, val initiatingParty: Party?, var state: FlowSessionState, 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 59f40a8533..0de001283e 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,9 +12,13 @@ import net.corda.core.crypto.random63BitValue import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate -import net.corda.core.internal.* +import net.corda.core.internal.FlowStateMachine +import net.corda.core.internal.abbreviate import net.corda.core.internal.concurrent.OpenFuture import net.corda.core.internal.concurrent.openFuture +import net.corda.core.internal.isRegularFile +import net.corda.core.internal.staticField +import net.corda.core.internal.uncheckedCast import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.* import net.corda.node.services.api.FlowAppAuditEvent @@ -171,8 +175,8 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, "@${InitiatingFlow::class.java.simpleName} sub-flow." ) } - createNewSession(otherParty, sessionFlow) val flowSession = FlowSessionImpl(otherParty) + createNewSession(otherParty, flowSession, sessionFlow) flowSession.stateMachine = this flowSession.sessionFlow = sessionFlow return flowSession @@ -299,6 +303,22 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, FlowStackSnapshotFactory.instance.persistAsJsonFile(flowClass, serviceHub.configuration.baseDirectory, id) } + @Suspendable + override fun receiveAll(sessions: Map>, sessionFlow: FlowLogic<*>): Map> { + val requests = ArrayList>() + for ((session, receiveType) in sessions) { + val sessionInternal = getConfirmedSession(session.counterparty, sessionFlow) + requests.add(ReceiveOnly(sessionInternal, SessionData::class.java, receiveType)) + } + val receivedMessages = ReceiveAll(requests).suspendAndExpectReceive(suspend) + val result = LinkedHashMap>() + for ((sessionInternal, requestAndMessage) in receivedMessages) { + val message = requestAndMessage.message.confirmReceiveType(requestAndMessage.request) + result[sessionInternal.flowSession] = message.checkPayloadIs(requestAndMessage.request.userReceiveType as Class) + } + return result + } + /** * This method will suspend the state machine and wait for incoming session init response from other party. */ @@ -362,10 +382,11 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, private fun createNewSession( otherParty: Party, + flowSession: FlowSession, sessionFlow: FlowLogic<*> ) { logger.trace { "Creating a new session with $otherParty" } - val session = FlowSessionInternal(sessionFlow, random63BitValue(), null, FlowSessionState.Uninitiated(otherParty)) + val session = FlowSessionInternal(sessionFlow, flowSession, random63BitValue(), null, FlowSessionState.Uninitiated(otherParty)) openSessions[Pair(sessionFlow, otherParty)] = session } @@ -402,6 +423,13 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, return receiveRequest.suspendAndExpectReceive().confirmReceiveType(receiveRequest) } + private val suspend : ReceiveAll.Suspend = object : ReceiveAll.Suspend { + @Suspendable + override fun invoke(request: FlowIORequest) { + suspend(request) + } + } + @Suspendable private fun ReceiveRequest<*>.suspendAndExpectReceive(): ReceivedSessionMessage<*> { val polledMessage = session.receivedMessages.poll() diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index 28e2c58958..c8189063a6 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -15,7 +15,11 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.random63BitValue import net.corda.core.flows.* import net.corda.core.identity.Party -import net.corda.core.internal.* +import net.corda.core.internal.FlowStateMachine +import net.corda.core.internal.ThreadBox +import net.corda.core.internal.bufferUntilSubscribed +import net.corda.core.internal.castIfPossible +import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.DataFeed import net.corda.core.serialization.SerializationDefaults.CHECKPOINT_CONTEXT import net.corda.core.serialization.SerializationDefaults.SERIALIZATION_FACTORY @@ -342,8 +346,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, // commit but a counterparty flow has ended with an error (in which case our flow also has to end) private fun resumeOnMessage(message: ExistingSessionMessage, session: FlowSessionInternal): Boolean { val waitingForResponse = session.fiber.waitingForResponse - return (waitingForResponse as? ReceiveRequest<*>)?.session === session || - waitingForResponse is WaitForLedgerCommit && message is ErrorSessionEnd + return waitingForResponse?.shouldResume(message, session) ?: false } private fun onSessionInit(sessionInit: SessionInit, receivedMessage: ReceivedMessage, sender: Party) { @@ -362,6 +365,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } val session = FlowSessionInternal( flow, + flowSession, random63BitValue(), sender, FlowSessionState.Initiated(sender, senderSessionId, FlowInfo(senderFlowVersion, sessionInit.appName))) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index b1f02788c6..52989475b3 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -51,6 +51,7 @@ import net.corda.testing.resetTestSerialization import net.corda.testing.testNodeConfiguration import org.apache.activemq.artemis.utils.ReusableLatch import org.slf4j.Logger +import java.io.Closeable import java.math.BigInteger import java.nio.file.Path import java.security.KeyPair @@ -81,12 +82,11 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random(), private val defaultFactory: Factory<*> = MockNetwork.DefaultFactory, - private val initialiseSerialization: Boolean = true) { + private val initialiseSerialization: Boolean = true) : Closeable { companion object { // TODO In future PR we're removing the concept of network map node so the details of this mock are not important. val MOCK_NET_MAP = Party(CordaX500Name(organisation = "Mock Network Map", locality = "Madrid", country = "ES"), DUMMY_KEY_1.public) } - var nextNodeId = 0 private set private val filesystem = Jimfs.newFileSystem(unix()) @@ -445,4 +445,17 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, fun waitQuiescent() { busyLatch.await() } + + override fun close() { + stopNodes() + } } + +fun network(nodesCount: Int, action: MockNetwork.(nodes: List>, notary: StartedNode) -> Unit) { + MockNetwork().use { + it.runNetwork() + val notary = it.createNotaryNode() + val nodes = (1..nodesCount).map { _ -> it.createPartyNode() } + action(it, nodes, notary) + } +} \ No newline at end of file From 69ad52cf5c8b4e1931238fc7504c49c6fcaaec53 Mon Sep 17 00:00:00 2001 From: Alberto Arri <30873160+al-r3@users.noreply.github.com> Date: Mon, 9 Oct 2017 13:53:36 +0100 Subject: [PATCH 118/185] Deflake node infoWatcherTest (#1836) * Stop using the watch service, just re-read the whole directory every time. On macOS it's quite unpredictable and the tests are almost always failing. --- .../services/network/NodeInfoWatcherTest.kt | 5 +- .../node/services/network/NodeInfoWatcher.kt | 60 ++++--------------- 2 files changed, 14 insertions(+), 51 deletions(-) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt index 61dee26ad0..f845395020 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt @@ -70,9 +70,9 @@ class NodeInfoWatcherTest : NodeBasedTest() { nodeInfoWatcher.nodeInfoUpdates() .subscribe(testSubscriber) + advanceTime() val readNodes = testSubscriber.onNextEvents.distinct() - advanceTime() assertEquals(0, readNodes.size) } @@ -82,6 +82,7 @@ class NodeInfoWatcherTest : NodeBasedTest() { nodeInfoWatcher.nodeInfoUpdates() .subscribe(testSubscriber) + advanceTime() val readNodes = testSubscriber.onNextEvents.distinct() @@ -114,7 +115,7 @@ class NodeInfoWatcherTest : NodeBasedTest() { } private fun advanceTime() { - scheduler.advanceTimeBy(1, TimeUnit.HOURS) + scheduler.advanceTimeBy(1, TimeUnit.MINUTES) } // Write a nodeInfo under the right path. 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 2d4db9e3fa..b73ded13e3 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,7 +3,12 @@ package net.corda.node.services.network import net.corda.cordform.CordformNode import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignedData -import net.corda.core.internal.* +import net.corda.core.internal.createDirectories +import net.corda.core.internal.div +import net.corda.core.internal.isDirectory +import net.corda.core.internal.isRegularFile +import net.corda.core.internal.list +import net.corda.core.internal.readAll import net.corda.core.node.NodeInfo import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.deserialize @@ -13,10 +18,6 @@ import rx.Observable import rx.Scheduler import rx.schedulers.Schedulers import java.nio.file.Path -import java.nio.file.StandardWatchEventKinds -import java.nio.file.WatchEvent -import java.nio.file.WatchKey -import java.nio.file.WatchService import java.util.concurrent.TimeUnit import kotlin.streams.toList @@ -33,7 +34,6 @@ class NodeInfoWatcher(private val nodePath: Path, private val scheduler: Scheduler = Schedulers.io()) { private val nodeInfoDirectory = nodePath / CordformNode.NODE_INFO_DIRECTORY - private val watchService : WatchService? by lazy { initWatch() } companion object { private val logger = loggerFor() @@ -67,14 +67,15 @@ class NodeInfoWatcher(private val nodePath: Path, * Read all the files contained in [nodePath] / [CordformNode.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 [NodeInfo]s, there is no guarantee that the same value isn't returned more * than once. */ fun nodeInfoUpdates(): Observable { - val pollForFiles = Observable.interval(5, TimeUnit.SECONDS, scheduler) - .flatMapIterable { pollWatch() } - val readCurrentFiles = Observable.from(loadFromDirectory()) - return readCurrentFiles.mergeWith(pollForFiles) + return Observable.interval(5, TimeUnit.SECONDS, scheduler) + .flatMapIterable { loadFromDirectory() } } /** @@ -84,7 +85,6 @@ class NodeInfoWatcher(private val nodePath: Path, * @return a list of [NodeInfo]s */ private fun loadFromDirectory(): List { - val nodeInfoDirectory = nodePath / CordformNode.NODE_INFO_DIRECTORY if (!nodeInfoDirectory.isDirectory()) { logger.info("$nodeInfoDirectory isn't a Directory, not loading NodeInfo from files") return emptyList() @@ -99,31 +99,6 @@ class NodeInfoWatcher(private val nodePath: Path, return result } - // Polls the watchService for changes to nodeInfoDirectory, return all the newly read NodeInfos. - private fun pollWatch(): List { - if (watchService == null) { - return emptyList() - } - val watchKey: WatchKey = watchService?.poll() ?: return emptyList() - val files = mutableSetOf() - for (event in watchKey.pollEvents()) { - val kind = event.kind() - if (kind == StandardWatchEventKinds.OVERFLOW) continue - - val ev: WatchEvent = uncheckedCast(event) - val filename = ev.context() - val absolutePath = nodeInfoDirectory.resolve(filename) - if (absolutePath.isRegularFile()) { - files.add(absolutePath) - } - } - val valid = watchKey.reset() - if (!valid) { - logger.warn("Can't poll $nodeInfoDirectory anymore, it was probably deleted.") - } - return files.mapNotNull { processFile(it) } - } - private fun processFile(file: Path) : NodeInfo? { try { logger.info("Reading NodeInfo from file: $file") @@ -134,17 +109,4 @@ class NodeInfoWatcher(private val nodePath: Path, return null } } - - // Create a WatchService watching for changes in nodeInfoDirectory. - private fun initWatch() : WatchService? { - if (!nodeInfoDirectory.isDirectory()) { - logger.warn("Not watching folder $nodeInfoDirectory it doesn't exist or it's not a directory") - return null - } - val watchService = nodeInfoDirectory.fileSystem.newWatchService() - nodeInfoDirectory.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, - StandardWatchEventKinds.ENTRY_MODIFY) - logger.info("Watching $nodeInfoDirectory for new files") - return watchService - } } From f6e5d2385a5aeffe6d89373c3dac0b978ee021c6 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Mon, 9 Oct 2017 15:24:39 +0100 Subject: [PATCH 119/185] Non-material public API change due to interfaces being re-arranged (#1852) --- .ci/api-current.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 86d889b83d..cd28b2719a 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -1519,7 +1519,6 @@ public interface net.corda.core.node.ServiceHub extends net.corda.core.node.Serv @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.TransactionStorage getValidatedTransactions() @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.VaultService getVaultService() @org.jetbrains.annotations.NotNull public abstract java.sql.Connection jdbcSession() - @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.TransactionState loadState(net.corda.core.contracts.StateRef) public abstract void recordTransactions(Iterable) public abstract void recordTransactions(boolean, Iterable) @org.jetbrains.annotations.NotNull public abstract net.corda.core.transactions.SignedTransaction signInitialTransaction(net.corda.core.transactions.TransactionBuilder) @@ -1527,10 +1526,12 @@ public interface net.corda.core.node.ServiceHub extends net.corda.core.node.Serv @org.jetbrains.annotations.NotNull public abstract net.corda.core.transactions.SignedTransaction signInitialTransaction(net.corda.core.transactions.TransactionBuilder, java.security.PublicKey) @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.StateAndRef toStateAndRef(net.corda.core.contracts.StateRef) ## -public interface net.corda.core.node.ServicesForResolution +public interface net.corda.core.node.ServicesForResolution extends net.corda.core.node.StateLoader @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.AttachmentStorage getAttachments() @org.jetbrains.annotations.NotNull public abstract net.corda.core.cordapp.CordappProvider getCordappProvider() @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.IdentityService getIdentityService() +## +public interface net.corda.core.node.StateLoader @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.TransactionState loadState(net.corda.core.contracts.StateRef) ## public interface net.corda.core.node.services.AttachmentStorage From a633f8dada3b996a3874153a130de4e1dad13882 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Fri, 6 Oct 2017 14:35:27 +0100 Subject: [PATCH 120/185] Reformat files in `core` --- .../corda/core/concurrent/ConcurrencyUtils.kt | 1 + .../kotlin/net/corda/core/contracts/Amount.kt | 2 +- .../net/corda/core/contracts/ContractsDSL.kt | 4 +- .../net/corda/core/contracts/TimeWindow.kt | 1 + .../core/crypto/CordaSecurityProvider.kt | 6 +- .../kotlin/net/corda/core/crypto/Crypto.kt | 4 +- .../net/corda/core/crypto/CryptoUtils.kt | 7 +- .../net/corda/core/crypto/DigitalSignature.kt | 2 + .../net/corda/core/crypto/SecureHash.kt | 15 +++- .../corda/core/crypto/TransactionSignature.kt | 2 +- .../flows/AbstractStateReplacementFlow.kt | 3 +- .../corda/core/flows/CollectSignaturesFlow.kt | 12 +-- .../corda/core/flows/ContractUpgradeFlow.kt | 4 +- .../net/corda/core/flows/FinalityFlow.kt | 4 +- .../net/corda/core/flows/FlowInitiator.kt | 4 + .../net/corda/core/flows/FlowSession.kt | 2 + .../kotlin/net/corda/core/flows/NotaryFlow.kt | 2 +- .../core/flows/ReceiveTransactionFlow.kt | 2 +- .../net/corda/core/identity/AbstractParty.kt | 1 + .../net/corda/core/identity/CordaX500Name.kt | 4 +- .../kotlin/net/corda/core/identity/Party.kt | 1 + .../core/identity/PartyAndCertificate.kt | 7 +- .../kotlin/net/corda/core/internal/Emoji.kt | 48 ++++++++---- .../net/corda/core/internal/InternalUtils.kt | 7 +- .../net/corda/core/internal/LazyPool.kt | 1 + .../net/corda/core/internal/LazyStickyPool.kt | 1 + .../core/internal/ResolveTransactionsFlow.kt | 1 + .../corda/core/internal/WriteOnceProperty.kt | 2 +- .../corda/core/internal/X509EdDSAEngine.kt | 1 + .../net/corda/core/messaging/CordaRPCOps.kt | 9 ++- .../net/corda/core/messaging/FlowHandle.kt | 2 +- .../kotlin/net/corda/core/node/NodeInfo.kt | 7 +- .../net/corda/core/node/services/PartyInfo.kt | 5 +- .../corda/core/node/services/VaultService.kt | 2 +- .../core/node/services/vault/QueryCriteria.kt | 58 +++++++-------- .../node/services/vault/QueryCriteriaUtils.kt | 74 +++++++++++++------ .../net/corda/core/schemas/CommonSchema.kt | 6 +- .../net/corda/core/schemas/NodeInfoSchema.kt | 9 ++- .../net/corda/core/schemas/PersistentTypes.kt | 3 +- .../core/serialization/SerializationAPI.kt | 1 + .../core/transactions/LedgerTransaction.kt | 5 +- .../core/transactions/MerkleTransaction.kt | 14 ++-- .../MissingContractAttachments.kt | 2 +- .../core/transactions/SignedTransaction.kt | 3 +- .../core/transactions/TransactionBuilder.kt | 5 +- .../core/transactions/WireTransaction.kt | 23 +++--- .../net/corda/core/utilities/ByteArrays.kt | 1 + .../net/corda/core/utilities/EncodingUtils.kt | 1 + .../core/utilities/NetworkHostAndPort.kt | 2 +- .../net/corda/core/utilities/NonEmptySet.kt | 2 + .../contracts/CompatibleTransactionTests.kt | 12 +-- .../core/crypto/X509NameConstraintsTest.kt | 2 +- .../core/flows/ContractUpgradeFlowTest.kt | 8 +- .../core/internal/X509EdDSAEngineTest.kt | 10 +-- .../AttachmentSerializationTest.kt | 1 + 55 files changed, 262 insertions(+), 156 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt b/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt index dedc468904..f86ca21fce 100644 --- a/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt +++ b/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt @@ -1,4 +1,5 @@ @file:JvmName("ConcurrencyUtils") + package net.corda.core.concurrent import net.corda.core.internal.concurrent.openFuture diff --git a/core/src/main/kotlin/net/corda/core/contracts/Amount.kt b/core/src/main/kotlin/net/corda/core/contracts/Amount.kt index 64849d3430..629461a3d7 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Amount.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Amount.kt @@ -167,7 +167,7 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, } } } - } catch(e: Exception) { + } catch (e: Exception) { throw IllegalArgumentException("Could not parse $input as a currency", e) } throw IllegalArgumentException("Did not recognise the currency in $input or could not parse") diff --git a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt index d43ae5c001..82a1d348a0 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt @@ -34,7 +34,7 @@ inline fun requireThat(body: Requirements.() -> R) = Requirements.body() /** Filters the command list by type, party and public key all at once. */ inline fun Collection>.select(signer: PublicKey? = null, - party: AbstractParty? = null) = + party: AbstractParty? = null) = filter { it.value is T }. filter { if (signer == null) true else signer in it.signers }. filter { if (party == null) true else party in it.signingParties }. @@ -44,7 +44,7 @@ inline fun Collection> /** Filters the command list by type, parties and public keys all at once. */ inline fun Collection>.select(signers: Collection?, - parties: Collection?) = + parties: Collection?) = filter { it.value is T }. filter { if (signers == null) true else it.signers.containsAll(signers) }. filter { if (parties == null) true else it.signingParties.containsAll(parties) }. diff --git a/core/src/main/kotlin/net/corda/core/contracts/TimeWindow.kt b/core/src/main/kotlin/net/corda/core/contracts/TimeWindow.kt index e26ea50cd0..c8c650257d 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TimeWindow.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TimeWindow.kt @@ -85,6 +85,7 @@ abstract class TimeWindow { init { require(fromTime < untilTime) { "fromTime must be earlier than untilTime" } } + override val midpoint: Instant get() = fromTime + (fromTime until untilTime) / 2 override fun contains(instant: Instant): Boolean = instant >= fromTime && instant < untilTime override fun toString(): String = "[$fromTime, $untilTime)" diff --git a/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt b/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt index eaae5e2ffb..b9bdd52afb 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt @@ -31,6 +31,8 @@ class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME secur object CordaObjectIdentifier { // UUID-based OID // TODO: Register for an OID space and issue our own shorter OID. - @JvmField val COMPOSITE_KEY = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791002") - @JvmField val COMPOSITE_SIGNATURE = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791003") + @JvmField + val COMPOSITE_KEY = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791002") + @JvmField + val COMPOSITE_SIGNATURE = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791003") } diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index b6df5e2bc9..58b68bb176 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -774,9 +774,10 @@ object Crypto { // it forms, by itself, the new private key, which in turn is used to compute the new public key. val pointQ = FixedPointCombMultiplier().multiply(parameterSpec.g, deterministicD) // This is unlikely to happen, but we should check for point at infinity. - if (pointQ.isInfinity) + if (pointQ.isInfinity) { // Instead of throwing an exception, we retry with SHA256(seed). return deriveKeyPairECDSA(parameterSpec, privateKey, seed.sha256().bytes) + } val publicKeySpec = ECPublicKeySpec(pointQ, parameterSpec) val publicKeyD = BCECPublicKey(privateKey.algorithm, publicKeySpec, BouncyCastleProvider.CONFIGURATION) @@ -849,6 +850,7 @@ object Crypto { override fun generatePublic(keyInfo: SubjectPublicKeyInfo?): PublicKey? { return keyInfo?.let { decodePublicKey(signatureScheme, it.encoded) } } + override fun generatePrivate(keyInfo: PrivateKeyInfo?): PrivateKey? { return keyInfo?.let { decodePrivateKey(signatureScheme, it.encoded) } } 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 2dffd5afda..32b14cffcd 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -35,6 +35,7 @@ fun PrivateKey.sign(bytesToSign: ByteArray, publicKey: PublicKey) = DigitalSigna */ @Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) fun KeyPair.sign(bytesToSign: ByteArray) = private.sign(bytesToSign, public) + fun KeyPair.sign(bytesToSign: OpaqueBytes) = sign(bytesToSign.bytes) /** * Helper function for signing a [SignableData] object. @@ -72,18 +73,18 @@ fun PublicKey.verify(content: ByteArray, signature: DigitalSignature) = Crypto.d * @return whether the signature is correct for this key. */ @Throws(IllegalStateException::class, SignatureException::class, IllegalArgumentException::class) -fun PublicKey.isValid(content: ByteArray, signature: DigitalSignature) : Boolean { +fun PublicKey.isValid(content: ByteArray, signature: DigitalSignature): Boolean { if (this is CompositeKey) throw IllegalStateException("Verification of CompositeKey signatures currently not supported.") // TODO CompositeSignature verification. return Crypto.isValid(this, signature.bytes, content) } /** 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() +fun PublicKey.toStringShort(): String = "DL" + this.toSHA256Bytes().toBase58() val PublicKey.keys: Set get() = (this as? CompositeKey)?.leafKeys ?: setOf(this) -fun PublicKey.isFulfilledBy(otherKey: PublicKey): Boolean = isFulfilledBy(setOf(otherKey)) +fun PublicKey.isFulfilledBy(otherKey: PublicKey): Boolean = isFulfilledBy(setOf(otherKey)) fun PublicKey.isFulfilledBy(otherKeys: Iterable): 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]. */ diff --git a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt index 9e28be8a3a..d343913712 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt @@ -23,6 +23,7 @@ open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) { */ @Throws(InvalidKeyException::class, SignatureException::class) fun verify(content: ByteArray) = by.verify(content, this) + /** * Utility to simplify the act of verifying a signature. * @@ -32,6 +33,7 @@ open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) { */ @Throws(InvalidKeyException::class, SignatureException::class) fun verify(content: OpaqueBytes) = by.verify(content.bytes, this) + /** * Utility to simplify the act of verifying a signature. In comparison to [verify] doesn't throw an * exception, making it more suitable where a boolean is required, but normally you should use the function diff --git a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt index f0773a7bac..16bf533550 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt @@ -34,11 +34,18 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) { } } - @JvmStatic fun sha256(bytes: ByteArray) = SHA256(MessageDigest.getInstance("SHA-256").digest(bytes)) - @JvmStatic fun sha256Twice(bytes: ByteArray) = sha256(sha256(bytes).bytes) - @JvmStatic fun sha256(str: String) = sha256(str.toByteArray()) + @JvmStatic + fun sha256(bytes: ByteArray) = SHA256(MessageDigest.getInstance("SHA-256").digest(bytes)) + + @JvmStatic + fun sha256Twice(bytes: ByteArray) = sha256(sha256(bytes).bytes) + + @JvmStatic + fun sha256(str: String) = sha256(str.toByteArray()) + + @JvmStatic + fun randomSHA256() = sha256(newSecureRandom().generateSeed(32)) - @JvmStatic fun randomSHA256() = sha256(newSecureRandom().generateSeed(32)) val zeroHash = SecureHash.SHA256(ByteArray(32, { 0.toByte() })) val allOnesHash = SecureHash.SHA256(ByteArray(32, { 255.toByte() })) } diff --git a/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt index 28e843a82d..4f5f7eb207 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt @@ -11,7 +11,7 @@ import java.util.* * This is similar to [DigitalSignature.WithKey], but targeted to DLT transaction signatures. */ @CordaSerializable -class TransactionSignature(bytes: ByteArray, val by: PublicKey, val signatureMetadata: SignatureMetadata): DigitalSignature(bytes) { +class TransactionSignature(bytes: ByteArray, val by: PublicKey, val signatureMetadata: SignatureMetadata) : DigitalSignature(bytes) { /** * Function to verify a [SignableData] object's signature. * Note that [SignableData] contains the id of the transaction and extra metadata, such as DLT's platform version. diff --git a/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt b/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt index bcc712df71..82a960cfc7 100644 --- a/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt @@ -136,7 +136,8 @@ abstract class AbstractStateReplacementFlow { // We use Void? instead of Unit? as that's what you'd use in Java. abstract class Acceptor(val initiatingSession: FlowSession, override val progressTracker: ProgressTracker = Acceptor.tracker()) : FlowLogic() { - constructor(initiatingSession: FlowSession) : this(initiatingSession, Acceptor.tracker()) + constructor(initiatingSession: FlowSession) : this(initiatingSession, Acceptor.tracker()) + companion object { object VERIFYING : ProgressTracker.Step("Verifying state replacement proposal") object APPROVING : ProgressTracker.Step("State replacement approved") diff --git a/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt b/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt index cfb232a7ec..8dfe4a69ff 100644 --- a/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt @@ -61,11 +61,12 @@ import java.security.PublicKey * just in the states. If null, the default well known identity of the node is used. */ // TODO: AbstractStateReplacementFlow needs updating to use this flow. -class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: SignedTransaction, - val sessionsToCollectFrom: Collection, - val myOptionalKeys: Iterable?, - override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic() { +class CollectSignaturesFlow @JvmOverloads constructor(val partiallySignedTx: SignedTransaction, + val sessionsToCollectFrom: Collection, + val myOptionalKeys: Iterable?, + override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic() { @JvmOverloads constructor(partiallySignedTx: SignedTransaction, sessionsToCollectFrom: Collection, progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : this(partiallySignedTx, sessionsToCollectFrom, null, progressTracker) + companion object { object COLLECTING : ProgressTracker.Step("Collecting signatures from counter-parties.") object VERIFYING : ProgressTracker.Step("Verifying collected signatures.") @@ -134,6 +135,7 @@ class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: Si class CollectSignatureFlow(val partiallySignedTx: SignedTransaction, val session: FlowSession, val signingKeys: List) : FlowLogic>() { constructor(partiallySignedTx: SignedTransaction, session: FlowSession, vararg signingKeys: PublicKey) : this(partiallySignedTx, session, listOf(*signingKeys)) + @Suspendable override fun call(): List { // SendTransactionFlow allows counterparty to access our data to resolve the transaction. @@ -224,7 +226,7 @@ abstract class SignTransactionFlow(val otherSideSession: FlowSession, // Perform some custom verification over the transaction. try { checkTransaction(stx) - } catch(e: Exception) { + } catch (e: Exception) { if (e is IllegalStateException || e is IllegalArgumentException || e is AssertionError) throw FlowException(e) else diff --git a/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt b/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt index d1217909da..423b6c1fa6 100644 --- a/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt @@ -28,7 +28,7 @@ object ContractUpgradeFlow { val stateAndRef: StateAndRef<*>, private val upgradedContractClass: Class> ) : FlowLogic() { - // DOCEND 1 + // DOCEND 1 @Suspendable override fun call(): Void? { val upgrade = upgradedContractClass.newInstance() @@ -50,7 +50,7 @@ object ContractUpgradeFlow { class Deauthorise(val stateRef: StateRef) : FlowLogic() { @Suspendable override fun call(): Void? { - //DOCEND 2 + //DOCEND 2 serviceHub.contractUpgradeService.removeAuthorisedContractUpgrade(stateRef) return null } diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt index 7b45eb6a73..e2aafc165c 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt @@ -26,8 +26,8 @@ import net.corda.core.utilities.ProgressTracker */ @InitiatingFlow class FinalityFlow(val transaction: SignedTransaction, - private val extraRecipients: Set, - override val progressTracker: ProgressTracker) : FlowLogic() { + private val extraRecipients: Set, + override val progressTracker: ProgressTracker) : FlowLogic() { constructor(transaction: SignedTransaction, extraParticipants: Set) : this(transaction, extraParticipants, tracker()) constructor(transaction: SignedTransaction) : this(transaction, emptySet(), tracker()) constructor(transaction: SignedTransaction, progressTracker: ProgressTracker) : this(transaction, emptySet(), progressTracker) diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowInitiator.kt b/core/src/main/kotlin/net/corda/core/flows/FlowInitiator.kt index 549af46dcf..6bc488c162 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowInitiator.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowInitiator.kt @@ -16,18 +16,22 @@ sealed class FlowInitiator : Principal { data class RPC(val username: String) : FlowInitiator() { override fun getName(): String = username } + /** Started when we get new session initiation request. */ data class Peer(val party: Party) : FlowInitiator() { override fun getName(): String = party.name.toString() } + /** Started by a CordaService. */ data class Service(val serviceClassName: String) : FlowInitiator() { override fun getName(): String = serviceClassName } + /** Started as scheduled activity. */ data class Scheduled(val scheduledState: ScheduledStateRef) : FlowInitiator() { override fun getName(): String = "Scheduler" } + // TODO When proper ssh access enabled, add username/use RPC? object Shell : FlowInitiator() { override fun getName(): String = "Shell User" diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt b/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt index 07e4b49d33..f5589f96c4 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt @@ -75,6 +75,7 @@ abstract class FlowSession { inline fun sendAndReceive(payload: Any): UntrustworthyData { return sendAndReceive(R::class.java, payload) } + /** * Serializes and queues the given [payload] object for sending to the [counterparty]. Suspends until a response * is received, which must be of the given [receiveType]. Remember that when receiving data from other parties the data @@ -100,6 +101,7 @@ abstract class FlowSession { inline fun receive(): UntrustworthyData { return receive(R::class.java) } + /** * Suspends until [counterparty] sends us a message of type [receiveType]. * diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt index c4d122c649..c59986c61b 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt @@ -173,5 +173,5 @@ sealed class NotaryError { override fun toString() = cause.toString() } - object WrongNotary: NotaryError() + object WrongNotary : NotaryError() } diff --git a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt index 35f17c64ea..2e686e8746 100644 --- a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt @@ -13,7 +13,7 @@ import java.security.SignatureException * This flow is a combination of [FlowSession.receive], resolve and [SignedTransaction.verify]. This flow will receive the * [SignedTransaction] and perform the resolution back-and-forth required to check the dependencies and download any missing * attachments. The flow will return the [SignedTransaction] after it is resolved and then verified using [SignedTransaction.verify]. - * + * * @param otherSideSession session to the other side which is calling [SendTransactionFlow]. * @param checkSufficientSignatures if true checks all required signatures are present. See [SignedTransaction.verify]. */ diff --git a/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt b/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt index 6f6f1a8109..8f03ed640b 100644 --- a/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt +++ b/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt @@ -13,6 +13,7 @@ import java.security.PublicKey abstract class AbstractParty(val owningKey: PublicKey) { /** Anonymised parties do not include any detail apart from owning key, so equality is dependent solely on the key */ override fun equals(other: Any?): Boolean = other === this || other is AbstractParty && other.owningKey == owningKey + override fun hashCode(): Int = owningKey.hashCode() abstract fun nameOrNull(): CordaX500Name? diff --git a/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt b/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt index a563ebb1ca..37e872d562 100644 --- a/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt +++ b/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt @@ -81,7 +81,7 @@ data class CordaX500Name(val commonName: String?, private val countryCodes: Set = ImmutableSet.copyOf(Locale.getISOCountries()) @JvmStatic - fun build(principal: X500Principal) : CordaX500Name { + fun build(principal: X500Principal): CordaX500Name { val x500Name = X500Name.getInstance(principal.encoded) val attrsMap: Map = x500Name.rdNs .flatMap { it.typesAndValues.asList() } @@ -109,7 +109,7 @@ data class CordaX500Name(val commonName: String?, } @JvmStatic - fun parse(name: String) : CordaX500Name = build(X500Principal(name)) + fun parse(name: String): CordaX500Name = build(X500Principal(name)) } @Transient diff --git a/core/src/main/kotlin/net/corda/core/identity/Party.kt b/core/src/main/kotlin/net/corda/core/identity/Party.kt index 78f36ca543..9316391ae7 100644 --- a/core/src/main/kotlin/net/corda/core/identity/Party.kt +++ b/core/src/main/kotlin/net/corda/core/identity/Party.kt @@ -29,6 +29,7 @@ import java.security.cert.X509Certificate class Party(val name: CordaX500Name, owningKey: PublicKey) : AbstractParty(owningKey) { constructor(certificate: X509Certificate) : this(CordaX500Name.build(certificate.subjectX500Principal), Crypto.toSupportedPublicKey(certificate.publicKey)) + override fun nameOrNull(): CordaX500Name = name fun anonymise(): AnonymousParty = AnonymousParty(owningKey) override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes) diff --git a/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt b/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt index f158138b84..12661d2881 100644 --- a/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt +++ b/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt @@ -11,7 +11,9 @@ import java.security.cert.* */ @CordaSerializable class PartyAndCertificate(val certPath: CertPath) { - @Transient val certificate: X509Certificate + @Transient + val certificate: X509Certificate + init { require(certPath.type == "X.509") { "Only X.509 certificates supported" } val certs = certPath.certificates @@ -19,7 +21,8 @@ class PartyAndCertificate(val certPath: CertPath) { certificate = certs[0] as X509Certificate } - @Transient val party: Party = Party(certificate) + @Transient + val party: Party = Party(certificate) val owningKey: PublicKey get() = party.owningKey val name: CordaX500Name get() = party.name diff --git a/core/src/main/kotlin/net/corda/core/internal/Emoji.kt b/core/src/main/kotlin/net/corda/core/internal/Emoji.kt index 7c7e5202f5..eb057cfe30 100644 --- a/core/src/main/kotlin/net/corda/core/internal/Emoji.kt +++ b/core/src/main/kotlin/net/corda/core/internal/Emoji.kt @@ -13,22 +13,38 @@ object Emoji { (System.getenv("TERM_PROGRAM") == "JediTerm" && System.getProperty("java.vendor") == "JetBrains s.r.o") } - @JvmStatic val CODE_SANTA_CLAUS: String = codePointsString(0x1F385) - @JvmStatic val CODE_DIAMOND: String = codePointsString(0x1F537) - @JvmStatic val CODE_BAG_OF_CASH: String = codePointsString(0x1F4B0) - @JvmStatic val CODE_NEWSPAPER: String = codePointsString(0x1F4F0) - @JvmStatic val CODE_RIGHT_ARROW: String = codePointsString(0x27A1, 0xFE0F) - @JvmStatic val CODE_LEFT_ARROW: String = codePointsString(0x2B05, 0xFE0F) - @JvmStatic val CODE_GREEN_TICK: String = codePointsString(0x2705) - @JvmStatic val CODE_PAPERCLIP: String = codePointsString(0x1F4CE) - @JvmStatic val CODE_COOL_GUY: String = codePointsString(0x1F60E) - @JvmStatic val CODE_NO_ENTRY: String = codePointsString(0x1F6AB) - @JvmStatic val CODE_SKULL_AND_CROSSBONES: String = codePointsString(0x2620) - @JvmStatic val CODE_BOOKS: String = codePointsString(0x1F4DA) - @JvmStatic val CODE_SLEEPING_FACE: String = codePointsString(0x1F634) - @JvmStatic val CODE_LIGHTBULB: String = codePointsString(0x1F4A1) - @JvmStatic val CODE_FREE: String = codePointsString(0x1F193) - @JvmStatic val CODE_SOON: String = codePointsString(0x1F51C) + @JvmStatic + val CODE_SANTA_CLAUS: String = codePointsString(0x1F385) + @JvmStatic + val CODE_DIAMOND: String = codePointsString(0x1F537) + @JvmStatic + val CODE_BAG_OF_CASH: String = codePointsString(0x1F4B0) + @JvmStatic + val CODE_NEWSPAPER: String = codePointsString(0x1F4F0) + @JvmStatic + val CODE_RIGHT_ARROW: String = codePointsString(0x27A1, 0xFE0F) + @JvmStatic + val CODE_LEFT_ARROW: String = codePointsString(0x2B05, 0xFE0F) + @JvmStatic + val CODE_GREEN_TICK: String = codePointsString(0x2705) + @JvmStatic + val CODE_PAPERCLIP: String = codePointsString(0x1F4CE) + @JvmStatic + val CODE_COOL_GUY: String = codePointsString(0x1F60E) + @JvmStatic + val CODE_NO_ENTRY: String = codePointsString(0x1F6AB) + @JvmStatic + val CODE_SKULL_AND_CROSSBONES: String = codePointsString(0x2620) + @JvmStatic + val CODE_BOOKS: String = codePointsString(0x1F4DA) + @JvmStatic + val CODE_SLEEPING_FACE: String = codePointsString(0x1F634) + @JvmStatic + val CODE_LIGHTBULB: String = codePointsString(0x1F4A1) + @JvmStatic + val CODE_FREE: String = codePointsString(0x1F193) + @JvmStatic + val CODE_SOON: String = codePointsString(0x1F51C) /** 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 ccc6cee84c..3996dee1de 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -49,6 +49,7 @@ operator fun Duration.times(multiplicand: Long): Duration = multipliedBy(multipl * separator problems. */ operator fun Path.div(other: String): Path = resolve(other) + operator fun String.div(other: String): Path = Paths.get(this) / other /** @@ -104,6 +105,7 @@ fun Path.copyToDirectory(targetDir: Path, vararg options: CopyOption): Path { Files.copy(this, targetFile, *options) return targetFile } + fun Path.moveTo(target: Path, vararg options: CopyOption): Path = Files.move(this, target, *options) fun Path.isRegularFile(vararg options: LinkOption): Boolean = Files.isRegularFile(this, *options) fun Path.isDirectory(vararg options: LinkOption): Boolean = Files.isDirectory(this, *options) @@ -238,10 +240,13 @@ fun Class.castIfPossible(obj: Any): T? = if (isInstance(obj)) cast(obj) e /** Returns a [DeclaredField] wrapper around the declared (possibly non-public) static field of the receiver [Class]. */ fun Class<*>.staticField(name: String): DeclaredField = DeclaredField(this, name, null) + /** Returns a [DeclaredField] wrapper around the declared (possibly non-public) static field of the receiver [KClass]. */ fun KClass<*>.staticField(name: String): DeclaredField = DeclaredField(java, name, null) + /** @suppress Returns a [DeclaredField] wrapper around the declared (possibly non-public) instance field of the receiver object. */ fun Any.declaredField(name: String): DeclaredField = DeclaredField(javaClass, name, this) + /** * Returns a [DeclaredField] wrapper around the (possibly non-public) instance field of the receiver object, but declared * in its superclass [clazz]. @@ -250,7 +255,7 @@ fun Any.declaredField(name: String): DeclaredField = DeclaredField(javaCl fun Any.declaredField(clazz: KClass<*>, name: String): DeclaredField = DeclaredField(clazz.java, name, this) /** creates a new instance if not a Kotlin object */ -fun KClass.objectOrNewInstance(): T { +fun KClass.objectOrNewInstance(): T { return this.objectInstance ?: this.createInstance() } diff --git a/core/src/main/kotlin/net/corda/core/internal/LazyPool.kt b/core/src/main/kotlin/net/corda/core/internal/LazyPool.kt index 3e4e3a526d..edbb545d20 100644 --- a/core/src/main/kotlin/net/corda/core/internal/LazyPool.kt +++ b/core/src/main/kotlin/net/corda/core/internal/LazyPool.kt @@ -27,6 +27,7 @@ class LazyPool( STARTED, FINISHED } + private val lifeCycle = LifeCycle(State.STARTED) private fun clearIfNeeded(instance: A): A { diff --git a/core/src/main/kotlin/net/corda/core/internal/LazyStickyPool.kt b/core/src/main/kotlin/net/corda/core/internal/LazyStickyPool.kt index 6746989291..5974f97f8e 100644 --- a/core/src/main/kotlin/net/corda/core/internal/LazyStickyPool.kt +++ b/core/src/main/kotlin/net/corda/core/internal/LazyStickyPool.kt @@ -18,6 +18,7 @@ class LazyStickyPool( private class InstanceBox { var instance: LinkedBlockingQueue? = null } + private val random = Random() private val boxes = Array(size) { InstanceBox() } diff --git a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt index c38368aaf6..e0fec0f7ac 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt @@ -28,6 +28,7 @@ class ResolveTransactionsFlow(private val txHashes: Set, constructor(signedTransaction: SignedTransaction, otherSide: FlowSession) : this(dependencyIDs(signedTransaction), otherSide) { this.signedTransaction = signedTransaction } + companion object { private fun dependencyIDs(stx: SignedTransaction) = stx.inputs.map { it.txhash }.toSet() diff --git a/core/src/main/kotlin/net/corda/core/internal/WriteOnceProperty.kt b/core/src/main/kotlin/net/corda/core/internal/WriteOnceProperty.kt index ad0ae9bc39..ae815be6ca 100644 --- a/core/src/main/kotlin/net/corda/core/internal/WriteOnceProperty.kt +++ b/core/src/main/kotlin/net/corda/core/internal/WriteOnceProperty.kt @@ -6,7 +6,7 @@ import kotlin.reflect.KProperty * A write-once property to be used as delegate for Kotlin var properties. The expectation is that this is initialised * prior to the spawning of any threads that may access it and so there's no need for it to be volatile. */ -class WriteOnceProperty(private val defaultValue:T? = null) { +class WriteOnceProperty(private val defaultValue: T? = null) { private var v: T? = defaultValue operator fun getValue(thisRef: Any?, property: KProperty<*>) = v ?: throw IllegalStateException("Write-once property $property not set.") diff --git a/core/src/main/kotlin/net/corda/core/internal/X509EdDSAEngine.kt b/core/src/main/kotlin/net/corda/core/internal/X509EdDSAEngine.kt index 0bebc73130..cd5fac1ee1 100644 --- a/core/src/main/kotlin/net/corda/core/internal/X509EdDSAEngine.kt +++ b/core/src/main/kotlin/net/corda/core/internal/X509EdDSAEngine.kt @@ -46,6 +46,7 @@ class X509EdDSAEngine : Signature { override fun engineSetParameter(params: AlgorithmParameterSpec) = engine.setParameter(params) @Suppress("DEPRECATION") override fun engineGetParameter(param: String): Any = engine.getParameter(param) + @Suppress("DEPRECATION") override fun engineSetParameter(param: String, value: Any?) = engine.setParameter(param, value) } 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 9fddfead5d..d29cc7a193 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -104,12 +104,15 @@ interface CordaRPCOps : RPCOps { fun vaultQuery(contractStateType: Class): Vault.Page { return vaultQueryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractStateType) } + fun vaultQueryByCriteria(criteria: QueryCriteria, contractStateType: Class): Vault.Page { return vaultQueryBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType) } + fun vaultQueryByWithPagingSpec(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification): Vault.Page { return vaultQueryBy(criteria, paging, Sort(emptySet()), contractStateType) } + fun vaultQueryByWithSorting(contractStateType: Class, criteria: QueryCriteria, sorting: Sort): Vault.Page { return vaultQueryBy(criteria, PageSpecification(), sorting, contractStateType) } @@ -142,12 +145,15 @@ interface CordaRPCOps : RPCOps { fun vaultTrack(contractStateType: Class): DataFeed, Vault.Update> { return vaultTrackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractStateType) } + fun vaultTrackByCriteria(contractStateType: Class, criteria: QueryCriteria): DataFeed, Vault.Update> { return vaultTrackBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType) } + fun vaultTrackByWithPagingSpec(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification): DataFeed, Vault.Update> { return vaultTrackBy(criteria, paging, Sort(emptySet()), contractStateType) } + fun vaultTrackByWithSorting(contractStateType: Class, criteria: QueryCriteria, sorting: Sort): DataFeed, Vault.Update> { return vaultTrackBy(criteria, PageSpecification(), sorting, contractStateType) } @@ -250,6 +256,7 @@ interface CordaRPCOps : RPCOps { * @return well known identity, if found. */ fun wellKnownPartyFromAnonymous(party: AbstractParty): Party? + /** Returns the [Party] corresponding to the given key, if found. */ fun partyFromKey(key: PublicKey): Party? @@ -310,7 +317,7 @@ inline fun > CordaRPCOps.startFlow( flowConstructor: () -> R ): FlowHandle = startFlowDynamic(R::class.java) -inline fun > CordaRPCOps.startFlow( +inline fun > CordaRPCOps.startFlow( @Suppress("UNUSED_PARAMETER") flowConstructor: (A) -> R, arg0: A diff --git a/core/src/main/kotlin/net/corda/core/messaging/FlowHandle.kt b/core/src/main/kotlin/net/corda/core/messaging/FlowHandle.kt index 9cd9776061..166825de0c 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/FlowHandle.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/FlowHandle.kt @@ -43,7 +43,7 @@ data class FlowHandleImpl( override val id: StateMachineRunId, override val returnValue: CordaFuture) : FlowHandle { - // Remember to add @Throws to FlowHandle.close() if this throws an exception. + // Remember to add @Throws to FlowHandle.close() if this throws an exception. override fun close() { returnValue.cancel(false) } diff --git a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt index d5ef1df776..cf3e5aed2e 100644 --- a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt +++ b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt @@ -35,9 +35,10 @@ data class NodeInfo(val addresses: List, * are porting code from earlier versions of Corda that expected a single party per node, just use the first item * in the returned list. */ - val legalIdentities: List get() { - return _legalIdentities ?: legalIdentitiesAndCerts.map { it.party }.also { _legalIdentities = it } - } + val legalIdentities: List + get() { + return _legalIdentities ?: legalIdentitiesAndCerts.map { it.party }.also { _legalIdentities = it } + } /** Returns true if [party] is one of the identities of this node, else false. */ fun isLegalIdentity(party: Party): Boolean = party in legalIdentities diff --git a/core/src/main/kotlin/net/corda/core/node/services/PartyInfo.kt b/core/src/main/kotlin/net/corda/core/node/services/PartyInfo.kt index 2db0543618..998c846c20 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/PartyInfo.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/PartyInfo.kt @@ -8,6 +8,7 @@ import net.corda.core.utilities.NetworkHostAndPort */ sealed class PartyInfo { abstract val party: Party - data class SingleNode(override val party: Party, val addresses: List): PartyInfo() - data class DistributedNode(override val party: Party): PartyInfo() + + data class SingleNode(override val party: Party, val addresses: List) : PartyInfo() + data class DistributedNode(override val party: Party) : PartyInfo() } 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 a16243a7db..38545ac754 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 @@ -99,7 +99,7 @@ class Vault(val states: Iterable>) { companion object { val NoUpdate = Update(emptySet(), emptySet(), type = Vault.UpdateType.GENERAL) - val NoNotaryUpdate = Vault.Update(emptySet(), emptySet(), type = Vault.UpdateType.NOTARY_CHANGE) + val NoNotaryUpdate = Vault.Update(emptySet(), emptySet(), type = Vault.UpdateType.NOTARY_CHANGE) } @CordaSerializable 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 6f15271057..1e5eeb858f 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 @@ -49,12 +49,12 @@ sealed class QueryCriteria { /** * VaultQueryCriteria: provides query by attributes defined in [VaultSchema.VaultStates] */ - data class VaultQueryCriteria @JvmOverloads constructor (override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, - override val contractStateTypes: Set>? = null, - val stateRefs: List? = null, - val notary: List? = null, - val softLockingCondition: SoftLockingCondition? = null, - val timeCondition: TimeCondition? = null) : CommonQueryCriteria() { + data class VaultQueryCriteria @JvmOverloads constructor(override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, + override val contractStateTypes: Set>? = null, + val stateRefs: List? = null, + val notary: List? = null, + val softLockingCondition: SoftLockingCondition? = null, + val timeCondition: TimeCondition? = null) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { super.visit(parser) return parser.parseCriteria(this) @@ -69,10 +69,10 @@ sealed class QueryCriteria { val externalId: List? = null, override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, override val contractStateTypes: Set>? = null) : CommonQueryCriteria() { - constructor(participants: List? = null, - linearId: List? = null, - status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, - contractStateTypes: Set>? = null) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes) + constructor(participants: List? = null, + linearId: List? = null, + status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, + contractStateTypes: Set>? = null) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes) override fun visit(parser: IQueryCriteriaParser): Collection { super.visit(parser) @@ -80,13 +80,13 @@ sealed class QueryCriteria { } } - /** - * FungibleStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultFungibleState] - * - * Valid TokenType implementations defined by Amount are - * [Currency] as used in [Cash] contract state - * [Commodity] as used in [CommodityContract] state - */ + /** + * FungibleStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultFungibleState] + * + * Valid TokenType implementations defined by Amount are + * [Currency] as used in [Cash] contract state + * [Commodity] as used in [CommodityContract] state + */ data class FungibleAssetQueryCriteria @JvmOverloads constructor(val participants: List? = null, val owner: List? = null, val quantity: ColumnPredicate? = null, @@ -94,11 +94,11 @@ sealed class QueryCriteria { val issuerRef: List? = null, override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, override val contractStateTypes: Set>? = null) : CommonQueryCriteria() { - override fun visit(parser: IQueryCriteriaParser): Collection { - super.visit(parser) - return parser.parseCriteria(this) - } - } + override fun visit(parser: IQueryCriteriaParser): Collection { + super.visit(parser) + return parser.parseCriteria(this) + } + } /** * VaultCustomQueryCriteria: provides query by custom attributes defined in a contracts @@ -111,9 +111,9 @@ sealed class QueryCriteria { * Refer to [CommercialPaper.State] for a concrete example. */ data class VaultCustomQueryCriteria @JvmOverloads constructor - (val expression: CriteriaExpression, - override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, - override val contractStateTypes: Set>? = null) : CommonQueryCriteria() { + (val expression: CriteriaExpression, + override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, + override val contractStateTypes: Set>? = null) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { super.visit(parser) return parser.parseCriteria(this) @@ -121,13 +121,13 @@ sealed class QueryCriteria { } // enable composition of [QueryCriteria] - private data class AndComposition(val a: QueryCriteria, val b: QueryCriteria): QueryCriteria() { + private data class AndComposition(val a: QueryCriteria, val b: QueryCriteria) : QueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { return parser.parseAnd(this.a, this.b) } } - private data class OrComposition(val a: QueryCriteria, val b: QueryCriteria): QueryCriteria() { + private data class OrComposition(val a: QueryCriteria, val b: QueryCriteria) : QueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { return parser.parseOr(this.a, this.b) } @@ -148,9 +148,9 @@ interface IQueryCriteriaParser { fun parseCriteria(criteria: QueryCriteria.CommonQueryCriteria): Collection fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria): Collection - fun parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria): Collection + fun parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria): Collection fun parseCriteria(criteria: QueryCriteria.VaultQueryCriteria): Collection fun parseOr(left: QueryCriteria, right: QueryCriteria): Collection fun parseAnd(left: QueryCriteria, right: QueryCriteria): Collection - fun parse(criteria: QueryCriteria, sorting: Sort? = null) : Collection + fun parse(criteria: QueryCriteria, sorting: Sort? = null): Collection } 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 1ee9ab89d4..85d7292061 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 @@ -170,8 +170,8 @@ data class Sort(val columns: Collection) { @CordaSerializable data class SortColumn( - val sortAttribute: SortAttribute, - val direction: Sort.Direction = Sort.Direction.ASC) + val sortAttribute: SortAttribute, + val direction: Sort.Direction = Sort.Direction.ASC) } @CordaSerializable @@ -199,8 +199,9 @@ object Builder { fun Field.predicate(predicate: ColumnPredicate) = CriteriaExpression.ColumnPredicateExpression(Column(this), predicate) - fun KProperty1.functionPredicate(predicate: ColumnPredicate, groupByColumns: List>? = null, orderBy: Sort.Direction? = null) + fun KProperty1.functionPredicate(predicate: ColumnPredicate, groupByColumns: List>? = null, orderBy: Sort.Direction? = null) = CriteriaExpression.AggregateFunctionExpression(Column(this), predicate, groupByColumns, orderBy) + fun Field.functionPredicate(predicate: ColumnPredicate, groupByColumns: List>? = null, orderBy: Sort.Direction? = null) = CriteriaExpression.AggregateFunctionExpression(Column(this), predicate, groupByColumns, orderBy) @@ -217,15 +218,32 @@ object Builder { fun > KProperty1.`in`(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)) fun > KProperty1.notIn(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)) - @JvmStatic fun Field.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value)) - @JvmStatic fun Field.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value)) - @JvmStatic fun > Field.lessThan(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN, value) - @JvmStatic fun > Field.lessThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value) - @JvmStatic fun > Field.greaterThan(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN, value) - @JvmStatic fun > Field.greaterThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value) - @JvmStatic fun > Field.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to)) - @JvmStatic fun > Field.`in`(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)) - @JvmStatic fun > Field.notIn(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)) + @JvmStatic + fun Field.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value)) + + @JvmStatic + fun Field.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value)) + + @JvmStatic + fun > Field.lessThan(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN, value) + + @JvmStatic + fun > Field.lessThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value) + + @JvmStatic + fun > Field.greaterThan(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN, value) + + @JvmStatic + fun > Field.greaterThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value) + + @JvmStatic + fun > Field.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to)) + + @JvmStatic + fun > Field.`in`(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)) + + @JvmStatic + fun > Field.notIn(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)) fun equal(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value) fun notEqual(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value) @@ -238,45 +256,57 @@ object Builder { fun > notIn(collection: Collection) = ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection) fun KProperty1.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string)) - @JvmStatic fun Field.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string)) + @JvmStatic + fun Field.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string)) + fun KProperty1.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string)) - @JvmStatic fun Field.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string)) + @JvmStatic + fun Field.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string)) fun KProperty1.isNull() = predicate(ColumnPredicate.NullExpression(NullOperator.IS_NULL)) - @JvmStatic fun Field.isNull() = predicate(ColumnPredicate.NullExpression(NullOperator.IS_NULL)) + @JvmStatic + fun Field.isNull() = predicate(ColumnPredicate.NullExpression(NullOperator.IS_NULL)) + fun KProperty1.notNull() = predicate(ColumnPredicate.NullExpression(NullOperator.NOT_NULL)) - @JvmStatic fun Field.notNull() = predicate(ColumnPredicate.NullExpression(NullOperator.NOT_NULL)) + @JvmStatic + fun Field.notNull() = predicate(ColumnPredicate.NullExpression(NullOperator.NOT_NULL)) /** aggregate functions */ fun KProperty1.sum(groupByColumns: List>? = null, orderBy: Sort.Direction? = null) = functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.SUM), groupByColumns?.map { Column(it) }, orderBy) - @JvmStatic @JvmOverloads + + @JvmStatic + @JvmOverloads fun Field.sum(groupByColumns: List? = null, orderBy: Sort.Direction? = null) = - functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.SUM), groupByColumns?.map { Column(it) }, orderBy) + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.SUM), groupByColumns?.map { Column(it) }, orderBy) fun KProperty1.count() = functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.COUNT)) - @JvmStatic fun Field.count() = functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.COUNT)) + @JvmStatic + fun Field.count() = functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.COUNT)) fun KProperty1.avg(groupByColumns: List>? = null, orderBy: Sort.Direction? = null) = functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.AVG), groupByColumns?.map { Column(it) }, orderBy) + @JvmStatic @JvmOverloads fun Field.avg(groupByColumns: List? = null, orderBy: Sort.Direction? = null) = - functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.AVG), groupByColumns?.map { Column(it) }, orderBy) + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.AVG), groupByColumns?.map { Column(it) }, orderBy) fun KProperty1.min(groupByColumns: List>? = null, orderBy: Sort.Direction? = null) = functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MIN), groupByColumns?.map { Column(it) }, orderBy) + @JvmStatic @JvmOverloads fun Field.min(groupByColumns: List? = null, orderBy: Sort.Direction? = null) = - functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MIN), groupByColumns?.map { Column(it) }, orderBy) + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MIN), groupByColumns?.map { Column(it) }, orderBy) fun KProperty1.max(groupByColumns: List>? = null, orderBy: Sort.Direction? = null) = functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MAX), groupByColumns?.map { Column(it) }, orderBy) + @JvmStatic @JvmOverloads fun Field.max(groupByColumns: List? = null, orderBy: Sort.Direction? = null) = - functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MAX), groupByColumns?.map { Column(it) }, orderBy) + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MAX), groupByColumns?.map { Column(it) }, orderBy) } inline fun builder(block: Builder.() -> A) = block(Builder) diff --git a/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt b/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt index 0f9d985791..1d5559d45d 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt @@ -38,9 +38,9 @@ object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, vers ) : PersistentState() { constructor(uid: UniqueIdentifier, _participants: Set) - : this(participants = _participants.toMutableSet(), - externalId = uid.externalId, - uuid = uid.id) + : this(participants = _participants.toMutableSet(), + externalId = uid.externalId, + uuid = uid.id) } @MappedSuperclass diff --git a/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt b/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt index 8e019d7b5b..a0e6c80f49 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt @@ -31,8 +31,8 @@ object NodeInfoSchemaV1 : MappedSchema( @Column(name = "legal_identities_certs") @ManyToMany(cascade = arrayOf(CascadeType.ALL)) @JoinTable(name = "link_nodeinfo_party", - joinColumns = arrayOf(JoinColumn(name="node_info_id")), - inverseJoinColumns = arrayOf(JoinColumn(name="party_name"))) + joinColumns = arrayOf(JoinColumn(name = "node_info_id")), + inverseJoinColumns = arrayOf(JoinColumn(name = "party_name"))) val legalIdentitiesAndCerts: List, @Column(name = "platform_version") @@ -64,14 +64,15 @@ object NodeInfoSchemaV1 : MappedSchema( @Entity data class DBHostAndPort( - @EmbeddedId - private val pk: PKHostAndPort + @EmbeddedId + private val pk: PKHostAndPort ) { companion object { fun fromHostAndPort(hostAndPort: NetworkHostAndPort) = DBHostAndPort( PKHostAndPort(hostAndPort.host, hostAndPort.port) ) } + fun toHostAndPort(): NetworkHostAndPort { return NetworkHostAndPort(this.pk.host!!, this.pk.port!!) } diff --git a/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt b/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt index a02241aae2..cd55da4982 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt @@ -49,7 +49,8 @@ open class MappedSchema(schemaFamily: Class<*>, * A super class for all mapped states exported to a schema that ensures the [StateRef] appears on the database row. The * [StateRef] will be set to the correct value by the framework (there's no need to set during mapping generation by the state itself). */ -@MappedSuperclass @CordaSerializable open class PersistentState(@EmbeddedId var stateRef: PersistentStateRef? = null) : StatePersistable +@MappedSuperclass +@CordaSerializable open class PersistentState(@EmbeddedId var stateRef: PersistentStateRef? = null) : StatePersistable /** * Embedded [StateRef] representation used in state mapping. 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 131578482e..b0499938ef 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt @@ -115,6 +115,7 @@ interface SerializationContext { * The use case we are serializing or deserializing for. See [UseCase]. */ val useCase: UseCase + /** * Helper method to return a new context based on this context with the property added. */ diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index d62b1c0e6e..1881431114 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -57,7 +57,7 @@ data class LedgerTransaction( } private val contracts: Map> = (inputs.map { it.state.contract } + outputs.map { it.contract }) - .toSet().map { it to createContractFor(it) }.toMap() + .toSet().map { it to createContractFor(it) }.toMap() val inputStates: List get() = inputs.map { it.state.data } @@ -79,8 +79,7 @@ data class LedgerTransaction( // TODO: make contract upgrade transactions have a separate type if (commands.any { it.value is UpgradeCommand }) { verifyContractUpgrade() - } - else { + } else { verifyContracts() } } 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 4cd1332659..8f2c476004 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt @@ -48,7 +48,7 @@ abstract class TraversableTransaction(open val componentGroups: List> get() { val result = mutableListOf(inputs, outputs, commands, attachments) @@ -138,7 +138,7 @@ class FilteredTransaction private constructor( 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.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) @@ -147,7 +147,7 @@ class FilteredTransaction private 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 >= ComponentGroupEnum.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]!!) @@ -156,7 +156,7 @@ class FilteredTransaction private constructor( updateFilteredComponents() val filteredComponentGroups: MutableList = mutableListOf() filteredSerialisedComponents.forEach { (groupIndex, value) -> - filteredComponentGroups.add(FilteredComponentGroup(groupIndex, value, filteredComponentNonces[groupIndex]!!, createPartialMerkleTree(groupIndex) )) + filteredComponentGroups.add(FilteredComponentGroup(groupIndex, value, filteredComponentNonces[groupIndex]!!, createPartialMerkleTree(groupIndex))) } return filteredComponentGroups } @@ -183,7 +183,7 @@ class FilteredTransaction private constructor( // Compute partial Merkle roots for each filtered component and verify each of the partial Merkle trees. filteredComponentGroups.forEach { (groupIndex, components, nonces, groupPartialTree) -> - verificationCheck(groupIndex < groupHashes.size ) { "There is no matching component group hash for group $groupIndex" } + 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" } @@ -226,7 +226,7 @@ class FilteredTransaction private constructor( "Did not receive components for group ${componentGroupEnum.ordinal} and cannot verify they didn't exist in the original wire transaction" } } else { - visibilityCheck(group.groupIndex < groupHashes.size ) { "There is no matching component group hash for group ${group.groupIndex}" } + visibilityCheck(group.groupIndex < groupHashes.size) { "There is no matching component group hash for group ${group.groupIndex}" } val groupPartialRoot = groupHashes[group.groupIndex] val groupFullRoot = MerkleTree.getMerkleTree(group.components.mapIndexed { index, component -> componentHash(group.nonces[index], component) }).hash visibilityCheck(groupPartialRoot == groupFullRoot) { "The partial Merkle tree root does not match with the received root for group ${group.groupIndex}" } @@ -253,7 +253,7 @@ class FilteredTransaction private constructor( * This is similar to [ComponentGroup], but it also includes the corresponding nonce per component. */ @CordaSerializable -data class FilteredComponentGroup(override val groupIndex: Int, override val components: List, val nonces: List, val partialMerkleTree: PartialMerkleTree): ComponentGroup(groupIndex, components) { +data class FilteredComponentGroup(override val groupIndex: Int, override val components: List, val nonces: List, 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/transactions/MissingContractAttachments.kt b/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt index 935ddfdd8d..725e75da0f 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt @@ -12,4 +12,4 @@ import net.corda.core.serialization.CordaSerializable */ @CordaSerializable class MissingContractAttachments(val states: List>) - : FlowException("Cannot find contract attachments for ${states.map { it.contract }.distinct() }") \ No newline at end of file + : FlowException("Cannot find contract attachments for ${states.map { it.contract }.distinct()}") \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt index 4e8602daf6..d583a5f386 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -47,7 +47,8 @@ data class SignedTransaction(val txBits: SerializedBytes, } /** Cache the deserialized form of the transaction. This is useful when building a transaction or collecting signatures. */ - @Volatile @Transient private var cachedTransaction: CoreTransaction? = null + @Volatile + @Transient private var cachedTransaction: CoreTransaction? = null /** Lazily calculated access to the deserialised/hashed transaction data. */ private val transaction: CoreTransaction get() = cachedTransaction ?: txBits.deserialize().apply { cachedTransaction = this } diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index ad6c538451..847bdfc31b 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -38,8 +38,8 @@ open class TransactionBuilder( protected val commands: MutableList> = arrayListOf(), protected var window: TimeWindow? = null, protected var privacySalt: PrivacySalt = PrivacySalt() - ) { - constructor(notary: Party) : this (notary, (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID()) +) { + constructor(notary: Party) : this(notary, (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID()) /** * Creates a copy of the builder. @@ -179,6 +179,7 @@ open class TransactionBuilder( // Accessors that yield immutable snapshots. fun inputStates(): List = ArrayList(inputs) + fun attachments(): List = ArrayList(attachments) fun outputStates(): List> = ArrayList(outputs) fun commands(): List> = ArrayList(commands) diff --git a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt index 3bba0b8ae6..2325344286 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -63,15 +63,16 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr override val id: SecureHash get() = merkleTree.hash /** Public keys that need to be fulfilled by signatures in order for the transaction to be valid. */ - val requiredSigningKeys: Set get() { - val commandKeys = commands.flatMap { it.signers }.toSet() - // TODO: prevent notary field from being set if there are no inputs and no timestamp. - return if (notary != null && (inputs.isNotEmpty() || timeWindow != null)) { - commandKeys + notary.owningKey - } else { - commandKeys + val requiredSigningKeys: Set + get() { + val commandKeys = commands.flatMap { it.signers }.toSet() + // TODO: prevent notary field from being set if there are no inputs and no timestamp. + return if (notary != null && (inputs.isNotEmpty() || timeWindow != null)) { + commandKeys + notary.owningKey + } else { + commandKeys + } } - } /** * Looks up identities and attachments from storage to generate a [LedgerTransaction]. A transaction is expected to @@ -84,7 +85,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr fun toLedgerTransaction(services: ServicesForResolution): LedgerTransaction { return toLedgerTransaction( resolveIdentity = { services.identityService.partyFromKey(it) }, - resolveAttachment = { services.attachments.openAttachment(it)}, + resolveAttachment = { services.attachments.openAttachment(it) }, resolveStateRef = { services.loadState(it) }, resolveContractAttachment = { services.cordappProvider.getContractAttachmentID(it.contract) } ) @@ -123,7 +124,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr * Build filtered transaction using provided filtering functions. */ fun buildFilteredTransaction(filtering: Predicate): FilteredTransaction = - FilteredTransaction.buildFilteredTransaction(this, filtering) + FilteredTransaction.buildFilteredTransaction(this, filtering) /** * Builds whole Merkle tree for a transaction. @@ -236,7 +237,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr ): List { val contractAttachments = (outputs + resolvedInputs.map { it.state }).map { Pair(it, resolveContractAttachment(it)) } val missingAttachments = contractAttachments.filter { it.second == null } - return if(missingAttachments.isEmpty()) { + return if (missingAttachments.isEmpty()) { contractAttachments.map { ContractAttachment(resolveAttachment(it.second!!) ?: throw AttachmentResolutionException(it.second!!), it.first.contract) } } else { throw MissingContractAttachments(missingAttachments.map { it.first }) 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 23eb6d86e5..a2b74a11ff 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt @@ -25,6 +25,7 @@ sealed class ByteSequence : Comparable { * The start position of the sequence within the byte array. */ abstract val offset: Int + /** Returns a [ByteArrayInputStream] of the bytes */ fun open() = ByteArrayInputStream(bytes, offset, size) diff --git a/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt index 1cc4e53807..2c1854c897 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt @@ -66,5 +66,6 @@ fun String.hexToBase64(): String = hexToByteArray().toBase64() // TODO: follow the crypto-conditions ASN.1 spec, some changes are needed to be compatible with the condition // structure, e.g. mapping a PublicKey to a condition with the specific feature (ED25519). fun parsePublicKeyBase58(base58String: String): PublicKey = base58String.base58ToByteArray().deserialize() + fun PublicKey.toBase58String(): String = this.serialize().bytes.toBase58() fun PublicKey.toSHA256Bytes(): ByteArray = this.serialize().bytes.sha256().bytes // TODO: decide on the format of hashed key (encoded Vs serialised). diff --git a/core/src/main/kotlin/net/corda/core/utilities/NetworkHostAndPort.kt b/core/src/main/kotlin/net/corda/core/utilities/NetworkHostAndPort.kt index 8866e79e1b..6153c1c5d2 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/NetworkHostAndPort.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/NetworkHostAndPort.kt @@ -27,7 +27,7 @@ data class NetworkHostAndPort(val host: String, val port: Int) { fun parse(str: String): NetworkHostAndPort { val uri = try { URI(null, str, null, null, null) - } catch(ex: URISyntaxException) { + } catch (ex: URISyntaxException) { throw IllegalArgumentException("Host and port syntax is invalid, expected host:port") } require(uri.host != null) { NetworkHostAndPort.UNPARSEABLE_ADDRESS_FORMAT.format(str) } diff --git a/core/src/main/kotlin/net/corda/core/utilities/NonEmptySet.kt b/core/src/main/kotlin/net/corda/core/utilities/NonEmptySet.kt index 9e364769e2..ab5f0b86d7 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/NonEmptySet.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/NonEmptySet.kt @@ -48,11 +48,13 @@ class NonEmptySet private constructor(private val elements: Set) : Set /** Returns the first element of the set. */ fun head(): T = elements.iterator().next() + override fun isEmpty(): Boolean = false override fun iterator() = object : Iterator by elements.iterator() {} // Following methods are not delegated by Kotlin's Class delegation override fun forEach(action: Consumer) = elements.forEach(action) + override fun stream(): Stream = elements.stream() override fun parallelStream(): Stream = elements.parallelStream() override fun spliterator(): Spliterator = elements.spliterator() diff --git a/core/src/test/kotlin/net/corda/core/contracts/CompatibleTransactionTests.kt b/core/src/test/kotlin/net/corda/core/contracts/CompatibleTransactionTests.kt index 79d06b3ab4..7ed31d3d38 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/CompatibleTransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/CompatibleTransactionTests.kt @@ -45,11 +45,11 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() { // Do not add attachments (empty list). private val componentGroupsA by lazy { listOf( - inputGroup, - outputGroup, - commandGroup, - notaryGroup, - timeWindowGroup + inputGroup, + outputGroup, + commandGroup, + notaryGroup, + timeWindowGroup ) } private val wireTransactionA by lazy { WireTransaction(componentGroups = componentGroupsA, privacySalt = privacySalt) } @@ -199,6 +199,7 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() { else -> false } } + val ftxInputs = wireTransactionA.buildFilteredTransaction(Predicate(::filtering)) // Inputs only filtered. ftxInputs.verify() ftxInputs.checkAllComponentsVisible(INPUTS_GROUP) @@ -210,6 +211,7 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() { // Filter one input only. fun filteringOneInput(elem: Any) = elem == inputs[0] + val ftxOneInput = wireTransactionA.buildFilteredTransaction(Predicate(::filteringOneInput)) // First input only filtered. ftxOneInput.verify() assertFailsWith { ftxOneInput.checkAllComponentsVisible(INPUTS_GROUP) } diff --git a/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt b/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt index 33e060453f..88854550a4 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt @@ -20,7 +20,7 @@ class X509NameConstraintsTest { private fun makeKeyStores(subjectName: X500Name, nameConstraints: NameConstraints): Pair { val rootKeys = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Root CA", organisation = "R3 Ltd", locality= "London", country = "GB"), rootKeys) + val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Root CA", organisation = "R3 Ltd", locality = "London", country = "GB"), rootKeys) val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootKeys, CordaX500Name(commonName = "Corda Intermediate CA", organisation = "R3 Ltd", locality = "London", country = "GB"), intermediateCAKeyPair.public) diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index 197f8ca00c..0720557b1f 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -161,14 +161,14 @@ class ContractUpgradeFlowTest { assertFailsWith(UnexpectedFlowEndException::class) { rejectedFuture.getOrThrow() } // Party B authorise the contract state upgrade, and immediately deauthorise the same. - rpcB.startFlow( { stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade ) }, + rpcB.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade) }, btx!!.tx.outRef(0), DummyContractV2::class.java).returnValue - rpcB.startFlow( { stateRef -> ContractUpgradeFlow.Deauthorise(stateRef) }, + rpcB.startFlow({ stateRef -> ContractUpgradeFlow.Deauthorise(stateRef) }, btx.tx.outRef(0).ref).returnValue // The request is expected to be rejected because party B has subsequently deauthorised and a previously authorised upgrade. - val deauthorisedFuture = rpcA.startFlow( {stateAndRef, upgrade -> ContractUpgradeFlow.Initiate(stateAndRef, upgrade) }, + val deauthorisedFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Initiate(stateAndRef, upgrade) }, atx.tx.outRef(0), DummyContractV2::class.java).returnValue @@ -176,7 +176,7 @@ class ContractUpgradeFlowTest { assertFailsWith(UnexpectedFlowEndException::class) { deauthorisedFuture.getOrThrow() } // Party B authorise the contract state upgrade. - rpcB.startFlow( { stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade ) }, + rpcB.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade) }, btx.tx.outRef(0), DummyContractV2::class.java).returnValue diff --git a/core/src/test/kotlin/net/corda/core/internal/X509EdDSAEngineTest.kt b/core/src/test/kotlin/net/corda/core/internal/X509EdDSAEngineTest.kt index 3f79a6ce36..c4cca75b83 100644 --- a/core/src/test/kotlin/net/corda/core/internal/X509EdDSAEngineTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/X509EdDSAEngineTest.kt @@ -15,11 +15,11 @@ import kotlin.test.assertFailsWith import kotlin.test.assertTrue class TestX509Key(algorithmId: AlgorithmId, key: BitArray) : X509Key() { - init { - this.algid = algorithmId - this.setKey(key) - this.encode() - } + init { + this.algid = algorithmId + this.setKey(key) + this.encode() + } } class X509EdDSAEngineTest { diff --git a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt index db7d27d06a..4b14705669 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt @@ -45,6 +45,7 @@ private fun Attachment.extractContent() = ByteArrayOutputStream().apply { extrac private fun StartedNode<*>.saveAttachment(content: String) = database.transaction { attachments.importAttachment(createAttachmentData(content).inputStream()) } + private fun StartedNode<*>.hackAttachment(attachmentId: SecureHash, content: String) = database.transaction { updateAttachment(attachmentId, createAttachmentData(content)) } From 08c4fe2e9ea0fcf78fdecbc2bc0db167a0a9041a Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Fri, 6 Oct 2017 14:51:19 +0100 Subject: [PATCH 121/185] Reformat files in `buildSrc` --- buildSrc/src/main/groovy/CanonicalizerPlugin.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/groovy/CanonicalizerPlugin.groovy b/buildSrc/src/main/groovy/CanonicalizerPlugin.groovy index 6b6b87b483..85034e7bbd 100644 --- a/buildSrc/src/main/groovy/CanonicalizerPlugin.groovy +++ b/buildSrc/src/main/groovy/CanonicalizerPlugin.groovy @@ -28,7 +28,7 @@ class CanonicalizerPlugin implements Plugin { output.setMethod(ZipOutputStream.DEFLATED) entries.each { - def newEntry = new ZipEntry( it.name ) + def newEntry = new ZipEntry(it.name) newEntry.setLastModifiedTime(zeroTime) newEntry.setCreationTime(zeroTime) From 551dc432659aecfafa96b35193cfafc779ab9edd Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Fri, 6 Oct 2017 14:54:07 +0100 Subject: [PATCH 122/185] Reformat files in `client` --- .../corda/client/jackson/JacksonSupport.kt | 13 +++++--- .../jackson/StringToMethodCallParser.kt | 31 ++++++++++--------- .../client/jackson/JacksonSupportTest.kt | 4 +-- .../net/corda/client/jfx/model/ModelsUtils.kt | 1 + .../client/jfx/model/NodeMonitorModel.kt | 4 +-- .../client/jfx/utils/ConcatenatedList.kt | 15 ++++----- .../corda/client/jfx/utils/ObservableFold.kt | 1 + .../client/jfx/utils/ObservableUtilities.kt | 1 + .../utils/ReadOnlyBackedObservableMapBase.kt | 25 ++++++++------- .../net/corda/client/mock/EventGenerator.kt | 4 +-- .../net/corda/client/mock/Generators.kt | 1 + .../net/corda/client/rpc/RPCStabilityTests.kt | 19 +++++++----- .../rpc/internal/RPCClientProxyHandler.kt | 1 + .../rpc/StandaloneCordaRPCJavaClientTest.java | 2 +- .../kotlin/rpc/StandaloneCordaRPClientTest.kt | 6 ++-- .../net/corda/client/rpc/AbstractRPCTest.kt | 5 ++- .../corda/client/rpc/RPCPerformanceTests.kt | 10 ++++-- .../corda/client/rpc/RPCPermissionsTests.kt | 2 +- .../client/rpc/RepeatingBytesInputStream.kt | 1 + 19 files changed, 85 insertions(+), 61 deletions(-) diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt index 809dbc45a9..65465e04bb 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt @@ -119,12 +119,14 @@ object JacksonSupport { * match an identity known from the network map. If true, the name is matched more leniently but if the match * is ambiguous a [JsonParseException] is thrown. */ - @JvmStatic @JvmOverloads + @JvmStatic + @JvmOverloads fun createDefaultMapper(rpc: CordaRPCOps, factory: JsonFactory = JsonFactory(), fuzzyIdentityMatch: Boolean = false): ObjectMapper = configureMapper(RpcObjectMapper(rpc, factory, fuzzyIdentityMatch)) /** For testing or situations where deserialising parties is not required */ - @JvmStatic @JvmOverloads + @JvmStatic + @JvmOverloads fun createNonRpcMapper(factory: JsonFactory = JsonFactory()): ObjectMapper = configureMapper(NoPartyObjectMapper(factory)) /** @@ -134,7 +136,8 @@ object JacksonSupport { * match an identity known from the network map. If true, the name is matched more leniently but if the match * is ambiguous a [JsonParseException] is thrown. */ - @JvmStatic @JvmOverloads + @JvmStatic + @JvmOverloads fun createInMemoryMapper(identityService: IdentityService, factory: JsonFactory = JsonFactory(), fuzzyIdentityMatch: Boolean = false) = configureMapper(IdentityObjectMapper(identityService, factory, fuzzyIdentityMatch)) @@ -227,7 +230,7 @@ object JacksonSupport { return try { CordaX500Name.parse(parser.text) - } catch(ex: IllegalArgumentException) { + } catch (ex: IllegalArgumentException) { throw JsonParseException(parser, "Invalid Corda X.500 name ${parser.text}: ${ex.message}", ex) } } @@ -310,7 +313,7 @@ object JacksonSupport { // Attempt parsing as a currency token. TODO: This needs thought about how to extend to other token types. val currency = Currency.getInstance(token) return Amount(quantity, currency) - } catch(e2: Exception) { + } catch (e2: Exception) { throw JsonParseException(parser, "Invalid amount ${parser.text}", e2) } } diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/StringToMethodCallParser.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/StringToMethodCallParser.kt index a49409eca2..a79d14f591 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/StringToMethodCallParser.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/StringToMethodCallParser.kt @@ -100,7 +100,7 @@ open class StringToMethodCallParser @JvmOverloads constructor( val methodParamNames: Map> = targetType.declaredMethods.mapNotNull { try { it.name to paramNamesFromMethod(it) - } catch(e: KotlinReflectionInternalError) { + } catch (e: KotlinReflectionInternalError) { // Kotlin reflection doesn't support every method that can exist on an object (in particular, reified // inline methods) so we just ignore those here. null @@ -175,7 +175,7 @@ open class StringToMethodCallParser @JvmOverloads constructor( try { val args = parseArguments(name, paramNamesFromMethod(method).zip(method.parameterTypes), argStr) return ParsedMethodCall(target, method, args) - } catch(e: UnparseableCallException) { + } catch (e: UnparseableCallException) { if (index == methods.size - 1) throw e } @@ -198,7 +198,7 @@ open class StringToMethodCallParser @JvmOverloads constructor( val entry = tree[argName] ?: throw UnparseableCallException.MissingParameter(methodNameHint, argName, args) try { om.readValue(entry.traverse(om), argType) - } catch(e: Exception) { + } catch (e: Exception) { throw UnparseableCallException.FailedParse(e) } } @@ -212,16 +212,17 @@ open class StringToMethodCallParser @JvmOverloads constructor( } /** Returns a string-to-string map of commands to a string describing available parameter types. */ - val availableCommands: Map get() { - return methodMap.entries().map { entry -> - val (name, args) = entry // TODO: Kotlin 1.1 - val argStr = if (args.parameterCount == 0) "" else { - val paramNames = methodParamNames[name]!! - val typeNames = args.parameters.map { it.type.simpleName } - val paramTypes = paramNames.zip(typeNames) - paramTypes.map { "${it.first}: ${it.second}" }.joinToString(", ") - } - Pair(name, argStr) - }.toMap() - } + val availableCommands: Map + get() { + return methodMap.entries().map { entry -> + val (name, args) = entry // TODO: Kotlin 1.1 + val argStr = if (args.parameterCount == 0) "" else { + val paramNames = methodParamNames[name]!! + val typeNames = args.parameters.map { it.type.simpleName } + val paramTypes = paramNames.zip(typeNames) + paramTypes.map { "${it.first}: ${it.second}" }.joinToString(", ") + } + Pair(name, argStr) + }.toMap() + } } 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 6f057b2ff5..d8595e6bd9 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 @@ -92,10 +92,10 @@ class JacksonSupportTest : TestDependencyInjectionBase() { fun writeTransaction() { val attachmentRef = SecureHash.randomSHA256() whenever(cordappProvider.getContractAttachmentID(DummyContract.PROGRAM_ID)) - .thenReturn(attachmentRef) + .thenReturn(attachmentRef) fun makeDummyTx(): SignedTransaction { val wtx = DummyContract.generateInitial(1, DUMMY_NOTARY, MINI_CORP.ref(1)) - .toWireTransaction(services) + .toWireTransaction(services) val signatures = TransactionSignature( ByteArray(1), ALICE_PUBKEY, diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ModelsUtils.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ModelsUtils.kt index 836e046887..868849d56b 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ModelsUtils.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ModelsUtils.kt @@ -1,4 +1,5 @@ @file:JvmName("ModelsUtils") + package net.corda.client.jfx.model import javafx.beans.property.ObjectProperty diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt index 9a4da742b0..0b866332f9 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt @@ -57,8 +57,8 @@ class NodeMonitorModel { */ fun register(nodeHostAndPort: NetworkHostAndPort, username: String, password: String) { val client = CordaRPCClient( - nodeHostAndPort, - CordaRPCClientConfiguration.DEFAULT.copy( + nodeHostAndPort, + CordaRPCClientConfiguration.DEFAULT.copy( connectionMaxRetryInterval = 10.seconds ) ) diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ConcatenatedList.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ConcatenatedList.kt index 9b8a5f45e5..58b602b834 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ConcatenatedList.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ConcatenatedList.kt @@ -253,14 +253,15 @@ class ConcatenatedList(sourceList: ObservableList>) : Trans } } - override val size: Int get() { - recalculateOffsets() - if (nestedIndexOffsets.size > 0) { - return nestedIndexOffsets.last() - } else { - return 0 + override val size: Int + get() { + recalculateOffsets() + if (nestedIndexOffsets.size > 0) { + return nestedIndexOffsets.last() + } else { + return 0 + } } - } override fun getSourceIndex(index: Int): Int { throw UnsupportedOperationException("Source index not supported in concatenation") diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableFold.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableFold.kt index 17c68ac20f..76c75c38de 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableFold.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableFold.kt @@ -1,4 +1,5 @@ @file:JvmName("ObservableFold") + package net.corda.client.jfx.utils import javafx.application.Platform diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableUtilities.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableUtilities.kt index 89366935fa..a863af5ade 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableUtilities.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ObservableUtilities.kt @@ -1,4 +1,5 @@ @file:JvmName("ObservableUtilities") + package net.corda.client.jfx.utils import javafx.application.Platform diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ReadOnlyBackedObservableMapBase.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ReadOnlyBackedObservableMapBase.kt index a751e3ac99..4ee03afc56 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ReadOnlyBackedObservableMapBase.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/ReadOnlyBackedObservableMapBase.kt @@ -50,18 +50,19 @@ open class ReadOnlyBackedObservableMapBase : ObservableMap { override fun isEmpty() = backingMap.isEmpty() - override val entries: MutableSet> get() = backingMap.entries.fold(mutableSetOf()) { set, entry -> - set.add(object : MutableMap.MutableEntry { - override var value: A = entry.value.first - override val key = entry.key - override fun setValue(newValue: A): A { - val old = value - value = newValue - return old - } - }) - set - } + override val entries: MutableSet> + get() = backingMap.entries.fold(mutableSetOf()) { set, entry -> + set.add(object : MutableMap.MutableEntry { + override var value: A = entry.value.first + override val key = entry.key + override fun setValue(newValue: A): A { + val old = value + value = newValue + return old + } + }) + set + } override val keys: MutableSet get() = backingMap.keys override val values: MutableCollection get() = ArrayList(backingMap.values.map { it.first }) diff --git a/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt b/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt index 1955dff5d2..28e938c843 100644 --- a/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt +++ b/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt @@ -50,7 +50,7 @@ open class EventGenerator(val parties: List, val currencies: List, currencies: List, notary: Party): EventGenerator(parties, currencies, notary) { +class ErrorFlowsEventGenerator(parties: List, currencies: List, notary: Party) : EventGenerator(parties, currencies, notary) { enum class IssuerEvents { NORMAL_EXIT, EXIT_ERROR @@ -62,7 +62,7 @@ class ErrorFlowsEventGenerator(parties: List, currencies: List, when (errorType) { IssuerEvents.NORMAL_EXIT -> { println("Normal exit") - if (currencyMap[ccy]!! <= amount) addToMap(ccy, -amount) + if (currencyMap[ccy]!! <= amount) addToMap(ccy, -amount) ExitRequest(Amount(amount, ccy), issueRef) // It may fail at the beginning, but we don't care. } IssuerEvents.EXIT_ERROR -> { diff --git a/client/mock/src/main/kotlin/net/corda/client/mock/Generators.kt b/client/mock/src/main/kotlin/net/corda/client/mock/Generators.kt index dc1315b175..ffd110fbeb 100644 --- a/client/mock/src/main/kotlin/net/corda/client/mock/Generators.kt +++ b/client/mock/src/main/kotlin/net/corda/client/mock/Generators.kt @@ -1,4 +1,5 @@ @file:JvmName("Generators") + package net.corda.client.mock import net.corda.core.contracts.Amount 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 2e83e1f08a..7ccf1913d5 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 @@ -76,10 +76,12 @@ class RPCStabilityTests { rpcDriver { Try.on { startRpcClient(NetworkHostAndPort("localhost", 9999)).get() } val server = startRpcServer(ops = DummyOps) - Try.on { startRpcClient( - server.get().broker.hostAndPort!!, - configuration = RPCClientConfiguration.default.copy(minimumServerProtocolVersion = 1) - ).get() } + Try.on { + startRpcClient( + server.get().broker.hostAndPort!!, + configuration = RPCClientConfiguration.default.copy(minimumServerProtocolVersion = 1) + ).get() + } } } repeat(5) { @@ -173,7 +175,7 @@ class RPCStabilityTests { } } - interface LeakObservableOps: RPCOps { + interface LeakObservableOps : RPCOps { fun leakObservable(): Observable } @@ -249,6 +251,7 @@ class RPCStabilityTests { val trackSubscriberCountObservable = UnicastSubject.create().share(). doOnSubscribe { subscriberCount.incrementAndGet() }. doOnUnsubscribe { subscriberCount.decrementAndGet() } + override fun subscribe(): Observable { return trackSubscriberCountObservable } @@ -261,7 +264,7 @@ class RPCStabilityTests { ).get() val numberOfClients = 4 - val clients = (1 .. numberOfClients).map { + val clients = (1..numberOfClients).map { startRandomRpcClient(server.broker.hostAndPort!!) }.transpose().get() @@ -272,7 +275,7 @@ class RPCStabilityTests { clients[0].destroyForcibly() pollUntilClientNumber(server, numberOfClients - 1) // Kill the rest - (1 .. numberOfClients - 1).forEach { + (1..numberOfClients - 1).forEach { clients[it].destroyForcibly() } pollUntilClientNumber(server, 0) @@ -284,6 +287,7 @@ class RPCStabilityTests { interface SlowConsumerRPCOps : RPCOps { fun streamAtInterval(interval: Duration, size: Int): Observable } + class SlowConsumerRPCOpsImpl : SlowConsumerRPCOps { override val protocolVersion = 0 @@ -292,6 +296,7 @@ class RPCStabilityTests { return Observable.interval(interval.toMillis(), TimeUnit.MILLISECONDS).map { chunk } } } + @Test fun `slow consumers are kicked`() { rpcDriver { 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 a777a0c96a..0a89aecc92 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 @@ -79,6 +79,7 @@ class RPCClientProxyHandler( STARTED, FINISHED } + private val lifeCycle = LifeCycle(State.UNSTARTED) private companion object { diff --git a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java index e9ebce530f..70ae26e1b6 100644 --- a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java +++ b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java @@ -67,7 +67,7 @@ public class StandaloneCordaRPCJavaClientTest { try { connection.close(); } finally { - if(notary != null) { + if (notary != null) { notary.close(); } } diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt index 40ae8c5f16..1d487f762b 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt @@ -114,14 +114,14 @@ class StandaloneCordaRPClientTest { @Test fun `test starting flow`() { rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryNodeIdentity) - .returnValue.getOrThrow(timeout) + .returnValue.getOrThrow(timeout) } @Test fun `test starting tracked flow`() { var trackCount = 0 val handle = rpcProxy.startTrackedFlow( - ::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryNodeIdentity + ::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryNodeIdentity ) val updateLatch = CountDownLatch(1) handle.progress.subscribe { msg -> @@ -156,7 +156,7 @@ class StandaloneCordaRPClientTest { // Now issue some cash rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryNodeIdentity) - .returnValue.getOrThrow(timeout) + .returnValue.getOrThrow(timeout) updateLatch.await() assertEquals(1, updateCount.get()) } diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/AbstractRPCTest.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/AbstractRPCTest.kt index c6b5329d8c..35eaa9dfd8 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/AbstractRPCTest.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/AbstractRPCTest.kt @@ -20,10 +20,13 @@ open class AbstractRPCTest { } companion object { - @JvmStatic @Parameterized.Parameters(name = "Mode = {0}") + @JvmStatic + @Parameterized.Parameters(name = "Mode = {0}") fun defaultModes() = modes(RPCTestMode.InVm, RPCTestMode.Netty) + fun modes(vararg modes: RPCTestMode) = listOf(*modes).map { arrayOf(it) } } + @Parameterized.Parameter lateinit var mode: RPCTestMode 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 b9d64a3cab..2923040034 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 @@ -26,9 +26,11 @@ import java.util.concurrent.TimeUnit @RunWith(Parameterized::class) class RPCPerformanceTests : AbstractRPCTest() { companion object { - @JvmStatic @Parameterized.Parameters(name = "Mode = {0}") + @JvmStatic + @Parameterized.Parameters(name = "Mode = {0}") fun modes() = modes(RPCTestMode.Netty) } + private interface TestOps : RPCOps { fun simpleReply(input: ByteArray, sizeOfReply: Int): ByteArray } @@ -60,7 +62,7 @@ class RPCPerformanceTests : AbstractRPCTest() { val executor = Executors.newFixedThreadPool(4) val N = 10000 val latch = CountDownLatch(N) - for (i in 1 .. N) { + for (i in 1..N) { executor.submit { proxy.ops.simpleReply(ByteArray(1024), 1024) latch.countDown() @@ -155,10 +157,12 @@ class RPCPerformanceTests : AbstractRPCTest() { data class BigMessagesResult( val Mbps: Double ) + @Test fun `big messages`() { warmup() - measure(listOf(1)) { clientParallelism -> // TODO this hangs with more parallelism + measure(listOf(1)) { clientParallelism -> + // TODO this hangs with more parallelism rpcDriver { val proxy = testProxy( RPCClientConfiguration.default, 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 4411bbfd07..cee75881aa 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 @@ -79,7 +79,7 @@ class RPCPermissionsTests : AbstractRPCTest() { } @Test - fun `check ALL is implemented the correct way round` () { + fun `check ALL is implemented the correct way round`() { rpcDriver { val joeUser = userOf("joe", setOf(DUMMY_FLOW)) val proxy = testProxyFor(joeUser) diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RepeatingBytesInputStream.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RepeatingBytesInputStream.kt index 8887aa2476..b22ed8b506 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RepeatingBytesInputStream.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RepeatingBytesInputStream.kt @@ -13,6 +13,7 @@ class RepeatingBytesInputStream(val bytesToRepeat: ByteArray, val numberOfBytes: return bytesToRepeat[(numberOfBytes - bytesLeft) % bytesToRepeat.size].toInt() } } + override fun read(byteArray: ByteArray, offset: Int, length: Int): Int { val lastIdx = Math.min(Math.min(offset + length, byteArray.size), offset + bytesLeft) for (i in offset until lastIdx) { From b1fb321230f67383ab3eadf4ce8f54353e8ce218 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Fri, 6 Oct 2017 14:56:44 +0100 Subject: [PATCH 123/185] Reformat files in `experimental` --- .../quasarhook/QuasarInstrumentationHook.kt | 9 ++--- .../net/corda/sandbox/CandidacyStatus.java | 20 +++++------ .../net/corda/sandbox/CandidateMethod.java | 20 +++++------ .../sandbox/SandboxAwareClassWriter.java | 16 ++++----- .../net/corda/sandbox/SandboxRemapper.java | 1 - .../main/java/net/corda/sandbox/Utils.java | 34 +++++++++--------- .../corda/sandbox/WhitelistClassLoader.java | 36 ++++++++++--------- .../WhitelistClassloadingException.java | 8 ++--- .../net/corda/sandbox/costing/Contract.java | 2 +- .../sandbox/costing/ContractExecutor.java | 6 ++-- .../sandbox/costing/RuntimeCostAccounter.java | 1 - .../corda/sandbox/tools/SandboxCreator.java | 15 ++++---- .../CostInstrumentingMethodVisitor.java | 14 ++++---- .../DefinitelyDisallowedMethodVisitor.java | 2 +- .../sandbox/visitors/SandboxPathVisitor.java | 8 +++-- .../WhitelistCheckingClassVisitor.java | 15 ++++---- .../WhitelistCheckingMethodVisitor.java | 19 +++++----- .../sandbox/costing/RuntimeCostAccounter.java | 2 +- .../java/net/corda/sandbox/TestUtils.java | 18 +++++----- .../sandbox/WhitelistClassLoaderTest.java | 7 ++-- .../DeterministicClassInstrumenterTest.java | 3 +- .../costing/SandboxedRewritingTest.java | 7 +++- .../sandbox/greymalkin/StringReturner.java | 1 - 23 files changed, 134 insertions(+), 130 deletions(-) diff --git a/experimental/quasar-hook/src/main/kotlin/net/corda/quasarhook/QuasarInstrumentationHook.kt b/experimental/quasar-hook/src/main/kotlin/net/corda/quasarhook/QuasarInstrumentationHook.kt index c32328ba4a..acd586fa31 100644 --- a/experimental/quasar-hook/src/main/kotlin/net/corda/quasarhook/QuasarInstrumentationHook.kt +++ b/experimental/quasar-hook/src/main/kotlin/net/corda/quasarhook/QuasarInstrumentationHook.kt @@ -32,7 +32,7 @@ fun recordUsedInstrumentedCallStack() { val throwable = Throwable() var index = 0 while (true) { - require (index < throwable.stackTrace.size) { "Can't find getStack call" } + require(index < throwable.stackTrace.size) { "Can't find getStack call" } val stackElement = throwable.stackTrace[index] if (stackElement.className == "co.paralleluniverse.fibers.Stack" && stackElement.methodName == "getStack") { break @@ -129,7 +129,7 @@ class QuasarInstrumentationHookAgent { // The separator append is a hack, it causes a package with an empty name to be added to the exclude tree, // which practically causes that level of the tree to be always expanded in the output globs. val expand = arguments.expand?.let { PackageTree.fromStrings(it.map { "$it${arguments.separator}" }, arguments.separator) } - val truncatedTree = truncate?.let { scannedTree.truncate(it)} ?: scannedTree + val truncatedTree = truncate?.let { scannedTree.truncate(it) } ?: scannedTree val expandedTree = expand?.let { alwaysExcludedTree.merge(it) } ?: alwaysExcludedTree val globs = truncatedTree.toGlobs(expandedTree) globs.forEach { @@ -152,7 +152,7 @@ object QuasarInstrumentationHook : ClassFileTransformer { val instrumentMap = mapOf Unit>( "co/paralleluniverse/fibers/Stack" to { clazz -> // This is called on each suspend, we hook into it to get the stack trace of actually used Suspendables - val getStackMethod = clazz.methods.single { it.name == "getStack" } + val getStackMethod = clazz.methods.single { it.name == "getStack" } getStackMethod.insertBefore( "$hookClassName.${::recordUsedInstrumentedCallStack.name}();" ) @@ -194,7 +194,7 @@ object QuasarInstrumentationHook : ClassFileTransformer { throwable.printStackTrace(System.out) classfileBuffer } - } + } } data class Glob(val parts: List, val isFull: Boolean) { @@ -271,6 +271,7 @@ data class PackageTree(val branches: Map) { val exclude: PackageTree, val globSoFar: List ) + val toExpandList = LinkedList(listOf(State(this, excludeTree, emptyList()))) val globs = ArrayList() while (true) { diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/CandidacyStatus.java b/experimental/sandbox/src/main/java/net/corda/sandbox/CandidacyStatus.java index 7bcaf86684..f0fd4bf668 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/CandidacyStatus.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/CandidacyStatus.java @@ -43,7 +43,6 @@ public class CandidacyStatus { } /** - * * @param signature * @return true if the input was absent from the underlying map */ @@ -52,7 +51,6 @@ public class CandidacyStatus { } /** - * * @param methodSignature * @return true if the input was absent from the underlying map */ @@ -62,7 +60,7 @@ public class CandidacyStatus { /** * Static factory method - * + * * @param startingSet * @return a candidacy status based on the starting set */ @@ -81,7 +79,7 @@ public class CandidacyStatus { /** * Static factory method - * + * * @return a candidacy status based on the starting set */ public static CandidacyStatus of() { @@ -90,8 +88,8 @@ public class CandidacyStatus { /** * Add additional methods that are known to be deterministic - * - * @param methodNames + * + * @param methodNames */ public void addKnownDeterministicMethods(final Set methodNames) { for (String known : methodNames) { @@ -101,7 +99,7 @@ public class CandidacyStatus { /** * Getter method for candidate methods - * + * * @param methodSignature * @return the candidate method corresponding to a method signature */ @@ -149,10 +147,10 @@ public class CandidacyStatus { } /** - * Increases the recursive depth of this classloading process, throwing a + * Increases the recursive depth of this classloading process, throwing a * ClassNotFoundException if it becomes too high - * - * @throws ClassNotFoundException + * + * @throws ClassNotFoundException */ public void incRecursiveCount() throws ClassNotFoundException { if (recursiveDepth >= MAX_CLASSLOADING_RECURSIVE_DEPTH - 1) { @@ -174,7 +172,7 @@ public class CandidacyStatus { out.add(candidateName); } } - + return out; } diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/CandidateMethod.java b/experimental/sandbox/src/main/java/net/corda/sandbox/CandidateMethod.java index 49e5091fbe..b806d58b65 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/CandidateMethod.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/CandidateMethod.java @@ -6,15 +6,14 @@ import java.util.Set; /** * A candidate method that is under evaluation. Candidate methods have one of the following states: - * + *

*

    - *
  • {@link CandidateMethod.State#DETERMINISTIC} - It's deterministic and therefore is allowed to be loaded.
  • - *
  • {@link CandidateMethod.State#DISALLOWED} - It's not deterministic and won't be allowed to be loaded.
  • - *
  • {@link CandidateMethod.State#SCANNED} - We're not sure if it's deterministic or not.
  • + *
  • {@link CandidateMethod.State#DETERMINISTIC} - It's deterministic and therefore is allowed to be loaded.
  • + *
  • {@link CandidateMethod.State#DISALLOWED} - It's not deterministic and won't be allowed to be loaded.
  • + *
  • {@link CandidateMethod.State#SCANNED} - We're not sure if it's deterministic or not.
  • *
- * + *

* CandidateMethods themselves reference other CandidateMethods which are be checked for their deterministic state - * */ public final class CandidateMethod { @@ -43,7 +42,7 @@ public final class CandidateMethod { private final Set referencedCandidateMethods = new HashSet<>(); - + public State getCurrentState() { return currentState; } @@ -59,7 +58,7 @@ public final class CandidateMethod { public void deterministic() { if (currentState == State.DISALLOWED) { - throw new IllegalArgumentException("Method "+ internalMethodName +" attempted to transition from DISALLOWED to DETERMINISTIC"); + throw new IllegalArgumentException("Method " + internalMethodName + " attempted to transition from DISALLOWED to DETERMINISTIC"); } currentState = State.DETERMINISTIC; } @@ -79,7 +78,7 @@ public final class CandidateMethod { public String getInternalMethodName() { return internalMethodName; } - + public void addReferencedCandidateMethod(final CandidateMethod referenceCandidateMethod) { referencedCandidateMethods.add(referenceCandidateMethod); } @@ -94,8 +93,9 @@ public final class CandidateMethod { /** * This factory constructor is only called for methods that are known to be deterministic in advance + * * @param methodSignature - * @return + * @return */ public static CandidateMethod proven(String methodSignature) { final CandidateMethod provenCandidateMethod = new CandidateMethod(methodSignature); diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/SandboxAwareClassWriter.java b/experimental/sandbox/src/main/java/net/corda/sandbox/SandboxAwareClassWriter.java index 03e3cf9d56..f5e742b95c 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/SandboxAwareClassWriter.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/SandboxAwareClassWriter.java @@ -1,11 +1,11 @@ package net.corda.sandbox; import static net.corda.sandbox.Utils.*; + import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; /** - * * @author ben */ public final class SandboxAwareClassWriter extends ClassWriter { @@ -25,17 +25,15 @@ public final class SandboxAwareClassWriter extends ClassWriter { * without actually loading any class, or to take into account the class * that is currently being generated by this ClassWriter, which can of * course not be loaded since it is under construction. - * - * @param type1 - * the internal name of a class. - * @param type2 - * the internal name of another class. + * + * @param type1 the internal name of a class. + * @param type2 the internal name of another class. * @return the internal name of the common super class of the two given - * classes. + * classes. */ @Override public String getCommonSuperClass(final String type1, final String type2) { - if (OBJECT.equals(type1) || OBJECT.equals(type2) + if (OBJECT.equals(type1) || OBJECT.equals(type2) || OBJECT.equals(unsandboxNameIfNeedBe(type1)) || OBJECT.equals(unsandboxNameIfNeedBe(type2))) { return OBJECT; } @@ -58,7 +56,7 @@ public final class SandboxAwareClassWriter extends ClassWriter { c = Class.forName(type1.replace('/', '.'), false, loader); d = Class.forName(type2.replace('/', '.'), false, loader); } catch (Exception e) { - + c = Class.forName(unsandboxNameIfNeedBe(type1).replace('/', '.'), false, loader); d = Class.forName(unsandboxNameIfNeedBe(type2).replace('/', '.'), false, loader); diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/SandboxRemapper.java b/experimental/sandbox/src/main/java/net/corda/sandbox/SandboxRemapper.java index 2f55c4edb6..32157b6ff5 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/SandboxRemapper.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/SandboxRemapper.java @@ -3,7 +3,6 @@ package net.corda.sandbox; import org.objectweb.asm.commons.Remapper; /** - * * @author ben */ public final class SandboxRemapper extends Remapper { diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/Utils.java b/experimental/sandbox/src/main/java/net/corda/sandbox/Utils.java index 85fa4fc078..08cbbd794b 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/Utils.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/Utils.java @@ -4,7 +4,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; /** - * * @author ben */ public final class Utils { @@ -12,7 +11,7 @@ public final class Utils { public final static String SANDBOX_PREFIX_INTERNAL = "sandbox/"; public final static String CLASSFILE_NAME_SUFFIX = "^(.*)\\.class$"; - + public static final Pattern JAVA_LANG_PATTERN_INTERNAL = Pattern.compile("^java/lang/(.*)"); public static final Pattern SANDBOX_PATTERN_INTERNAL = Pattern.compile("^" + SANDBOX_PREFIX_INTERNAL + "(.*)"); @@ -28,13 +27,13 @@ public final class Utils { public static final Pattern CLASSNAME_PATTERN_QUALIFIED = Pattern.compile("([^\\.]+)\\."); public static final String OBJECT = "java/lang/Object"; - + public static final String THROWABLE = "java/lang/Throwable"; - + public static final String ERROR = "java/lang/Error"; - + public static final String THREAD_DEATH = "java/lang/ThreadDeath"; - + // Hide constructor private Utils() { } @@ -43,6 +42,7 @@ public final class Utils { * Helper method that converts from the internal class name format (as used in the * Constant Pool) to a fully-qualified class name. No obvious library method to do this * appears to exist, hence this code. If one exists, rip this out. + * * @param classInternalName * @return */ @@ -52,12 +52,11 @@ public final class Utils { } /** - * This method takes in an internal method name but needs to return a qualified + * This method takes in an internal method name but needs to return a qualified * classname (suitable for loading) - * - * + * * @param internalMethodName - * @return + * @return */ public static String convertInternalMethodNameToQualifiedClassName(final String internalMethodName) { final Matcher classMatch = CLASSNAME_PATTERN_QUALIFIED.matcher(internalMethodName); @@ -72,6 +71,7 @@ public final class Utils { * Helper method that converts from a fully-qualified class name to the internal class * name format (as used in the Constant Pool). No obvious library method to do this * appears to exist, hence this code. If one exists, rip this out. + * * @param qualifiedClassName * @return */ @@ -81,7 +81,7 @@ public final class Utils { } /** - * This method potentially rewrites the classname. + * This method potentially rewrites the classname. * * @param internalClassname - specified in internal form * @return @@ -102,9 +102,8 @@ public final class Utils { } /** - * * @param qualifiedTypeName - * @return + * @return */ public static String sandboxQualifiedTypeName(final String qualifiedTypeName) { final String internal = convertQualifiedClassNameToInternalForm(qualifiedTypeName); @@ -118,7 +117,7 @@ public final class Utils { /** * This method removes the sandboxing prefix from a method or type name, if it has * one, otherwise it returns the input string. - * + * * @param internalClassname * @return the internal classname, unsandboxed if that was required */ @@ -131,7 +130,6 @@ public final class Utils { } /** - * * @param desc - internal * @return the rewritten desc string */ @@ -169,9 +167,9 @@ public final class Utils { * loading. This should not attempt to load a classname that starts with java. as * the only permissable classes have already been transformed into sandboxed * methods - * + * * @param qualifiedClassName - * @return + * @return */ public static boolean shouldAttemptToTransitivelyLoad(final String qualifiedClassName) { return !JAVA_PATTERN_QUALIFIED.asPredicate().test(qualifiedClassName); @@ -179,7 +177,7 @@ public final class Utils { /** * Helper method that determines whether this class requires sandboxing - * + * * @param clazzName - specified in internal form * @return true if the class should be sandboxed */ diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/WhitelistClassLoader.java b/experimental/sandbox/src/main/java/net/corda/sandbox/WhitelistClassLoader.java index 171c2770f9..2a99ffbdca 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/WhitelistClassLoader.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/WhitelistClassLoader.java @@ -2,6 +2,7 @@ package net.corda.sandbox; import net.corda.sandbox.visitors.CostInstrumentingMethodVisitor; import net.corda.sandbox.visitors.WhitelistCheckingClassVisitor; + import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -9,13 +10,13 @@ import java.net.URISyntaxException; import java.net.URL; import java.nio.file.*; import java.util.*; + import org.objectweb.asm.*; import org.objectweb.asm.commons.ClassRemapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * * @author ben */ public final class WhitelistClassLoader extends ClassLoader { @@ -61,8 +62,8 @@ public final class WhitelistClassLoader extends ClassLoader { } /** - * Static factory method. Throws URISyntaxException currently, as this method is - * called with user data, so a checked exception is not unreasonable. Could use a + * Static factory method. Throws URISyntaxException currently, as this method is + * called with user data, so a checked exception is not unreasonable. Could use a * runtime exception instead. * * @param auxiliaryClassPath @@ -70,7 +71,7 @@ public final class WhitelistClassLoader extends ClassLoader { * methods to be deterministic, instead the classloader * will remove all non-deterministic methods. * @return a suitably constructed whitelisting classloader - * @throws URISyntaxException + * @throws URISyntaxException */ public static WhitelistClassLoader of(final String auxiliaryClassPath, final boolean stripNonDeterministic) throws URISyntaxException { final WhitelistClassLoader out = new WhitelistClassLoader(stripNonDeterministic); @@ -96,10 +97,10 @@ public final class WhitelistClassLoader extends ClassLoader { /** * Static factory method. Used for recursive classloading - * + * * @param other * @return a suitably constructed whitelisting classloader based on the state - * of the passed classloader + * of the passed classloader */ public static WhitelistClassLoader of(final WhitelistClassLoader other) { final WhitelistClassLoader out = new WhitelistClassLoader(other); @@ -110,7 +111,7 @@ public final class WhitelistClassLoader extends ClassLoader { /** * Helper method that adds a jar to the path to be searched * - * @param knownGoodJar + * @param knownGoodJar */ void addJarToSandbox(final Path knownGoodJar) { fileSystemSearchPath.add(knownGoodJar); @@ -120,9 +121,9 @@ public final class WhitelistClassLoader extends ClassLoader { * Setup the auxiliary classpath so that classes that are not on the original * classpath can be scanned for. * Note that this this method hardcodes Unix conventions, so won't work on e.g. Windows - * + * * @param auxiliaryClassPath - * @throws URISyntaxException + * @throws URISyntaxException */ void setupClasspath(final String auxiliaryClassPath) throws URISyntaxException { for (String entry : auxiliaryClassPath.split(":")) { @@ -136,11 +137,10 @@ public final class WhitelistClassLoader extends ClassLoader { } /** - * * @param qualifiedClassName * @return a class object that has been whitelist checked and is known to be - * deterministic - * @throws ClassNotFoundException + * deterministic + * @throws ClassNotFoundException */ @Override public Class findClass(final String qualifiedClassName) throws ClassNotFoundException { @@ -244,10 +244,10 @@ public final class WhitelistClassLoader extends ClassLoader { * around a limitation of the ASM library that does not integrate cleanly with Java 7 * NIO.2 Path APIs. This method also performs a couple of basic sanity check on the * class file (e.g. that it exists, is a regular file and is readable). - * + * * @param internalClassName * @return a path object that corresponds to a class that has been found - * @throws ClassNotFoundException + * @throws ClassNotFoundException */ Path locateClassfileDir(final String internalClassName) throws ClassNotFoundException { // Check the primaryClasspathSearchPath @@ -300,7 +300,7 @@ public final class WhitelistClassLoader extends ClassLoader { /** * Creates a jar archive of all the transformed classes that this classloader * has loaded. - * + * * @return true on success, false on failure * @throws java.io.IOException * @throws java.net.URISyntaxException @@ -328,7 +328,8 @@ public final class WhitelistClassLoader extends ClassLoader { /** * Getter method for the reason for failure - * @return + * + * @return */ public WhitelistClassloadingException reason() { return candidacyStatus.getReason(); @@ -336,7 +337,8 @@ public final class WhitelistClassLoader extends ClassLoader { /** * Getter method for the method candidacy status - * @return + * + * @return */ public CandidacyStatus getCandidacyStatus() { return candidacyStatus; diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/WhitelistClassloadingException.java b/experimental/sandbox/src/main/java/net/corda/sandbox/WhitelistClassloadingException.java index 5f6c13ca9f..9705d4bf0e 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/WhitelistClassloadingException.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/WhitelistClassloadingException.java @@ -4,7 +4,7 @@ package net.corda.sandbox; * */ public class WhitelistClassloadingException extends Exception { - + public WhitelistClassloadingException() { super(); } @@ -22,10 +22,10 @@ public class WhitelistClassloadingException extends Exception { } protected WhitelistClassloadingException(String message, Throwable cause, - boolean enableSuppression, - boolean writableStackTrace) { + boolean enableSuppression, + boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } - + } diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/costing/Contract.java b/experimental/sandbox/src/main/java/net/corda/sandbox/costing/Contract.java index ec098ecfbd..f20aaa0b79 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/costing/Contract.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/costing/Contract.java @@ -5,7 +5,7 @@ import org.slf4j.LoggerFactory; /** * This class is the runtime representation of a running contract. - * + * * @author ben */ public class Contract { diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/costing/ContractExecutor.java b/experimental/sandbox/src/main/java/net/corda/sandbox/costing/ContractExecutor.java index 243a3ddf11..5b719978a4 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/costing/ContractExecutor.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/costing/ContractExecutor.java @@ -3,17 +3,17 @@ package net.corda.sandbox.costing; /** * This interface is to decouple the actual executable code from the entry point and * how vetted deterministic code will be used inside the sandbox - * + * * @author ben */ public interface ContractExecutor { /** * Executes a smart contract - * + * * @param contract the contract to be executed */ void execute(Contract contract); - + /** * Checks to see if the supplied Contract is suitable * diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/costing/RuntimeCostAccounter.java b/experimental/sandbox/src/main/java/net/corda/sandbox/costing/RuntimeCostAccounter.java index f00c39defa..7274d914c7 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/costing/RuntimeCostAccounter.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/costing/RuntimeCostAccounter.java @@ -4,7 +4,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * * @author ben */ public class RuntimeCostAccounter { diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/tools/SandboxCreator.java b/experimental/sandbox/src/main/java/net/corda/sandbox/tools/SandboxCreator.java index 01b0906dc2..33ab308a2e 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/tools/SandboxCreator.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/tools/SandboxCreator.java @@ -2,6 +2,7 @@ package net.corda.sandbox.tools; import net.corda.sandbox.WhitelistClassLoader; import net.corda.sandbox.visitors.SandboxPathVisitor; + import java.io.FileInputStream; import java.io.IOException; import java.net.URISyntaxException; @@ -18,7 +19,6 @@ import joptsimple.OptionSet; * This class takes in an exploded set of JRE classes, and a whitelist, and rewrites all * classes (note: not methods) that have at least one whitelisted method to create a * sandboxed version of the class. - * */ // java8.scan.java.lang_and_util java8.interfaces_for_compat java8 sandbox public final class SandboxCreator { @@ -30,7 +30,7 @@ public final class SandboxCreator { private final String outputJarName; private final WhitelistClassLoader wlcl; private final boolean hasInputJar; - + private final static OptionParser parser = new OptionParser(); private static void usage() { @@ -53,7 +53,7 @@ public final class SandboxCreator { static String unpackJar(final String zipFilePath) throws IOException { final Path tmpDir = Files.createTempDirectory(Paths.get("/tmp"), "wlcl-extract"); - + try (final ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath))) { ZipEntry entry = zipIn.getNextEntry(); @@ -68,13 +68,13 @@ public final class SandboxCreator { entry = zipIn.getNextEntry(); } } - + return tmpDir.toString(); } - + void cleanup() { if (hasInputJar) { - + } } @@ -107,10 +107,9 @@ public final class SandboxCreator { } /** - * * @param basePath * @param packageName - * @throws IOException + * @throws IOException */ void walk() throws IOException { final Path scanDir = Paths.get(basePathName); diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/CostInstrumentingMethodVisitor.java b/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/CostInstrumentingMethodVisitor.java index 5f7dc3fdea..8b108fed89 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/CostInstrumentingMethodVisitor.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/CostInstrumentingMethodVisitor.java @@ -9,7 +9,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * * @author ben */ public final class CostInstrumentingMethodVisitor extends GeneratorAdapter { @@ -34,9 +33,10 @@ public final class CostInstrumentingMethodVisitor extends GeneratorAdapter { } /** - * This method replaces MONITORENTER / MONITOREXIT opcodes with POP - basically + * This method replaces MONITORENTER / MONITOREXIT opcodes with POP - basically * stripping the synchronization out of any sandboxed code. - * @param opcode + * + * @param opcode */ @Override public void visitInsn(final int opcode) { @@ -60,7 +60,7 @@ public final class CostInstrumentingMethodVisitor extends GeneratorAdapter { * For our purposes this is a NEWARRAY opcode. * * @param opcode - * @param operand + * @param operand */ @Override public void visitIntInsn(final int opcode, final int operand) { @@ -103,11 +103,11 @@ public final class CostInstrumentingMethodVisitor extends GeneratorAdapter { /** * This method is called when visiting an opcode with a single operand, that * is a type (represented here as a String). - * + *

* For our purposes this is either a NEW opcode or a ANEWARRAY * - * @param opcode - * @param type + * @param opcode + * @param type */ @Override public void visitTypeInsn(final int opcode, final String type) { diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/DefinitelyDisallowedMethodVisitor.java b/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/DefinitelyDisallowedMethodVisitor.java index 62b882f269..d4ade3415a 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/DefinitelyDisallowedMethodVisitor.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/DefinitelyDisallowedMethodVisitor.java @@ -11,5 +11,5 @@ class DefinitelyDisallowedMethodVisitor extends MethodVisitor { DefinitelyDisallowedMethodVisitor(MethodVisitor baseMethodVisitor) { super(Opcodes.ASM5, baseMethodVisitor); } - + } diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/SandboxPathVisitor.java b/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/SandboxPathVisitor.java index bdc81bf995..e5528acbd9 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/SandboxPathVisitor.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/SandboxPathVisitor.java @@ -2,15 +2,17 @@ package net.corda.sandbox.visitors; import net.corda.sandbox.Utils; import net.corda.sandbox.WhitelistClassLoader; + import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This helper class visits each file (represented as a Path) in some directory * tree containing classes to be sandboxed. - * + * * @author ben */ public final class SandboxPathVisitor extends SimpleFileVisitor { @@ -30,10 +32,10 @@ public final class SandboxPathVisitor extends SimpleFileVisitor { public FileVisitResult visitFile(final Path path, final BasicFileAttributes attr) { // Check that this is a class file if (!path.toString().matches(Utils.CLASSFILE_NAME_SUFFIX)) { - System.out.println("Skipping: "+ path); + System.out.println("Skipping: " + path); return FileVisitResult.CONTINUE; } - + // Check to see if this path corresponds to an allowedClass final String classFileName = startFrom.relativize(path).toString().replace(".class", ""); diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/WhitelistCheckingClassVisitor.java b/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/WhitelistCheckingClassVisitor.java index 5994b445a6..534c89bc6b 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/WhitelistCheckingClassVisitor.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/WhitelistCheckingClassVisitor.java @@ -2,10 +2,12 @@ package net.corda.sandbox.visitors; import net.corda.sandbox.WhitelistClassLoader; import net.corda.sandbox.CandidacyStatus; + import java.util.Arrays; import net.corda.sandbox.CandidateMethod; import net.corda.sandbox.Utils; + import java.util.HashSet; import java.util.Set; @@ -14,6 +16,7 @@ import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import static org.objectweb.asm.Opcodes.*; /** @@ -57,7 +60,7 @@ public final class WhitelistCheckingClassVisitor extends ClassVisitor { /** * We initially take the method passed in and store an internal representation of * the method signature in the our CandidacyStatus working set. - * + *

* We then get an ASM MethodVisitor (which can read the byte code of the method) and pass that to our * custom method visitor which perform additional checks. * @@ -66,7 +69,7 @@ public final class WhitelistCheckingClassVisitor extends ClassVisitor { * @param desc * @param signature * @param exceptions - * @return + * @return */ @Override public MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) { @@ -77,7 +80,7 @@ public final class WhitelistCheckingClassVisitor extends ClassVisitor { // Force new access control flags - for now just strictfp for deterministic // compliance to IEEE 754 final int maskedAccess = access | ACC_STRICT; - + final String internalName = classname + "." + name + ":" + desc; internalMethodNames.add(internalName); candidacyStatus.putIfAbsent(internalName); @@ -151,10 +154,10 @@ public final class WhitelistCheckingClassVisitor extends ClassVisitor { } /** - * Take the name of a class and attempts to load it using a WLCL. - * + * Take the name of a class and attempts to load it using a WLCL. + * * @param qualifiedClassname - * @return + * @return */ CandidateMethod.State resolveState(final String qualifiedClassname) { Class clz = null; diff --git a/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/WhitelistCheckingMethodVisitor.java b/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/WhitelistCheckingMethodVisitor.java index 28338f5285..0d392f342b 100644 --- a/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/WhitelistCheckingMethodVisitor.java +++ b/experimental/sandbox/src/main/java/net/corda/sandbox/visitors/WhitelistCheckingMethodVisitor.java @@ -13,7 +13,6 @@ import org.objectweb.asm.Label; /** * A MethodVisitor which checks method instructions in order to determine if this * method is deterministic or not - * */ final class WhitelistCheckingMethodVisitor extends MethodVisitor { @@ -29,7 +28,7 @@ final class WhitelistCheckingMethodVisitor extends MethodVisitor { } /** - * Visits a method instruction. A method instruction is an instruction that + * Visits a method instruction. A method instruction is an instruction that * invokes a method. *

* Some method instructions are by their nature un-deterministic, so we set those methods to have a @@ -84,15 +83,15 @@ final class WhitelistCheckingMethodVisitor extends MethodVisitor { } /** - * Currently a no-op. - * + * Currently a no-op. + *

* The JVMspec seems to permit the possibility of using a backwards branch in a - * tableswitch to try to create an infinite loop. However, it seems to be + * tableswitch to try to create an infinite loop. However, it seems to be * impossible in practice - the specification of StackMapFrame seems to prevent * it in modern classfile formats, and even by explicitly generating a version * 49 (Java 5) classfile, the verifier seems to be specifically resistant to a - * backwards branch from a tableswitch. - * + * backwards branch from a tableswitch. + *

* We could still add a belt-and-braces static instrumentation to protect * against this but it currently seems unnecessary - at worse it is a branch that * should count against the branch limit, or an explicit disallow of a backwards @@ -102,7 +101,7 @@ final class WhitelistCheckingMethodVisitor extends MethodVisitor { * @param min * @param max * @param dflt - * @param labels + * @param labels */ @Override public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) { @@ -112,11 +111,11 @@ final class WhitelistCheckingMethodVisitor extends MethodVisitor { /** * Visits an invokedynamic instruction - which is specifically disallowed for * deterministic apps. - * + * * @param name * @param desc * @param bsm - * @param bsmArgs + * @param bsmArgs */ @Override public void visitInvokeDynamicInsn(final String name, final String desc, final Handle bsm, final Object... bsmArgs) { diff --git a/experimental/sandbox/src/main/java/sandbox/net/corda/sandbox/costing/RuntimeCostAccounter.java b/experimental/sandbox/src/main/java/sandbox/net/corda/sandbox/costing/RuntimeCostAccounter.java index 7acc1f445d..22e895c41b 100644 --- a/experimental/sandbox/src/main/java/sandbox/net/corda/sandbox/costing/RuntimeCostAccounter.java +++ b/experimental/sandbox/src/main/java/sandbox/net/corda/sandbox/costing/RuntimeCostAccounter.java @@ -4,7 +4,7 @@ package sandbox.net.corda.sandbox.costing; * A helper class that just forwards any static sandboxed calls to the real runtime * cost accounting class. This removes the need to special case the accounting * method calls during rewriting of method names - * + * * @author ben */ public class RuntimeCostAccounter { diff --git a/experimental/sandbox/src/test/java/net/corda/sandbox/TestUtils.java b/experimental/sandbox/src/test/java/net/corda/sandbox/TestUtils.java index d8e0fd0007..16cbda6e84 100644 --- a/experimental/sandbox/src/test/java/net/corda/sandbox/TestUtils.java +++ b/experimental/sandbox/src/test/java/net/corda/sandbox/TestUtils.java @@ -22,7 +22,7 @@ public class TestUtils { // Copy resource jar to tmp dir tmpdir = Files.createTempDirectory("wlcl-tmp-test"); Path copiedJar = tmpdir.resolve("tmp-resource.jar"); - try(final InputStream in = TestUtils.class.getResourceAsStream(resourcePathToJar)) { + try (final InputStream in = TestUtils.class.getResourceAsStream(resourcePathToJar)) { Files.copy(in, copiedJar, StandardCopyOption.REPLACE_EXISTING); } final FileSystem fs = FileSystems.newFileSystem(copiedJar, null); @@ -33,20 +33,20 @@ public class TestUtils { public static Path copySandboxJarToTmpDir(final String resourcePathToJar) throws IOException { Path sandboxJar = tmpdir.resolve("tmp-sandbox.jar"); - try(final InputStream in = TestUtils.class.getResourceAsStream(resourcePathToJar)) { + try (final InputStream in = TestUtils.class.getResourceAsStream(resourcePathToJar)) { Files.copy(in, sandboxJar, StandardCopyOption.REPLACE_EXISTING); } final FileSystem sandboxFs = FileSystems.newFileSystem(sandboxJar, null); tmpFileSystems.add(sandboxFs); return sandboxFs.getRootDirectories().iterator().next(); } - + public static Path getJarFSRoot() { return jarFSDir; } public static void cleanupTmpJar() throws IOException { - for (FileSystem fs: tmpFileSystems) { + for (FileSystem fs : tmpFileSystems) { fs.close(); } tmpFileSystems.clear(); @@ -92,15 +92,15 @@ public class TestUtils { // Helper for finding the correct offsets if they change public static void printBytes(byte[] data) { byte[] datum = new byte[1]; - for (int i=0; i < data.length; i++) { + for (int i = 0; i < data.length; i++) { datum[0] = data[i]; - System.out.println(i +" : "+ DatatypeConverter.printHexBinary(datum)); + System.out.println(i + " : " + DatatypeConverter.printHexBinary(datum)); } } public static int findOffset(byte[] classBytes, byte[] originalSeq) { int offset = 0; - for (int i=415; i < classBytes.length; i++) { + for (int i = 415; i < classBytes.length; i++) { if (classBytes[i] != originalSeq[offset]) { offset = 0; continue; @@ -110,7 +110,7 @@ public class TestUtils { } offset++; } - + return -1; } @@ -119,7 +119,7 @@ public class TestUtils { return wlcl.instrumentWithCosts(basic, hashSet); } - + public static final class MyClassloader extends ClassLoader { public Class byPath(Path p) throws IOException { diff --git a/experimental/sandbox/src/test/java/net/corda/sandbox/WhitelistClassLoaderTest.java b/experimental/sandbox/src/test/java/net/corda/sandbox/WhitelistClassLoaderTest.java index 91d455b0d2..bcc5e94172 100644 --- a/experimental/sandbox/src/test/java/net/corda/sandbox/WhitelistClassLoaderTest.java +++ b/experimental/sandbox/src/test/java/net/corda/sandbox/WhitelistClassLoaderTest.java @@ -1,10 +1,13 @@ package net.corda.sandbox; import static org.junit.Assert.*; + import org.junit.Test; + import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.concurrent.atomic.AtomicBoolean; + import org.junit.*; public class WhitelistClassLoaderTest { @@ -123,8 +126,8 @@ public class WhitelistClassLoaderTest { final Class clz = wlcl.loadClass("resource.ThrowExceptions"); assertNotNull("ThrowExceptions class could not be transformed and loaded", clz); } - - + + // TODO Test cases that terminate when other resource limits are broken @Test public void when_too_much_memory_is_allocated_then_thread_dies() throws Exception { diff --git a/experimental/sandbox/src/test/java/net/corda/sandbox/costing/DeterministicClassInstrumenterTest.java b/experimental/sandbox/src/test/java/net/corda/sandbox/costing/DeterministicClassInstrumenterTest.java index c276907870..15130086d3 100644 --- a/experimental/sandbox/src/test/java/net/corda/sandbox/costing/DeterministicClassInstrumenterTest.java +++ b/experimental/sandbox/src/test/java/net/corda/sandbox/costing/DeterministicClassInstrumenterTest.java @@ -11,7 +11,6 @@ import java.util.*; import static org.junit.Assert.*; /** - * * @author ben */ public class DeterministicClassInstrumenterTest { @@ -66,7 +65,7 @@ public class DeterministicClassInstrumenterTest { // TestUtils.printBytes(basic); final int origOffset = TestUtils.findOffset(basic, originalSeq); final int tmfdOffset = TestUtils.findOffset(tfmd, tfmdSeq); - + for (int i = 0; i < originalSeq.length; i++) { assertEquals(originalSeq[i], basic[origOffset + i]); assertEquals(tfmdSeq[i], tfmd[tmfdOffset + i]); diff --git a/experimental/sandbox/src/test/java/net/corda/sandbox/costing/SandboxedRewritingTest.java b/experimental/sandbox/src/test/java/net/corda/sandbox/costing/SandboxedRewritingTest.java index 15bd7dafe2..16b3972390 100644 --- a/experimental/sandbox/src/test/java/net/corda/sandbox/costing/SandboxedRewritingTest.java +++ b/experimental/sandbox/src/test/java/net/corda/sandbox/costing/SandboxedRewritingTest.java @@ -1,19 +1,24 @@ package net.corda.sandbox.costing; import net.corda.sandbox.TestUtils; + import static net.corda.sandbox.TestUtils.*; + import net.corda.sandbox.Utils; + import java.io.IOException; import java.lang.reflect.Method; import java.net.URISyntaxException; + import org.junit.AfterClass; import org.junit.Test; + import static org.junit.Assert.*; + import org.junit.Before; import org.junit.BeforeClass; /** - * * @author ben */ public class SandboxedRewritingTest { diff --git a/experimental/sandbox/src/test/java/sandbox/greymalkin/StringReturner.java b/experimental/sandbox/src/test/java/sandbox/greymalkin/StringReturner.java index e0aee3b576..d34132570e 100644 --- a/experimental/sandbox/src/test/java/sandbox/greymalkin/StringReturner.java +++ b/experimental/sandbox/src/test/java/sandbox/greymalkin/StringReturner.java @@ -1,7 +1,6 @@ package sandbox.greymalkin; /** - * * @author ben */ // Simple hack for now, generalise to lambdas later... From 7a372bed593a1e6588a56ffc2b55c35dbe15e42d Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Fri, 6 Oct 2017 15:20:22 +0100 Subject: [PATCH 124/185] Reformat files in `finance` --- .../kotlin/net/corda/finance/Currencies.kt | 18 +++++--- .../finance/contracts/CommercialPaper.kt | 4 +- .../corda/finance/contracts/FinanceTypes.kt | 2 +- .../net/corda/finance/contracts/asset/Cash.kt | 15 +++++-- .../contracts/asset/CommodityContract.kt | 5 ++- .../finance/contracts/asset/Obligation.kt | 9 ++-- .../finance/contracts/asset/OnLedgerAsset.kt | 42 +++++++++---------- .../cash/selection/CashSelectionH2Impl.kt | 10 ++--- .../cash/selection/CashSelectionMySQLImpl.kt | 2 +- .../net/corda/finance/flows/CashExitFlow.kt | 2 +- .../finance/flows/CashIssueAndPaymentFlow.kt | 1 + .../net/corda/finance/flows/CashIssueFlow.kt | 1 + .../corda/finance/flows/CashPaymentFlow.kt | 2 + .../corda/finance/flows/TwoPartyDealFlow.kt | 4 +- .../corda/finance/flows/TwoPartyTradeFlow.kt | 1 + .../net/corda/finance/schemas/CashSchemaV1.kt | 4 +- .../schemas/CommercialPaperSchemaV1.kt | 6 +-- .../finance/utils/StateSummingUtilities.kt | 1 + .../contracts/asset/CashTestsJava.java | 2 +- .../finance/contracts/CommercialPaperTests.kt | 5 ++- .../finance/contracts/asset/CashTests.kt | 30 ++++++------- .../contracts/asset/ObligationTests.kt | 4 +- .../corda/finance/flows/CashExitFlowTests.kt | 2 +- .../corda/finance/flows/CashIssueFlowTests.kt | 2 +- .../finance/flows/CashPaymentFlowTests.kt | 2 +- .../finance/schemas/SampleCashSchemaV1.kt | 4 +- .../finance/schemas/SampleCashSchemaV2.kt | 34 +++++++-------- .../finance/schemas/SampleCashSchemaV3.kt | 2 +- .../schemas/SampleCommercialPaperSchemaV1.kt | 6 +-- .../schemas/SampleCommercialPaperSchemaV2.kt | 6 +-- 30 files changed, 128 insertions(+), 100 deletions(-) diff --git a/finance/src/main/kotlin/net/corda/finance/Currencies.kt b/finance/src/main/kotlin/net/corda/finance/Currencies.kt index 906e01d8af..75bb50cd83 100644 --- a/finance/src/main/kotlin/net/corda/finance/Currencies.kt +++ b/finance/src/main/kotlin/net/corda/finance/Currencies.kt @@ -8,12 +8,18 @@ import net.corda.core.contracts.PartyAndReference import java.math.BigDecimal import java.util.* -@JvmField val USD: Currency = Currency.getInstance("USD") -@JvmField val GBP: Currency = Currency.getInstance("GBP") -@JvmField val EUR: Currency = Currency.getInstance("EUR") -@JvmField val CHF: Currency = Currency.getInstance("CHF") -@JvmField val JPY: Currency = Currency.getInstance("JPY") -@JvmField val RUB: Currency = Currency.getInstance("RUB") +@JvmField +val USD: Currency = Currency.getInstance("USD") +@JvmField +val GBP: Currency = Currency.getInstance("GBP") +@JvmField +val EUR: Currency = Currency.getInstance("EUR") +@JvmField +val CHF: Currency = Currency.getInstance("CHF") +@JvmField +val JPY: Currency = Currency.getInstance("JPY") +@JvmField +val RUB: Currency = Currency.getInstance("RUB") fun AMOUNT(amount: Int, token: T): Amount = AMOUNT(amount.toLong(), token) fun AMOUNT(amount: Long, token: T): Amount = Amount.fromDecimal(BigDecimal.valueOf(amount), token) diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/CommercialPaper.kt b/finance/src/main/kotlin/net/corda/finance/contracts/CommercialPaper.kt index f7fcdc3e69..1c302480f3 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/CommercialPaper.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/CommercialPaper.kt @@ -49,6 +49,7 @@ class CommercialPaper : Contract { companion object { const val CP_PROGRAM_ID: ContractClassName = "net.corda.finance.contracts.CommercialPaper" } + data class State( val issuance: PartyAndReference, override val owner: AbstractParty, @@ -89,7 +90,8 @@ class CommercialPaper : Contract { } } - /** @suppress */ infix fun `owned by`(owner: AbstractParty) = copy(owner = owner) + /** @suppress */ + infix fun `owned by`(owner: AbstractParty) = copy(owner = owner) } interface Commands : CommandData { diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt b/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt index 1079beeccc..be6ada5492 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt @@ -202,7 +202,7 @@ enum class Frequency(val annualCompoundCount: Int, val offset: LocalDate.(Long) * no staff are around to handle problems. */ @CordaSerializable -open class BusinessCalendar (val holidayDates: List) { +open class BusinessCalendar(val holidayDates: List) { @CordaSerializable class UnknownCalendar(name: String) : FlowException("$name not found") diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt index 9a578b7ff2..13e1725c09 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt @@ -401,10 +401,17 @@ class Cash : OnLedgerAsset() { // Small DSL extensions. -/** @suppress */ infix fun Cash.State.`owned by`(owner: AbstractParty) = ownedBy(owner) -/** @suppress */ infix fun Cash.State.`issued by`(party: AbstractParty) = issuedBy(party) -/** @suppress */ infix fun Cash.State.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) -/** @suppress */ infix fun Cash.State.`with deposit`(deposit: PartyAndReference): Cash.State = withDeposit(deposit) +/** @suppress */ +infix fun Cash.State.`owned by`(owner: AbstractParty) = ownedBy(owner) + +/** @suppress */ +infix fun Cash.State.`issued by`(party: AbstractParty) = issuedBy(party) + +/** @suppress */ +infix fun Cash.State.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) + +/** @suppress */ +infix fun Cash.State.`with deposit`(deposit: PartyAndReference): Cash.State = withDeposit(deposit) // Unit testing helpers. These could go in a separate file but it's hardly worth it for just a few functions. diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/CommodityContract.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/CommodityContract.kt index bf31292c69..052223b7e4 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/CommodityContract.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/CommodityContract.kt @@ -44,6 +44,7 @@ class CommodityContract : OnLedgerAsset { constructor(deposit: PartyAndReference, amount: Amount, owner: AbstractParty) : this(Amount(amount.quantity, Issued(deposit, amount.token)), owner) + override val exitKeys: Set = Collections.singleton(owner.owningKey) override val participants = listOf(owner) @@ -91,7 +92,7 @@ class CommodityContract : OnLedgerAsset().firstOrNull() @@ -107,7 +108,7 @@ class CommodityContract : OnLedgerAsset : Contract { companion object { const val PROGRAM_ID: ContractClassName = "net.corda.finance.contracts.asset.Obligation" } + /** * Represents where in its lifecycle a contract state is, which in turn controls the commands that can be applied * to the state. Most states will not leave the [NORMAL] lifecycle. Note that settled (as an end lifecycle) is @@ -191,7 +192,7 @@ class Obligation

: Contract { */ data class Move(override val contract: Class? = null) : MoveCommand - /** + /** * Allows new obligation states to be issued into existence. */ class Issue : TypeOnlyCommandData() @@ -785,9 +786,11 @@ infix fun Obligation.State.between(parties: Pair Obligation.State.`owned by`(owner: AbstractParty) = copy(beneficiary = owner) infix fun Obligation.State.`issued by`(party: AbstractParty) = copy(obligor = party) // For Java users: -@Suppress("unused") fun Obligation.State.ownedBy(owner: AbstractParty) = copy(beneficiary = owner) +@Suppress("unused") +fun Obligation.State.ownedBy(owner: AbstractParty) = copy(beneficiary = owner) -@Suppress("unused") fun Obligation.State.issuedBy(party: AnonymousParty) = copy(obligor = party) +@Suppress("unused") +fun Obligation.State.issuedBy(party: AnonymousParty) = copy(obligor = party) /** A randomly generated key. */ val DUMMY_OBLIGATION_ISSUER_KEY by lazy { entropyToKeyPair(BigInteger.valueOf(10)) } diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/OnLedgerAsset.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/OnLedgerAsset.kt index 7042f3a94f..9f93a80a8c 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/OnLedgerAsset.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/OnLedgerAsset.kt @@ -55,13 +55,13 @@ abstract class OnLedgerAsset> : C */ @Throws(InsufficientBalanceException::class) @JvmStatic - fun , T: Any> generateSpend(tx: TransactionBuilder, - amount: Amount, - to: AbstractParty, - acceptableStates: List>, - payChangeTo: AbstractParty, - deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, - generateMoveCommand: () -> CommandData): Pair> { + fun , T : Any> generateSpend(tx: TransactionBuilder, + amount: Amount, + to: AbstractParty, + acceptableStates: List>, + payChangeTo: AbstractParty, + deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, + generateMoveCommand: () -> CommandData): Pair> { return generateSpend(tx, listOf(PartyAndAmount(to, amount)), acceptableStates, payChangeTo, deriveState, generateMoveCommand) } @@ -91,12 +91,12 @@ abstract class OnLedgerAsset> : C */ @Throws(InsufficientBalanceException::class) @JvmStatic - fun , T: Any> generateSpend(tx: TransactionBuilder, - payments: List>, - acceptableStates: List>, - payChangeTo: AbstractParty, - deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, - generateMoveCommand: () -> CommandData): Pair> { + fun , T : Any> generateSpend(tx: TransactionBuilder, + payments: List>, + acceptableStates: List>, + payChangeTo: AbstractParty, + deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, + generateMoveCommand: () -> CommandData): Pair> { // Discussion // // This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline. @@ -230,11 +230,11 @@ abstract class OnLedgerAsset> : C */ @Throws(InsufficientBalanceException::class) @JvmStatic - fun , T: Any> generateExit(tx: TransactionBuilder, amountIssued: Amount>, - assetStates: List>, - deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, - generateMoveCommand: () -> CommandData, - generateExitCommand: (Amount>) -> CommandData): Set { + fun , T : Any> generateExit(tx: TransactionBuilder, amountIssued: Amount>, + assetStates: List>, + deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, + generateMoveCommand: () -> CommandData, + generateExitCommand: (Amount>) -> CommandData): Set { val owner = assetStates.map { it.state.data.owner }.toSet().singleOrNull() ?: throw InsufficientBalanceException(amountIssued) val currency = amountIssued.token.product val amount = Amount(amountIssued.quantity, currency) @@ -272,9 +272,9 @@ abstract class OnLedgerAsset> : C * wrappers around this function, which build the state for you, and those should be used in preference. */ @JvmStatic - fun , T: Any> generateIssue(tx: TransactionBuilder, - transactionState: TransactionState, - issueCommand: CommandData): Set { + fun , T : Any> generateIssue(tx: TransactionBuilder, + transactionState: TransactionState, + issueCommand: CommandData): Set { check(tx.inputStates().isEmpty()) check(tx.outputStates().map { it.data }.filterIsInstance(transactionState.javaClass).isEmpty()) require(transactionState.data.amount.quantity > 0) 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 7c18981857..a447827205 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 @@ -54,11 +54,11 @@ class CashSelectionH2Impl : CashSelection { */ @Suspendable override fun unconsumedCashStatesForSpending(services: ServiceHub, - amount: Amount, - onlyFromIssuerParties: Set, - notary: Party?, - lockId: UUID, - withIssuerRefs: Set): List> { + amount: Amount, + onlyFromIssuerParties: Set, + notary: Party?, + lockId: UUID, + withIssuerRefs: Set): List> { val issuerKeysStr = onlyFromIssuerParties.fold("") { left, right -> left + "('${right.owningKey.toBase58String()}')," }.dropLast(1) val issuerRefsStr = withIssuerRefs.fold("") { left, right -> left + "('${right.bytes.toHexString()}')," }.dropLast(1) diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionMySQLImpl.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionMySQLImpl.kt index 73a49e8a18..1a0eac0169 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionMySQLImpl.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionMySQLImpl.kt @@ -29,4 +29,4 @@ class CashSelectionMySQLImpl : CashSelection { withIssuerRefs: Set): List> { TODO("MySQL cash selection not implemented") } - } \ No newline at end of file +} \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt index f0084fb169..c978ded6f0 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt @@ -60,7 +60,7 @@ class CashExitFlow(private val amount: Amount, // Work out who the owners of the burnt states were (specify page size so we don't silently drop any if > DEFAULT_PAGE_SIZE) val inputStates = serviceHub.vaultService.queryBy(VaultQueryCriteria(stateRefs = builder.inputStates()), - PageSpecification(pageNumber = DEFAULT_PAGE_NUM, pageSize = builder.inputStates().size)).states + PageSpecification(pageNumber = DEFAULT_PAGE_NUM, pageSize = builder.inputStates().size)).states // TODO: Is it safe to drop participants we don't know how to contact? Does not knowing how to contact them // count as a reason to fail? diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashIssueAndPaymentFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashIssueAndPaymentFlow.kt index 48f0f43c4a..95a58e8cfb 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/CashIssueAndPaymentFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashIssueAndPaymentFlow.kt @@ -32,6 +32,7 @@ class CashIssueAndPaymentFlow(val amount: Amount, recipient: Party, anonymous: Boolean, notary: Party) : this(amount, issueRef, recipient, anonymous, notary, tracker()) + constructor(request: IssueAndPaymentRequest) : this(request.amount, request.issueRef, request.recipient, request.anonymous, request.notary, tracker()) @Suspendable diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashIssueFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashIssueFlow.kt index 56f071783a..39ef76823b 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/CashIssueFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashIssueFlow.kt @@ -31,6 +31,7 @@ class CashIssueFlow(private val amount: Amount, constructor(amount: Amount, issuerBankPartyRef: OpaqueBytes, notary: Party) : this(amount, issuerBankPartyRef, notary, tracker()) + constructor(request: IssueRequest) : this(request.amount, request.issueRef, request.notary, tracker()) @Suspendable diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt index 8857835da2..dc71e54884 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt @@ -31,8 +31,10 @@ open class CashPaymentFlow( val issuerConstraint: Set = emptySet()) : AbstractCashFlow(progressTracker) { /** A straightforward constructor that constructs spends using cash states of any issuer. */ constructor(amount: Amount, recipient: Party) : this(amount, recipient, true, tracker()) + /** A straightforward constructor that constructs spends using cash states of any issuer. */ constructor(amount: Amount, recipient: Party, anonymous: Boolean) : this(amount, recipient, anonymous, tracker()) + constructor(request: PaymentRequest) : this(request.amount, request.recipient, request.anonymous, tracker(), request.issuerConstraint) @Suspendable diff --git a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt index 0525cead6a..b1dd0137aa 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt @@ -43,6 +43,7 @@ object TwoPartyDealFlow { companion object { object GENERATING_ID : ProgressTracker.Step("Generating anonymous identities") object SENDING_PROPOSAL : ProgressTracker.Step("Handshaking and awaiting transaction proposal.") + fun tracker() = ProgressTracker(GENERATING_ID, SENDING_PROPOSAL) } @@ -57,7 +58,7 @@ object TwoPartyDealFlow { val txIdentities = subFlow(SwapIdentitiesFlow(otherSideSession.counterparty)) val anonymousMe = txIdentities[ourIdentity] ?: ourIdentity.anonymise() val anonymousCounterparty = txIdentities[otherSideSession.counterparty] ?: otherSideSession.counterparty.anonymise() - // DOCEND 2 + // DOCEND 2 progressTracker.currentStep = SENDING_PROPOSAL // Make the first message we'll send to kick off the flow. val hello = Handshake(payload, anonymousMe, anonymousCounterparty) @@ -148,6 +149,7 @@ object TwoPartyDealFlow { @Suspendable protected abstract fun validateHandshake(handshake: Handshake): Handshake + @Suspendable protected abstract fun assembleSharedTX(handshake: Handshake): Triple, List> } diff --git a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt index d243792253..2d5093a583 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt @@ -136,6 +136,7 @@ object TwoPartyTradeFlow { private val anonymous: Boolean) : FlowLogic() { constructor(otherSideSession: FlowSession, notary: Party, acceptablePrice: Amount, typeToBuy: Class) : this(otherSideSession, notary, acceptablePrice, typeToBuy, true) + // DOCSTART 2 object RECEIVING : ProgressTracker.Step("Waiting for seller trading info") 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 6ae8d9a9a0..5fdfc58492 100644 --- a/finance/src/main/kotlin/net/corda/finance/schemas/CashSchemaV1.kt +++ b/finance/src/main/kotlin/net/corda/finance/schemas/CashSchemaV1.kt @@ -22,8 +22,8 @@ object CashSchema object CashSchemaV1 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCashState::class.java)) { @Entity @Table(name = "contract_cash_states", - indexes = arrayOf(Index(name = "ccy_code_idx", columnList = "ccy_code"), - Index(name = "pennies_idx", columnList = "pennies"))) + indexes = arrayOf(Index(name = "ccy_code_idx", columnList = "ccy_code"), + Index(name = "pennies_idx", columnList = "pennies"))) class PersistentCashState( /** X500Name of owner party **/ @Column(name = "owner_name") diff --git a/finance/src/main/kotlin/net/corda/finance/schemas/CommercialPaperSchemaV1.kt b/finance/src/main/kotlin/net/corda/finance/schemas/CommercialPaperSchemaV1.kt index b1d58a5c8c..f8c1b46b0e 100644 --- a/finance/src/main/kotlin/net/corda/finance/schemas/CommercialPaperSchemaV1.kt +++ b/finance/src/main/kotlin/net/corda/finance/schemas/CommercialPaperSchemaV1.kt @@ -22,9 +22,9 @@ object CommercialPaperSchema object CommercialPaperSchemaV1 : MappedSchema(schemaFamily = CommercialPaperSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCommercialPaperState::class.java)) { @Entity @Table(name = "cp_states", - indexes = arrayOf(Index(name = "ccy_code_index", columnList = "ccy_code"), - Index(name = "maturity_index", columnList = "maturity_instant"), - Index(name = "face_value_index", columnList = "face_value"))) + indexes = arrayOf(Index(name = "ccy_code_index", columnList = "ccy_code"), + Index(name = "maturity_index", columnList = "maturity_instant"), + Index(name = "face_value_index", columnList = "face_value"))) class PersistentCommercialPaperState( @Column(name = "issuance_key") var issuanceParty: String, diff --git a/finance/src/main/kotlin/net/corda/finance/utils/StateSummingUtilities.kt b/finance/src/main/kotlin/net/corda/finance/utils/StateSummingUtilities.kt index 75d6c072f9..5f3d7c8f37 100644 --- a/finance/src/main/kotlin/net/corda/finance/utils/StateSummingUtilities.kt +++ b/finance/src/main/kotlin/net/corda/finance/utils/StateSummingUtilities.kt @@ -1,4 +1,5 @@ @file:JvmName("StateSumming") + package net.corda.finance.utils import net.corda.core.contracts.Amount diff --git a/finance/src/test/java/net/corda/finance/contracts/asset/CashTestsJava.java b/finance/src/test/java/net/corda/finance/contracts/asset/CashTestsJava.java index 59ba1ad99c..455472503f 100644 --- a/finance/src/test/java/net/corda/finance/contracts/asset/CashTestsJava.java +++ b/finance/src/test/java/net/corda/finance/contracts/asset/CashTestsJava.java @@ -34,7 +34,7 @@ public class CashTestsJava { }); tx.tweak(tw -> { - tw.output(Cash.PROGRAM_ID, () -> outState ); + tw.output(Cash.PROGRAM_ID, () -> outState); tw.command(getMEGA_CORP_PUBKEY(), DummyCommandData.INSTANCE); // Invalid command return tw.failsWith("required net.corda.finance.contracts.asset.Cash.Commands.Move command"); 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 73bdd6d4aa..a1a296eb05 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt @@ -80,7 +80,8 @@ class KotlinCommercialPaperLegacyTest : ICommercialPaperTestTemplate { @RunWith(Parameterized::class) class CommercialPaperTestsGeneric { companion object { - @Parameterized.Parameters @JvmStatic + @Parameterized.Parameters + @JvmStatic fun data() = listOf(JavaCommercialPaperTest(), KotlinCommercialPaperTest(), KotlinCommercialPaperLegacyTest()) } @@ -227,7 +228,7 @@ class CommercialPaperTestsGeneric { private lateinit var moveTX: SignedTransaction -// @Test + // @Test @Ignore fun `issue move and then redeem`() { setCordappPackages("net.corda.finance.contracts") diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt index f30507ec3f..952aa3d9df 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt @@ -590,9 +590,9 @@ class CashTests : TestDependencyInjectionBase() { fun generateSimpleDirectSpend() { initialiseTestSerialization() val wtx = - database.transaction { - makeSpend(100.DOLLARS, THEIR_IDENTITY_1) - } + database.transaction { + makeSpend(100.DOLLARS, THEIR_IDENTITY_1) + } database.transaction { val vaultState = vaultStatesUnconsumed.elementAt(0) assertEquals(vaultState.ref, wtx.inputs[0]) @@ -617,9 +617,9 @@ class CashTests : TestDependencyInjectionBase() { fun generateSimpleSpendWithChange() { initialiseTestSerialization() val wtx = - database.transaction { - makeSpend(10.DOLLARS, THEIR_IDENTITY_1) - } + database.transaction { + makeSpend(10.DOLLARS, THEIR_IDENTITY_1) + } database.transaction { val vaultState = vaultStatesUnconsumed.elementAt(0) val changeAmount = 90.DOLLARS `issued by` defaultIssuer @@ -643,9 +643,9 @@ class CashTests : TestDependencyInjectionBase() { fun generateSpendWithTwoInputs() { initialiseTestSerialization() val wtx = - database.transaction { - makeSpend(500.DOLLARS, THEIR_IDENTITY_1) - } + database.transaction { + makeSpend(500.DOLLARS, THEIR_IDENTITY_1) + } database.transaction { val vaultState0 = vaultStatesUnconsumed.elementAt(0) val vaultState1 = vaultStatesUnconsumed.elementAt(1) @@ -660,11 +660,11 @@ class CashTests : TestDependencyInjectionBase() { fun generateSpendMixedDeposits() { initialiseTestSerialization() val wtx = - database.transaction { - val wtx = makeSpend(580.DOLLARS, THEIR_IDENTITY_1) - assertEquals(3, wtx.inputs.size) - wtx - } + database.transaction { + val wtx = makeSpend(580.DOLLARS, THEIR_IDENTITY_1) + assertEquals(3, wtx.inputs.size) + wtx + } database.transaction { val vaultState0: StateAndRef = vaultStatesUnconsumed.elementAt(0) val vaultState1: StateAndRef = vaultStatesUnconsumed.elementAt(1) @@ -796,7 +796,7 @@ class CashTests : TestDependencyInjectionBase() { transaction { attachment(Cash.PROGRAM_ID) input("MEGA_CORP cash") - output(Cash.PROGRAM_ID, "MEGA_CORP cash 2", "MEGA_CORP cash".output().copy(owner = AnonymousParty(ALICE_PUBKEY)) ) + output(Cash.PROGRAM_ID, "MEGA_CORP cash 2", "MEGA_CORP cash".output().copy(owner = AnonymousParty(ALICE_PUBKEY))) command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } this.verifies() } diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt index 3b053c212b..8a293c4009 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt @@ -286,7 +286,7 @@ class ObligationTests { assertEquals(expected, actual) } - private inline fun getStateAndRef(state: T, contractClassName: ContractClassName): StateAndRef { + private inline fun getStateAndRef(state: T, contractClassName: ContractClassName): StateAndRef { val txState = TransactionState(state, contractClassName, DUMMY_NOTARY) return StateAndRef(txState, StateRef(SecureHash.randomSHA256(), 0)) @@ -418,7 +418,7 @@ class ObligationTests { @Test fun `payment netting`() { // Try netting out two obligations - ledger(mockService) { + ledger(mockService) { cashObligationTestRoots(this) transaction("Issuance") { attachments(Obligation.PROGRAM_ID) diff --git a/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt b/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt index 515480760a..c8c92fad91 100644 --- a/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt @@ -18,7 +18,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith class CashExitFlowTests { - private lateinit var mockNet : MockNetwork + private lateinit var mockNet: MockNetwork private val initialBalance = 2000.DOLLARS private val ref = OpaqueBytes.of(0x01) private lateinit var bankOfCordaNode: StartedNode 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 791dc0d9da..3aeb782a1d 100644 --- a/finance/src/test/kotlin/net/corda/finance/flows/CashIssueFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashIssueFlowTests.kt @@ -22,7 +22,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith class CashIssueFlowTests { - private lateinit var mockNet : MockNetwork + private lateinit var mockNet: MockNetwork private lateinit var bankOfCordaNode: StartedNode private lateinit var bankOfCorda: Party private lateinit var notaryNode: StartedNode diff --git a/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt b/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt index 9304425778..a9b1352646 100644 --- a/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt @@ -21,7 +21,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith class CashPaymentFlowTests { - private lateinit var mockNet : MockNetwork + private lateinit var mockNet: MockNetwork private val initialBalance = 2000.DOLLARS private val ref = OpaqueBytes.of(0x01) private lateinit var bankOfCordaNode: StartedNode diff --git a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV1.kt b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV1.kt index f9395c1adb..b94d6ac700 100644 --- a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV1.kt +++ b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV1.kt @@ -19,8 +19,8 @@ object CashSchema object SampleCashSchemaV1 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCashState::class.java)) { @Entity @Table(name = "contract_cash_states", - indexes = arrayOf(Index(name = "ccy_code_idx", columnList = "ccy_code"), - Index(name = "pennies_idx", columnList = "pennies"))) + indexes = arrayOf(Index(name = "ccy_code_idx", columnList = "ccy_code"), + Index(name = "pennies_idx", columnList = "pennies"))) class PersistentCashState( @Column(name = "owner_key") var owner: String, diff --git a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV2.kt b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV2.kt index 7a5e0da89c..95108eddfa 100644 --- a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV2.kt +++ b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV2.kt @@ -13,25 +13,25 @@ import javax.persistence.Table * [VaultFungibleState] abstract schema */ object SampleCashSchemaV2 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 2, - mappedTypes = listOf(PersistentCashState::class.java)) { + mappedTypes = listOf(PersistentCashState::class.java)) { @Entity @Table(name = "cash_states_v2", - indexes = arrayOf(Index(name = "ccy_code_idx2", columnList = "ccy_code"))) - class PersistentCashState ( - /** product type */ - @Column(name = "ccy_code", length = 3) - var currency: String, + indexes = arrayOf(Index(name = "ccy_code_idx2", columnList = "ccy_code"))) + class PersistentCashState( + /** product type */ + @Column(name = "ccy_code", length = 3) + var currency: String, - /** parent attributes */ - @Transient - val _participants: Set, - @Transient - val _owner: AbstractParty, - @Transient - val _quantity: Long, - @Transient - val _issuerParty: AbstractParty, - @Transient - val _issuerRef: ByteArray + /** parent attributes */ + @Transient + val _participants: Set, + @Transient + val _owner: AbstractParty, + @Transient + val _quantity: Long, + @Transient + val _issuerParty: AbstractParty, + @Transient + val _issuerRef: ByteArray ) : CommonSchemaV1.FungibleState(_participants.toMutableSet(), _owner, _quantity, _issuerParty, _issuerRef) } diff --git a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV3.kt b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV3.kt index f6288c0591..eaae33410f 100644 --- a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV3.kt +++ b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV3.kt @@ -13,7 +13,7 @@ import javax.persistence.Table * at the time of writing. */ object SampleCashSchemaV3 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 3, - mappedTypes = listOf(PersistentCashState::class.java)) { + mappedTypes = listOf(PersistentCashState::class.java)) { @Entity @Table(name = "cash_states_v3") class PersistentCashState( diff --git a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV1.kt b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV1.kt index 9b4c91b179..4da37b7143 100644 --- a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV1.kt +++ b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV1.kt @@ -20,9 +20,9 @@ object CommercialPaperSchema object SampleCommercialPaperSchemaV1 : MappedSchema(schemaFamily = CommercialPaperSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCommercialPaperState::class.java)) { @Entity @Table(name = "cp_states", - indexes = arrayOf(Index(name = "ccy_code_index", columnList = "ccy_code"), - Index(name = "maturity_index", columnList = "maturity_instant"), - Index(name = "face_value_index", columnList = "face_value"))) + indexes = arrayOf(Index(name = "ccy_code_index", columnList = "ccy_code"), + Index(name = "maturity_index", columnList = "maturity_instant"), + Index(name = "face_value_index", columnList = "face_value"))) class PersistentCommercialPaperState( @Column(name = "issuance_key") var issuanceParty: String, diff --git a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV2.kt b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV2.kt index ead55b4b1e..a3b855fe25 100644 --- a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV2.kt +++ b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV2.kt @@ -14,11 +14,11 @@ import javax.persistence.Table * [VaultFungibleState] abstract schema */ object SampleCommercialPaperSchemaV2 : MappedSchema(schemaFamily = CommercialPaperSchema.javaClass, version = 1, - mappedTypes = listOf(PersistentCommercialPaperState::class.java)) { + mappedTypes = listOf(PersistentCommercialPaperState::class.java)) { @Entity @Table(name = "cp_states_v2", - indexes = arrayOf(Index(name = "ccy_code_index2", columnList = "ccy_code"), - Index(name = "maturity_index2", columnList = "maturity_instant"))) + indexes = arrayOf(Index(name = "ccy_code_index2", columnList = "ccy_code"), + Index(name = "maturity_index2", columnList = "maturity_instant"))) class PersistentCommercialPaperState( @Column(name = "maturity_instant") var maturity: Instant, From cb9e27a84ab82bae6e4270f05eff9512fb53817b Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Fri, 6 Oct 2017 15:24:53 +0100 Subject: [PATCH 125/185] Reformat files in `node` --- .../kotlin/net/corda/node/BootTests.kt | 2 +- .../corda/node/CordappScanningDriverTest.kt | 2 +- .../corda/node/NodeStartupPerformanceTests.kt | 2 +- .../node/services/AttachmentLoadingTests.kt | 2 +- .../services/network/NodeInfoWatcherTest.kt | 4 +- .../network/PersistentNetworkMapCacheTest.kt | 5 +- .../statemachine/LargeTransactionsTest.kt | 6 +- .../corda/node/shell/FlowShellCommand.java | 10 +-- .../net/corda/node/shell/RunShellCommand.java | 6 +- .../main/kotlin/net/corda/node/ArgsParser.kt | 6 +- .../net/corda/node/internal/AbstractNode.kt | 2 +- .../kotlin/net/corda/node/internal/Node.kt | 2 +- .../net/corda/node/internal/NodeStartup.kt | 8 +- .../node/internal/cordapp/CordappLoader.kt | 2 +- .../internal/cordapp/CordappProviderImpl.kt | 2 +- .../corda/node/services/api/AuditService.kt | 1 + .../node/services/config/NodeConfiguration.kt | 15 ++-- .../services/events/NodeSchedulerService.kt | 16 ++-- .../identity/InMemoryIdentityService.kt | 4 +- .../keys/PersistentKeyManagementService.kt | 8 +- .../messaging/ArtemisMessagingServer.kt | 3 +- .../services/messaging/NodeMessagingClient.kt | 22 ++--- .../node/services/messaging/RPCServer.kt | 6 ++ .../node/services/network/NodeInfoWatcher.kt | 2 +- .../network/PersistentNetworkMapCache.kt | 1 + ...bstractPartyToX500NameAsStringConverter.kt | 2 +- .../persistence/DBTransactionStorage.kt | 15 ++-- .../persistence/HibernateConfiguration.kt | 6 +- ...achineRecordedTransactionMappingStorage.kt | 1 + .../persistence/NodeAttachmentService.kt | 24 ++--- .../node/services/schema/NodeSchemaService.kt | 16 ++-- .../statemachine/FlowStateMachineImpl.kt | 48 +++++----- .../statemachine/StateMachineManager.kt | 6 +- .../BFTNonValidatingNotaryService.kt | 2 +- .../PersistentUniquenessProvider.kt | 46 +++++----- .../transactions/RaftUniquenessProvider.kt | 4 +- .../vault/HibernateQueryCriteriaParser.kt | 26 +++--- .../node/services/vault/NodeVaultService.kt | 5 +- .../corda/node/services/vault/VaultSchema.kt | 34 +++---- .../node/shell/FlowWatchPrintingSubscriber.kt | 8 +- .../net/corda/node/shell/InteractiveShell.kt | 14 +-- .../node/utilities/AppendOnlyPersistentMap.kt | 18 ++-- .../corda/node/utilities/CordaPersistence.kt | 20 ++--- .../utilities/DatabaseTransactionManager.kt | 2 +- .../corda/node/utilities/KeyStoreUtilities.kt | 2 +- .../node/utilities/NonInvalidatingCache.kt | 3 +- .../utilities/NonInvalidatingUnboundCache.kt | 5 +- .../net/corda/node/utilities/PersistentMap.kt | 89 ++++++++++--------- .../net/corda/node/utilities/TestClock.kt | 3 +- .../net/corda/node/utilities/X509Utilities.kt | 8 +- .../services/vault/VaultQueryJavaTests.java | 36 ++++---- .../net/corda/node/CordaRPCOpsImplTest.kt | 2 +- .../net/corda/node/InteractiveShellTest.kt | 2 +- .../internal/cordapp/CordappLoaderTest.kt | 12 ++- .../cordapp/CordappProviderImplTests.kt | 2 +- .../node/messaging/TwoPartyTradeFlowTests.kt | 5 +- .../services/events/ScheduledFlowTests.kt | 2 +- .../messaging/ArtemisMessagingTests.kt | 4 +- .../network/AbstractNetworkMapServiceTest.kt | 4 +- .../persistence/DBTransactionStorageTests.kt | 11 +-- .../persistence/HibernateConfigurationTest.kt | 64 ++++++------- .../persistence/NodeAttachmentStorageTest.kt | 22 ++--- .../services/schema/NodeSchemaServiceTest.kt | 4 +- .../statemachine/FlowFrameworkTests.kt | 36 +++++--- .../PersistentUniquenessProviderTests.kt | 6 +- .../services/vault/NodeVaultServiceTest.kt | 6 +- .../node/services/vault/VaultQueryTests.kt | 6 +- .../node/services/vault/VaultWithCashTest.kt | 59 ++++++------ .../node/utilities/AffinityExecutorTests.kt | 12 ++- 69 files changed, 448 insertions(+), 393 deletions(-) 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 c61ad1cca9..4b5c17a069 100644 --- a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt @@ -32,7 +32,7 @@ class BootTests { driver { val user = User("u", "p", setOf(startFlowPermission())) val future = startNode(rpcUsers = listOf(user)).getOrThrow().rpcClientToNode(). - start(user.username, user.password).proxy.startFlow(::ObjectInputStreamFlow).returnValue + start(user.username, user.password).proxy.startFlow(::ObjectInputStreamFlow).returnValue assertThatThrownBy { future.getOrThrow() }.isInstanceOf(InvalidClassException::class.java).hasMessage("filter status: REJECTED") } } diff --git a/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt b/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt index a46856b2ce..50788582b5 100644 --- a/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt @@ -36,7 +36,7 @@ class CordappScanningDriverTest { @StartableByRPC @InitiatingFlow - class ReceiveFlow(val otherParty: Party) :FlowLogic() { + class ReceiveFlow(val otherParty: Party) : FlowLogic() { @Suspendable override fun call(): String = initiateFlow(otherParty).receive().unwrap { it } } diff --git a/node/src/integration-test/kotlin/net/corda/node/NodeStartupPerformanceTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodeStartupPerformanceTests.kt index a6aef9b4fd..2f29d8df6d 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodeStartupPerformanceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodeStartupPerformanceTests.kt @@ -17,7 +17,7 @@ class NodeStartupPerformanceTests { driver(networkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = false)) { startDedicatedNetworkMapService().get() val times = ArrayList() - for (i in 1 .. 10) { + for (i in 1..10) { val time = Stopwatch.createStarted().apply { startNode().get() }.stop().elapsed(TimeUnit.MICROSECONDS) 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 bb177abfd9..e5a79d7d5c 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 @@ -70,7 +70,7 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() { val contract = contractClass.newInstance() val txBuilder = generateInitialMethod.invoke(contract, PartyAndReference(DUMMY_BANK_A, OpaqueBytes(kotlin.ByteArray(1))), 1, DUMMY_NOTARY) as TransactionBuilder val context = SerializationFactory.defaultFactory.defaultContext - .withClassLoader(appClassLoader) + .withClassLoader(appClassLoader) val ledgerTx = txBuilder.toLedgerTransaction(services, context) contract.verify(ledgerTx) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt index f845395020..556e47cb50 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt @@ -29,7 +29,9 @@ import kotlin.test.assertTrue class NodeInfoWatcherTest : NodeBasedTest() { - @Rule @JvmField var folder = TemporaryFolder() + @Rule + @JvmField + var folder = TemporaryFolder() lateinit var keyManagementService: KeyManagementService lateinit var nodeInfoPath: Path 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 465fb72229..fae4c25c51 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 @@ -170,8 +170,7 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { (customRetryIntervalMs?.let { mapOf("activeMQServer.bridge.retryIntervalMs" to it.toString()) } ?: emptyMap()) if (party == DUMMY_NOTARY) { startNetworkMapNode(party.name, configOverrides = configOverrides) - } - else { + } else { startNode(party.name, configOverrides = configOverrides, noNetworkMap = noNetworkMap, @@ -184,7 +183,7 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { private fun checkConnectivity(nodes: List>) { nodes.forEach { node1 -> nodes.forEach { node2 -> - if(!(node1 === node2)) { // Do not check connectivity to itself + if (!(node1 === node2)) { // Do not check connectivity to itself node2.internals.registerInitiatedFlow(SendBackFlow::class.java) val resultFuture = node1.services.startFlow(SendFlow(node2.info.chooseIdentity())).resultFuture assertThat(resultFuture.getOrThrow()).isEqualTo("Hello!") diff --git a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt index 0e66989f10..46ac098355 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt @@ -21,7 +21,8 @@ import kotlin.test.assertEquals * transaction size limit (which should only consider the hashes). */ class LargeTransactionsTest { - @StartableByRPC @InitiatingFlow + @StartableByRPC + @InitiatingFlow class SendLargeTransactionFlow(private val hash1: SecureHash, private val hash2: SecureHash, private val hash3: SecureHash, @@ -44,7 +45,8 @@ class LargeTransactionsTest { } } - @InitiatedBy(SendLargeTransactionFlow::class) @Suppress("UNUSED") + @InitiatedBy(SendLargeTransactionFlow::class) + @Suppress("UNUSED") class ReceiveLargeTransactionFlow(private val otherSide: FlowSession) : FlowLogic() { @Suspendable override fun call() { diff --git a/node/src/main/java/net/corda/node/shell/FlowShellCommand.java b/node/src/main/java/net/corda/node/shell/FlowShellCommand.java index 8f16381e01..4bac0ce5b5 100644 --- a/node/src/main/java/net/corda/node/shell/FlowShellCommand.java +++ b/node/src/main/java/net/corda/node/shell/FlowShellCommand.java @@ -12,11 +12,11 @@ import java.util.*; import static net.corda.node.shell.InteractiveShell.*; @Man( - "Allows you to start flows, list the ones available and to watch flows currently running on the node.\n\n" + - "Starting flow is the primary way in which you command the node to change the ledger.\n\n" + - "This command is generic, so the right way to use it depends on the flow you wish to start. You can use the 'flow start'\n" + - "command with either a full class name, or a substring of the class name that's unambiguous. The parameters to the \n" + - "flow constructors (the right one is picked automatically) are then specified using the same syntax as for the run command." + "Allows you to start flows, list the ones available and to watch flows currently running on the node.\n\n" + + "Starting flow is the primary way in which you command the node to change the ledger.\n\n" + + "This command is generic, so the right way to use it depends on the flow you wish to start. You can use the 'flow start'\n" + + "command with either a full class name, or a substring of the class name that's unambiguous. The parameters to the \n" + + "flow constructors (the right one is picked automatically) are then specified using the same syntax as for the run command." ) public class FlowShellCommand extends InteractiveShellCommand { @Command diff --git a/node/src/main/java/net/corda/node/shell/RunShellCommand.java b/node/src/main/java/net/corda/node/shell/RunShellCommand.java index c388ccfe5f..108b567a9b 100644 --- a/node/src/main/java/net/corda/node/shell/RunShellCommand.java +++ b/node/src/main/java/net/corda/node/shell/RunShellCommand.java @@ -13,10 +13,10 @@ import java.util.*; public class RunShellCommand extends InteractiveShellCommand { @Command @Man( - "Runs a method from the CordaRPCOps interface, which is the same interface exposed to RPC clients.\n\n" + + "Runs a method from the CordaRPCOps interface, which is the same interface exposed to RPC clients.\n\n" + - "You can learn more about what commands are available by typing 'run' just by itself, or by\n" + - "consulting the developer guide at https://docs.corda.net/api/kotlin/corda/net.corda.core.messaging/-corda-r-p-c-ops/index.html" + "You can learn more about what commands are available by typing 'run' just by itself, or by\n" + + "consulting the developer guide at https://docs.corda.net/api/kotlin/corda/net.corda.core.messaging/-corda-r-p-c-ops/index.html" ) @Usage("runs a method from the CordaRPCOps interface on the node.") public Object main( diff --git a/node/src/main/kotlin/net/corda/node/ArgsParser.kt b/node/src/main/kotlin/net/corda/node/ArgsParser.kt index db175ea2b7..7139055566 100644 --- a/node/src/main/kotlin/net/corda/node/ArgsParser.kt +++ b/node/src/main/kotlin/net/corda/node/ArgsParser.kt @@ -69,8 +69,8 @@ data class CmdLineOptions(val baseDirectory: Path, val isVersion: Boolean, val noLocalShell: Boolean, val sshdServer: Boolean, - val justGenerateNodeInfo : Boolean) { + val justGenerateNodeInfo: Boolean) { fun loadConfig() = ConfigHelper - .loadConfig(baseDirectory, configFile) - .parseAs() + .loadConfig(baseDirectory, configFile) + .parseAs() } 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 640ac65389..779604259d 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -277,7 +277,7 @@ abstract class AbstractNode(config: NodeConfiguration, /** * This customizes the ServiceHub for each CordaService that is initiating flows */ - private class AppServiceHubImpl(val serviceHub: ServiceHubInternal): AppServiceHub, ServiceHub by serviceHub { + private class AppServiceHubImpl(val serviceHub: ServiceHubInternal) : AppServiceHub, ServiceHub by serviceHub { lateinit var serviceInstance: T override fun startTrackedFlow(flow: FlowLogic): FlowProgressHandle { val stateMachine = startFlowChecked(flow) 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 adf92b70c1..696098df29 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -349,7 +349,7 @@ open class Node(override val configuration: FullNodeConfiguration, _startupComplete.set(Unit) } }, - { th -> logger.error("Unexpected exception", th)} + { th -> logger.error("Unexpected exception", th) } ) shutdownHook = addShutdownHook { stop() 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 631361cd04..2294024e90 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -110,14 +110,14 @@ open class NodeStartup(val args: Array) { startedNode.internals.startupComplete.then { try { InteractiveShell.startShell(cmdlineOptions.baseDirectory, runShell, cmdlineOptions.sshdServer, startedNode) - } catch(e: Throwable) { + } catch (e: Throwable) { logger.error("Shell failed to start", e) } } }, - { - th -> logger.error("Unexpected exception during registration", th) - }) + { th -> + logger.error("Unexpected exception during registration", th) + }) startedNode.internals.run() } diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt index e1cec24244..493b965ba6 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt @@ -111,7 +111,7 @@ class CordappLoader private constructor(private val cordappJarPaths: List) /** Takes a package of classes and creates a JAR from them - only use in tests. */ private fun createDevCordappJar(scanPackage: String, path: URL, jarPackageName: String): URI { - if(!generatedCordapps.contains(path)) { + if (!generatedCordapps.contains(path)) { val cordappDir = File("build/tmp/generated-test-cordapps") cordappDir.mkdirs() val cordappJAR = File(cordappDir, "$scanPackage-${UUID.randomUUID()}.jar") 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 9c3f58293f..967c332fd3 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 @@ -19,7 +19,7 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader) : Singl // TODO: Use better supported APIs in Java 9 Exception().stackTrace.forEach { stackFrame -> val cordapp = getCordappForClass(stackFrame.className) - if(cordapp != null) { + if (cordapp != null) { return getAppContext(cordapp) } } diff --git a/node/src/main/kotlin/net/corda/node/services/api/AuditService.kt b/node/src/main/kotlin/net/corda/node/services/api/AuditService.kt index b9f53f9831..127f69e3d6 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/AuditService.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/AuditService.kt @@ -118,6 +118,7 @@ data class FlowPermissionAuditEvent(override val timestamp: Instant, override val flowId: StateMachineRunId, val permissionRequested: String, val permissionGranted: Boolean) : AuditEvent(), FlowAuditInfo + /** * Minimal interface for recording audit information within the system. The AuditService is assumed to be available only * to trusted internal components via ServiceHubInternal. 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 b15e768f19..95467b63bc 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 @@ -122,12 +122,13 @@ enum class CertChainPolicyType { } data class CertChainPolicyConfig(val role: String, private val policy: CertChainPolicyType, private val trustedAliases: Set) { - val certificateChainCheckPolicy: CertificateChainCheckPolicy get() { - return when (policy) { - CertChainPolicyType.Any -> CertificateChainCheckPolicy.Any - CertChainPolicyType.RootMustMatch -> CertificateChainCheckPolicy.RootMustMatch - CertChainPolicyType.LeafMustMatch -> CertificateChainCheckPolicy.LeafMustMatch - CertChainPolicyType.MustContainOneOf -> CertificateChainCheckPolicy.MustContainOneOf(trustedAliases) + val certificateChainCheckPolicy: CertificateChainCheckPolicy + get() { + return when (policy) { + CertChainPolicyType.Any -> CertificateChainCheckPolicy.Any + CertChainPolicyType.RootMustMatch -> CertificateChainCheckPolicy.RootMustMatch + CertChainPolicyType.LeafMustMatch -> CertificateChainCheckPolicy.LeafMustMatch + CertChainPolicyType.MustContainOneOf -> CertificateChainCheckPolicy.MustContainOneOf(trustedAliases) + } } - } } diff --git a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt index 1d0b5e3d54..26a8322b80 100644 --- a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt +++ b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt @@ -72,7 +72,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, // to wait in our code, rather than Thread.sleep() or other time-based pauses. @Suspendable @VisibleForTesting - // We specify full classpath on SettableFuture to differentiate it from the Quasar class of the same name + // We specify full classpath on SettableFuture to differentiate it from the Quasar class of the same name fun awaitWithDeadline(clock: Clock, deadline: Instant, future: Future<*> = GuavaSettableFuture.create()): Boolean { var nanos: Long do { @@ -89,11 +89,11 @@ class NodeSchedulerService(private val services: ServiceHubInternal, try { // This will return when it times out, or when the clock mutates or when when the original future completes. originalFutureCompleted.get(nanos, TimeUnit.NANOSECONDS) - } catch(e: ExecutionException) { + } catch (e: ExecutionException) { // No need to take action as will fall out of the loop due to future.isDone - } catch(e: CancellationException) { + } catch (e: CancellationException) { // No need to take action as will fall out of the loop due to future.isDone - } catch(e: TimeoutException) { + } catch (e: TimeoutException) { // No need to take action as will fall out of the loop due to future.isDone } } @@ -111,7 +111,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, var txId = it.output.txId ?: throw IllegalStateException("DB returned null SecureHash transactionId") var index = it.output.index ?: throw IllegalStateException("DB returned null SecureHash index") Pair(StateRef(SecureHash.parse(txId), index), - ScheduledStateRef(StateRef(SecureHash.parse(txId), index), it.scheduledAt)) + ScheduledStateRef(StateRef(SecureHash.parse(txId), index), it.scheduledAt)) }, toPersistentEntity = { key: StateRef, value: ScheduledStateRef -> PersistentScheduledState().apply { @@ -152,7 +152,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, private class InnerState { var scheduledStates = createMap() - var scheduledStatesQueue: PriorityQueue = PriorityQueue( { a, b -> a.scheduledAt.compareTo(b.scheduledAt) } ) + var scheduledStatesQueue: PriorityQueue = PriorityQueue({ a, b -> a.scheduledAt.compareTo(b.scheduledAt) }) var rescheduled: GuavaSettableFuture? = null } @@ -162,7 +162,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, // We need the [StateMachineManager] to be constructed before this is called in case it schedules a flow. fun start() { mutex.locked { - scheduledStatesQueue.addAll(scheduledStates.all().map { it.second } .toMutableList()) + scheduledStatesQueue.addAll(scheduledStates.all().map { it.second }.toMutableList()) rescheduleWakeUp() } } @@ -182,7 +182,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, if (action.scheduledAt.isBefore(previousEarliest?.scheduledAt ?: Instant.MAX)) { // We are earliest rescheduleWakeUp() - } else if(previousEarliest?.ref == action.ref && previousEarliest.scheduledAt != action.scheduledAt) { + } else if (previousEarliest?.ref == action.ref && previousEarliest.scheduledAt != action.scheduledAt) { // We were earliest but might not be any more rescheduleWakeUp() } diff --git a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt index 9ee5707a57..616e4bd8c1 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt @@ -30,6 +30,7 @@ class InMemoryIdentityService(identities: Iterable = emptyS constructor(wellKnownIdentities: Iterable = emptySet(), confidentialIdentities: Iterable = emptySet(), trustRoot: X509CertificateHolder) : this(wellKnownIdentities, confidentialIdentities, trustRoot.cert) + companion object { private val log = loggerFor() } @@ -45,7 +46,7 @@ class InMemoryIdentityService(identities: Iterable = emptyS init { val caCertificatesWithRoot: Set = caCertificates.toSet() + trustRoot caCertStore = CertStore.getInstance("Collection", CollectionCertStoreParameters(caCertificatesWithRoot)) - keyToParties.putAll(identities.associateBy { it.owningKey } ) + keyToParties.putAll(identities.associateBy { it.owningKey }) principalToParties.putAll(identities.associateBy { it.name }) confidentialIdentities.forEach { identity -> principalToParties.computeIfAbsent(identity.name) { identity } @@ -94,6 +95,7 @@ class InMemoryIdentityService(identities: Iterable = emptyS null } } + override fun wellKnownPartyFromAnonymous(partyRef: PartyAndReference) = wellKnownPartyFromAnonymous(partyRef.party) override fun requireWellKnownPartyFromAnonymous(party: AbstractParty): Party { return wellKnownPartyFromAnonymous(party) ?: throw IllegalStateException("Could not deanonymise party ${party.owningKey.toStringShort()}") 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 de1b479c1f..4645c1d8e0 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 @@ -48,8 +48,10 @@ class PersistentKeyManagementService(val identityService: IdentityService, fun createKeyMap(): AppendOnlyPersistentMap { return AppendOnlyPersistentMap( toPersistentEntityKey = { it.toBase58String() }, - fromPersistentEntity = { Pair(parsePublicKeyBase58(it.publicKey), - it.privateKey.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)) }, + fromPersistentEntity = { + Pair(parsePublicKeyBase58(it.publicKey), + it.privateKey.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)) + }, toPersistentEntity = { key: PublicKey, value: PrivateKey -> PersistentKey().apply { publicKey = key.toBase58String() @@ -81,7 +83,7 @@ class PersistentKeyManagementService(val identityService: IdentityService, override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): PartyAndCertificate = freshCertificate(identityService, freshKey(), identity, getSigner(identity.owningKey), revocationEnabled) - private fun getSigner(publicKey: PublicKey): ContentSigner = getSigner(getSigningKeyPair(publicKey)) + private fun getSigner(publicKey: PublicKey): ContentSigner = getSigner(getSigningKeyPair(publicKey)) //It looks for the PublicKey in the (potentially) CompositeKey that is ours, and then returns the associated PrivateKey to use in signing private fun getSigningKeyPair(publicKey: PublicKey): KeyPair { 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 b648721633..52421eb6d1 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 @@ -97,7 +97,8 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, companion object { private val log = loggerFor() /** 10 MiB maximum allowed file size for attachments, including message headers. TODO: acquire this value from Network Map when supported. */ - @JvmStatic val MAX_FILE_SIZE = 10485760 + @JvmStatic + val MAX_FILE_SIZE = 10485760 val ipDetectRequestProperty = "ip-request-id" val ipDetectResponseProperty = "ip-address" diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt index 410a753f10..02ee5500ec 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt @@ -117,10 +117,12 @@ class NodeMessagingClient(override val config: NodeConfiguration, fun createMessageToRedeliver(): PersistentMap, RetryMessage, Long> { return PersistentMap( toPersistentEntityKey = { it }, - fromPersistentEntity = { Pair(it.key, - Pair(it.message.deserialize( context = SerializationDefaults.STORAGE_CONTEXT), - it.recipients.deserialize( context = SerializationDefaults.STORAGE_CONTEXT)) - ) }, + fromPersistentEntity = { + Pair(it.key, + Pair(it.message.deserialize(context = SerializationDefaults.STORAGE_CONTEXT), + it.recipients.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)) + ) + }, toPersistentEntity = { _key: Long, (_message: Message, _recipient: MessageRecipients): Pair -> RetryMessage().apply { key = _key @@ -241,7 +243,7 @@ class NodeMessagingClient(override val config: NodeConfiguration, log.info("Network map is complete, so removing filter from P2P consumer.") try { p2pConsumer!!.close() - } catch(e: ActiveMQObjectClosedException) { + } catch (e: ActiveMQObjectClosedException) { // Ignore it: this can happen if the server has gone away before we do. } p2pConsumer = makeP2PConsumer(session, false) @@ -283,8 +285,8 @@ class NodeMessagingClient(override val config: NodeConfiguration, } private fun resumeMessageRedelivery() { - messagesToRedeliver.forEach { - retryId, (message, target) -> send(message, target, retryId) + messagesToRedeliver.forEach { retryId, (message, target) -> + send(message, target, retryId) } } @@ -301,7 +303,7 @@ class NodeMessagingClient(override val config: NodeConfiguration, // It's safe to call into receive simultaneous with other threads calling send on a producer. val artemisMessage: ClientMessage = try { consumer.receive() - } catch(e: ActiveMQObjectClosedException) { + } catch (e: ActiveMQObjectClosedException) { null } ?: return false @@ -433,7 +435,7 @@ class NodeMessagingClient(override val config: NodeConfiguration, } } } - } catch(e: Exception) { + } catch (e: Exception) { log.error("Caught exception whilst executing message handler for ${msg.topicSession}", e) } return true @@ -454,7 +456,7 @@ class NodeMessagingClient(override val config: NodeConfiguration, val c = p2pConsumer ?: throw IllegalStateException("stop can't be called twice") try { c.close() - } catch(e: ActiveMQObjectClosedException) { + } catch (e: ActiveMQObjectClosedException) { // Ignore it: this can happen if the server has gone away before we do. } p2pConsumer = null 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 ad2eb239fa..ae4f328378 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 @@ -86,6 +86,7 @@ class RPCServer( private companion object { val log = loggerFor() } + private enum class State { UNSTARTED, STARTED, @@ -350,6 +351,7 @@ class RPCServer( // TODO remove this User once webserver doesn't need it private val nodeUser = User(NODE_USER, NODE_USER, setOf()) + private fun getUser(message: ClientMessage): User { val validatedUser = message.getStringProperty(Message.HDR_VALIDATED_USER) ?: throw IllegalArgumentException("Missing validated user from the Artemis message") val rpcUser = userService.getUser(validatedUser) @@ -365,6 +367,7 @@ class RPCServer( @JvmField internal val CURRENT_RPC_CONTEXT: ThreadLocal = ThreadLocal() + /** * Returns a context specific to the current RPC call. Note that trying to call this function outside of an RPC will * throw. If you'd like to use the context outside of the call (e.g. in another thread) then pass the returned reference @@ -422,6 +425,7 @@ class ObservableContext( object RpcServerObservableSerializer : Serializer>() { private object RpcObservableContextKey + private val log = loggerFor() fun createContext(observableContext: ObservableContext): SerializationContext { @@ -448,9 +452,11 @@ object RpcServerObservableSerializer : Serializer>() { } } } + override fun onError(exception: Throwable) { log.error("onError called in materialize()d RPC Observable", exception) } + override fun onCompleted() { } } 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 b73ded13e3..83121ba969 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 @@ -99,7 +99,7 @@ class NodeInfoWatcher(private val nodePath: Path, return result } - private fun processFile(file: Path) : NodeInfo? { + private fun processFile(file: Path): NodeInfo? { try { logger.info("Reading NodeInfo from file: $file") val signedData = file.readAll().deserialize>() 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 322a2c9299..09b3c8997f 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 @@ -118,6 +118,7 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) override fun getNodesByLegalName(name: CordaX500Name): List = serviceHub.database.transaction { queryByLegalName(name) } override fun getNodesByLegalIdentityKey(identityKey: PublicKey): List = serviceHub.database.transaction { queryByIdentityKey(identityKey) } + override fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? { val wellKnownParty = serviceHub.identityService.wellKnownPartyFromAnonymous(party) return wellKnownParty?.let { diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/AbstractPartyToX500NameAsStringConverter.kt b/node/src/main/kotlin/net/corda/node/services/persistence/AbstractPartyToX500NameAsStringConverter.kt index ac1d696af7..8dc9c857bd 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/AbstractPartyToX500NameAsStringConverter.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/AbstractPartyToX500NameAsStringConverter.kt @@ -32,7 +32,7 @@ class AbstractPartyToX500NameAsStringConverter(identitySvc: () -> IdentityServic if (dbData != null) { val party = identityService.wellKnownPartyFromX500Name(CordaX500Name.parse(dbData)) if (party != null) return party - log.warn ("Identity service unable to resolve X500name: $dbData") + log.warn("Identity service unable to resolve X500name: $dbData") } return null // non resolvable anonymous parties are stored as nulls } 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 0e68c98cce..ff29e4c1da 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 @@ -30,8 +30,10 @@ class DBTransactionStorage : WritableTransactionStorage, SingletonSerializeAsTok fun createTransactionsMap(): AppendOnlyPersistentMap { return AppendOnlyPersistentMap( toPersistentEntityKey = { it.toString() }, - fromPersistentEntity = { Pair(SecureHash.parse(it.txId), - it.transaction.deserialize( context = SerializationDefaults.STORAGE_CONTEXT)) }, + fromPersistentEntity = { + Pair(SecureHash.parse(it.txId), + it.transaction.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)) + }, toPersistentEntity = { key: SecureHash, value: SignedTransaction -> DBTransaction().apply { txId = key.toString() @@ -46,9 +48,9 @@ class DBTransactionStorage : WritableTransactionStorage, SingletonSerializeAsTok private val txStorage = createTransactionsMap() override fun addTransaction(transaction: SignedTransaction): Boolean = - txStorage.addWithDuplicatesAllowed(transaction.id, transaction).apply { - updatesPublisher.bufferUntilDatabaseCommit().onNext(transaction) - } + txStorage.addWithDuplicatesAllowed(transaction.id, transaction).apply { + updatesPublisher.bufferUntilDatabaseCommit().onNext(transaction) + } override fun getTransaction(id: SecureHash): SignedTransaction? = txStorage[id] @@ -59,5 +61,6 @@ class DBTransactionStorage : WritableTransactionStorage, SingletonSerializeAsTok DataFeed(txStorage.allPersisted().map { it.second }.toList(), updatesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction()) @VisibleForTesting - val transactions: Iterable get() = txStorage.allPersisted().map { it.second }.toList() + val transactions: Iterable + get() = txStorage.allPersisted().map { it.second }.toList() } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/HibernateConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/persistence/HibernateConfiguration.kt index 73d8f77dff..bf6efb6c8c 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/HibernateConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/HibernateConfiguration.kt @@ -32,7 +32,7 @@ class HibernateConfiguration(val schemaService: SchemaService, private val datab // TODO: make this a guava cache or similar to limit ability for this to grow forever. private val sessionFactories = ConcurrentHashMap, SessionFactory>() - private val transactionIsolationLevel = parserTransactionIsolationLevel(databaseProperties.getProperty("transactionIsolationLevel") ?:"") + private val transactionIsolationLevel = parserTransactionIsolationLevel(databaseProperties.getProperty("transactionIsolationLevel") ?: "") init { logger.info("Init HibernateConfiguration for schemas: ${schemaService.schemaOptions.keys}") @@ -61,7 +61,7 @@ class HibernateConfiguration(val schemaService: SchemaService, private val datab // necessarily remain and would likely be replaced by something like Liquibase. For now it is very convenient though. // TODO: replace auto schema generation as it isn't intended for production use, according to Hibernate docs. val config = Configuration(metadataSources).setProperty("hibernate.connection.provider_class", NodeDatabaseConnectionProvider::class.java.name) - .setProperty("hibernate.hbm2ddl.auto", if (databaseProperties.getProperty("initDatabase","true") == "true") "update" else "validate") + .setProperty("hibernate.hbm2ddl.auto", if (databaseProperties.getProperty("initDatabase", "true") == "true") "update" else "validate") .setProperty("hibernate.format_sql", "true") .setProperty("hibernate.connection.isolation", transactionIsolationLevel.toString()) @@ -70,7 +70,7 @@ class HibernateConfiguration(val schemaService: SchemaService, private val datab schema.mappedTypes.forEach { config.addAnnotatedClass(it) } } - val sessionFactory = buildSessionFactory(config, metadataSources, databaseProperties.getProperty("serverNameTablePrefix","")) + val sessionFactory = buildSessionFactory(config, metadataSources, databaseProperties.getProperty("serverNameTablePrefix", "")) logger.info("Created session factory for schemas: $schemas") return sessionFactory } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/InMemoryStateMachineRecordedTransactionMappingStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/InMemoryStateMachineRecordedTransactionMappingStorage.kt index 740373d3d7..84dd4974dd 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/InMemoryStateMachineRecordedTransactionMappingStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/InMemoryStateMachineRecordedTransactionMappingStorage.kt @@ -22,6 +22,7 @@ class InMemoryStateMachineRecordedTransactionMappingStorage : StateMachineRecord val stateMachineTransactionMap = HashMap>() val updates = PublishSubject.create()!! } + private val mutex = ThreadBox(InnerState()) override fun addMapping(stateMachineRunId: StateMachineRunId, transactionId: SecureHash) { 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 3a175effb4..c864521d7b 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 @@ -29,7 +29,7 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single @Entity @Table(name = "${NODE_DATABASE_PREFIX}attachments", - indexes = arrayOf(Index(name = "att_id_idx", columnList = "att_id"))) + indexes = arrayOf(Index(name = "att_id_idx", columnList = "att_id"))) class DBAttachment( @Id @Column(name = "att_id", length = 65535) @@ -69,7 +69,8 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single * around inside it, we haven't read the whole file, so we can't check the hash. But when copying it over the network * this will provide an additional safety check against user error. */ - @VisibleForTesting @CordaSerializable + @VisibleForTesting + @CordaSerializable class HashCheckingStream(val expected: SecureHash.SHA256, val expectedSize: Int, input: InputStream, @@ -110,16 +111,17 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single } private var _hash: HashCode? = null // Backing field for hash property - private val hash: HashCode get() { - var h = _hash - return if (h == null) { - h = stream.hash() - _hash = h - h - } else { - h + private val hash: HashCode + get() { + var h = _hash + return if (h == null) { + h = stream.hash() + _hash = h + h + } else { + h + } } - } } private class AttachmentImpl(override val id: SecureHash, dataLoader: () -> ByteArray, private val checkOnLoad: Boolean) : AbstractAttachment(dataLoader), SerializeAsToken { 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 333ecd6152..0599616644 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 @@ -57,18 +57,18 @@ class NodeSchemaService(customSchemas: Set = emptySet()) : SchemaS PersistentIdentityService.PersistentIdentity::class.java, PersistentIdentityService.PersistentIdentityNames::class.java, ContractUpgradeServiceImpl.DBContractUpgrade::class.java - )) + )) // Required schemas are those used by internal Corda services // For example, cash is used by the vault for coin selection (but will be extracted as a standalone CorDapp in future) private val requiredSchemas: Map = mapOf(Pair(CommonSchemaV1, SchemaService.SchemaOptions()), - Pair(VaultSchemaV1, SchemaService.SchemaOptions()), - Pair(NodeInfoSchemaV1, SchemaService.SchemaOptions()), - Pair(NodeServicesV1, SchemaService.SchemaOptions())) + Pair(VaultSchemaV1, SchemaService.SchemaOptions()), + Pair(NodeInfoSchemaV1, SchemaService.SchemaOptions()), + Pair(NodeServicesV1, SchemaService.SchemaOptions())) - override var schemaOptions: Map = requiredSchemas.plus(customSchemas.map { - mappedSchema -> Pair(mappedSchema, SchemaService.SchemaOptions()) + override var schemaOptions: Map = requiredSchemas.plus(customSchemas.map { mappedSchema -> + Pair(mappedSchema, SchemaService.SchemaOptions()) }) // Currently returns all schemas supported by the state, with no filtering or enrichment. @@ -94,8 +94,8 @@ class NodeSchemaService(customSchemas: Set = emptySet()) : SchemaS } override fun registerCustomSchemas(_customSchemas: Set) { - schemaOptions = schemaOptions.plus(_customSchemas.map { - mappedSchema -> Pair(mappedSchema, SchemaService.SchemaOptions()) + schemaOptions = schemaOptions.plus(_customSchemas.map { mappedSchema -> + Pair(mappedSchema, SchemaService.SchemaOptions()) }) } } 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 0de001283e..0b478243f6 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 @@ -42,7 +42,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, val logic: FlowLogic, scheduler: FiberScheduler, override val flowInitiator: FlowInitiator, - // Store the Party rather than the full cert path with PartyAndCertificate + // Store the Party rather than the full cert path with PartyAndCertificate val ourIdentity: Party) : Fiber(id.toString(), scheduler), FlowStateMachine { companion object { @@ -482,7 +482,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, private fun suspend(ioRequest: FlowIORequest) { // We have to pass the thread local database transaction across via a transient field as the fiber park // swaps them out. - txTrampoline = DatabaseTransactionManager.setThreadLocalTx(null) + txTrampoline = DatabaseTransactionManager.setThreadLocalTx(null) if (ioRequest is WaitingRequest) waitingForResponse = ioRequest @@ -541,28 +541,30 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, } } -val Class>.flowVersionAndInitiatingClass: Pair>> get() { - var current: Class<*> = this - var found: Pair>>? = null - while (true) { - val annotation = current.getDeclaredAnnotation(InitiatingFlow::class.java) - if (annotation != null) { - if (found != null) throw IllegalArgumentException("${InitiatingFlow::class.java.name} can only be annotated once") - require(annotation.version > 0) { "Flow versions have to be greater or equal to 1" } - found = annotation.version to uncheckedCast(current) +val Class>.flowVersionAndInitiatingClass: Pair>> + get() { + var current: Class<*> = this + var found: Pair>>? = null + while (true) { + val annotation = current.getDeclaredAnnotation(InitiatingFlow::class.java) + if (annotation != null) { + if (found != null) throw IllegalArgumentException("${InitiatingFlow::class.java.name} can only be annotated once") + require(annotation.version > 0) { "Flow versions have to be greater or equal to 1" } + found = annotation.version to uncheckedCast(current) + } + current = current.superclass + ?: return found + ?: throw IllegalArgumentException("$name, as a flow that initiates other flows, must be annotated with " + + "${InitiatingFlow::class.java.name}. See https://docs.corda.net/api-flows.html#flowlogic-annotations.") } - current = current.superclass - ?: return found - ?: throw IllegalArgumentException("$name, as a flow that initiates other flows, must be annotated with " + - "${InitiatingFlow::class.java.name}. See https://docs.corda.net/api-flows.html#flowlogic-annotations.") } -} -val Class>.appName: String get() { - val jarFile = Paths.get(protectionDomain.codeSource.location.toURI()) - return if (jarFile.isRegularFile() && jarFile.toString().endsWith(".jar")) { - jarFile.fileName.toString().removeSuffix(".jar") - } else { - "" +val Class>.appName: String + get() { + val jarFile = Paths.get(protectionDomain.codeSource.location.toURI()) + return if (jarFile.isRegularFile() && jarFile.toString().endsWith(".jar")) { + jarFile.fileName.toString().removeSuffix(".jar") + } else { + "" + } } -} diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index c8189063a6..42fd677f65 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -397,7 +397,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } val (ourFlowVersion, appName) = when (initiatedFlowFactory) { - // The flow version for the core flows is the platform version + // The flow version for the core flows is the platform version is InitiatedFlowFactory.Core -> serviceHub.myInfo.platformVersion to "corda" is InitiatedFlowFactory.CorDapp -> initiatedFlowFactory.flowVersion to initiatedFlowFactory.appName } @@ -631,8 +631,8 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, val serialized = try { message.serialize() } catch (e: Exception) { - when(e) { - // Handling Kryo and AMQP serialization problems. Unfortunately the two exception types do not share much of a common exception interface. + when (e) { + // Handling Kryo and AMQP serialization problems. Unfortunately the two exception types do not share much of a common exception interface. is KryoException, is NotSerializableException -> { if (message !is ErrorSessionEnd || message.errorResponse == null) throw e 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 8ea6e6af09..f0c1e17823 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 @@ -112,7 +112,7 @@ class BFTNonValidatingNotaryService(override val services: ServiceHubInternal, name = CordaX500Name.parse(it.party.name), owningKey = parsePublicKeyBase58(it.party.owningKey)))) }, - toPersistentEntity = { (txHash, index) : StateRef, (id, inputIndex, requestingParty): UniquenessProvider.ConsumingTx -> + toPersistentEntity = { (txHash, index): StateRef, (id, inputIndex, requestingParty): UniquenessProvider.ConsumingTx -> PersistedCommittedState( id = PersistentStateRef(txHash.toString(), index), consumingTxHash = id.toString(), 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 b17b9902fc..f669ae3426 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 @@ -24,7 +24,7 @@ import javax.persistence.* class PersistentUniquenessProvider : UniquenessProvider, SingletonSerializeAsToken() { @MappedSuperclass - open class PersistentUniqueness ( + open class PersistentUniqueness( @EmbeddedId var id: PersistentStateRef = PersistentStateRef(), @@ -45,11 +45,11 @@ class PersistentUniquenessProvider : UniquenessProvider, SingletonSerializeAsTok @Column(name = "requesting_party_key", length = 255) var owningKey: String = "" - ): Serializable + ) : Serializable @Entity @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}notary_commit_log") - class PersistentNotaryCommit(id: PersistentStateRef, consumingTxHash: String, consumingIndex: Int, party: PersistentParty): + class PersistentNotaryCommit(id: PersistentStateRef, consumingTxHash: String, consumingIndex: Int, party: PersistentParty) : PersistentUniqueness(id, consumingTxHash, consumingIndex, party) @@ -77,37 +77,37 @@ class PersistentUniquenessProvider : UniquenessProvider, SingletonSerializeAsTok name = CordaX500Name.parse(it.party.name), owningKey = parsePublicKeyBase58(it.party.owningKey)))) }, - toPersistentEntity = { (txHash, index) : StateRef, (id, inputIndex, requestingParty) : UniquenessProvider.ConsumingTx -> + toPersistentEntity = { (txHash, index): StateRef, (id, inputIndex, requestingParty): UniquenessProvider.ConsumingTx -> PersistentNotaryCommit( id = PersistentStateRef(txHash.toString(), index), consumingTxHash = id.toString(), consumingIndex = inputIndex, party = PersistentParty(requestingParty.name.toString(), requestingParty.owningKey.toBase58String()) ) - }, - persistentEntityClass = PersistentNotaryCommit::class.java - ) - } + }, + persistentEntityClass = PersistentNotaryCommit::class.java + ) + } override fun commit(states: List, txId: SecureHash, callerIdentity: Party) { val conflict = mutex.locked { - val conflictingStates = LinkedHashMap() - for (inputState in states) { - val consumingTx = committedStates.get(inputState) - if (consumingTx != null) conflictingStates[inputState] = consumingTx - } - if (conflictingStates.isNotEmpty()) { - log.debug("Failure, input states already committed: ${conflictingStates.keys}") - UniquenessProvider.Conflict(conflictingStates) - } else { - states.forEachIndexed { i, stateRef -> - committedStates[stateRef] = UniquenessProvider.ConsumingTx(txId, i, callerIdentity) - } - log.debug("Successfully committed all input states: $states") - null - } + val conflictingStates = LinkedHashMap() + for (inputState in states) { + val consumingTx = committedStates.get(inputState) + if (consumingTx != null) conflictingStates[inputState] = consumingTx + } + if (conflictingStates.isNotEmpty()) { + log.debug("Failure, input states already committed: ${conflictingStates.keys}") + UniquenessProvider.Conflict(conflictingStates) + } else { + states.forEachIndexed { i, stateRef -> + committedStates[stateRef] = UniquenessProvider.ConsumingTx(txId, i, callerIdentity) } + log.debug("Successfully committed all input states: $states") + null + } + } if (conflict != null) throw UniquenessException(conflict) } 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 93daf5a23a..130f954a50 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 @@ -97,7 +97,8 @@ class RaftUniquenessProvider(private val services: ServiceHubInternal, private v fun start() { log.info("Creating Copycat server, log stored in: ${storagePath.toFile()}") val stateMachineFactory = { - DistributedImmutableMap(db, RaftUniquenessProvider.Companion::createMap) } + DistributedImmutableMap(db, RaftUniquenessProvider.Companion::createMap) + } val address = raftConfig.nodeAddress.let { Address(it.host, it.port) } val storage = buildStorage(storagePath) val transport = buildTransport(transportConfiguration) @@ -110,6 +111,7 @@ class RaftUniquenessProvider(private val services: ServiceHubInternal, private v serializer: Serializer) { writeMap(obj.entries, buffer, serializer) } + override fun read(type: Class>, buffer: BufferInput>, serializer: Serializer): DistributedImmutableMap.Commands.PutAll { 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 30c32220f7..7b8816b361 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 @@ -36,11 +36,11 @@ class HibernateQueryCriteriaParser(val contractStateType: Class, Root<*>>(Pair(VaultSchemaV1.VaultStates::class.java, vaultStates)) private val aggregateExpressions = mutableListOf>() - private val commonPredicates = mutableMapOf, Predicate>() // schema attribute Name, operator -> predicate + private val commonPredicates = mutableMapOf, Predicate>() // schema attribute Name, operator -> predicate var stateTypes: Vault.StateStatus = Vault.StateStatus.UNCONSUMED - override fun parseCriteria(criteria: QueryCriteria.VaultQueryCriteria) : Collection { + override fun parseCriteria(criteria: QueryCriteria.VaultQueryCriteria): Collection { log.trace { "Parsing VaultQueryCriteria: $criteria" } val predicateSet = mutableSetOf() @@ -48,7 +48,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class predicateSet.add(criteriaBuilder.and(vaultStates.get("lockId").isNull)) QueryCriteria.SoftLockingType.LOCKED_ONLY -> @@ -56,7 +56,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class { require(softLocking.lockIds.isNotEmpty()) { "Must specify one or more lockIds" } predicateSet.add(criteriaBuilder.or(vaultStates.get("lockId").isNull, - vaultStates.get("lockId").`in`(softLocking.lockIds.map { it.toString() }))) + vaultStates.get("lockId").`in`(softLocking.lockIds.map { it.toString() }))) } QueryCriteria.SoftLockingType.SPECIFIED -> { require(softLocking.lockIds.isNotEmpty()) { "Must specify one or more lockIds" } @@ -154,7 +154,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class parseExpression(entityRoot: Root, expression: CriteriaExpression, predicateSet: MutableSet) { - if (expression is CriteriaExpression.AggregateFunctionExpression) { + if (expression is CriteriaExpression.AggregateFunctionExpression) { parseAggregateFunction(entityRoot, expression) } else { predicateSet.add(parseExpression(entityRoot, expression) as Predicate) @@ -221,7 +221,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class { + override fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection { log.trace { "Parsing FungibleAssetQueryCriteria: $criteria" } val predicateSet = mutableSetOf() @@ -265,7 +265,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class { + override fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria): Collection { log.trace { "Parsing LinearStateQueryCriteria: $criteria" } val predicateSet = mutableSetOf() @@ -314,8 +314,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class if (message.contains("Not an entity")) throw VaultQueryException(""" @@ -386,8 +385,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class(VaultSchemaV1.VaultStates::stateStatus.name), criteria.status)) } - } - else { + } else { commonPredicates.put(predicateID, criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::stateStatus.name), criteria.status)) } } @@ -417,7 +415,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class val (entityStateClass, entityStateAttributeParent, entityStateAttributeChild) = - when(sortAttribute) { + when (sortAttribute) { is SortAttribute.Standard -> parse(sortAttribute.attribute) is SortAttribute.Custom -> Triple(sortAttribute.entityStateClass, sortAttribute.entityStateColumnName, null) } @@ -451,8 +449,8 @@ class HibernateQueryCriteriaParser(val contractStateType: Class, String, String?> { - val entityClassAndColumnName : Triple, String, String?> = - when(sortAttribute) { + val entityClassAndColumnName: Triple, String, String?> = + when (sortAttribute) { is Sort.CommonStateAttribute -> { Triple(VaultSchemaV1.VaultStates::class.java, sortAttribute.attributeParent, sortAttribute.attributeChild) } 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 6564859865..3b5236d97a 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 @@ -247,7 +247,7 @@ class NodeVaultService(private val clock: Clock, private val keyManagementServic val vaultStates = criteriaUpdate.from(VaultSchemaV1.VaultStates::class.java) val stateStatusPredication = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) val lockIdPredicate = criteriaBuilder.or(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name).isNull, - criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString())) + criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString())) val persistentStateRefs = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) } val compositeKey = vaultStates.get(VaultSchemaV1.VaultStates::stateRef.name) val stateRefsPredicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs)) @@ -468,8 +468,7 @@ class NodeVaultService(private val clock: Clock, private val keyManagementServic vaultState.lockId, vaultState.lockUpdateTime)) statesAndRefs.add(StateAndRef(state, stateRef)) - } - else { + } else { // TODO: improve typing of returned other results log.debug { "OtherResults: ${Arrays.toString(result.toArray())}" } otherResults.addAll(result.toArray().asList()) 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 8dd74ab76f..0c8ba52a4e 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 @@ -25,7 +25,7 @@ object VaultSchema */ @CordaSerializable object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, version = 1, - mappedTypes = listOf(VaultStates::class.java, VaultLinearStates::class.java, VaultFungibleStates::class.java, VaultTxnNote::class.java)) { + mappedTypes = listOf(VaultStates::class.java, VaultLinearStates::class.java, VaultFungibleStates::class.java, VaultTxnNote::class.java)) { @Entity @Table(name = "vault_states", indexes = arrayOf(Index(name = "state_status_idx", columnList = "state_status"))) @@ -90,8 +90,8 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio ) : PersistentState() { constructor(uid: UniqueIdentifier, _participants: List) : this(externalId = uid.externalId, - uuid = uid.id, - participants = _participants.toMutableSet()) + uuid = uid.id, + participants = _participants.toMutableSet()) } @Entity @@ -131,27 +131,27 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio ) : PersistentState() { constructor(_owner: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: OpaqueBytes, _participants: List) : this(owner = _owner, - quantity = _quantity, - issuer = _issuerParty, - issuerRef = _issuerRef.bytes, - participants = _participants.toMutableSet()) + quantity = _quantity, + issuer = _issuerParty, + issuerRef = _issuerRef.bytes, + participants = _participants.toMutableSet()) } @Entity @Table(name = "vault_transaction_notes", - indexes = arrayOf(Index(name = "seq_no_index", columnList = "seq_no"), - Index(name = "transaction_id_index", columnList = "transaction_id"))) + indexes = arrayOf(Index(name = "seq_no_index", columnList = "seq_no"), + Index(name = "transaction_id_index", columnList = "transaction_id"))) class VaultTxnNote( - @Id - @GeneratedValue - @Column(name = "seq_no") - var seqNo: Int, + @Id + @GeneratedValue + @Column(name = "seq_no") + var seqNo: Int, - @Column(name = "transaction_id", length = 64) - var txId: String, + @Column(name = "transaction_id", length = 64) + var txId: String, - @Column(name = "note") - var note: String + @Column(name = "note") + var note: String ) : Serializable { constructor(txId: String, note: String) : this(0, txId, note) } diff --git a/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt b/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt index 32920ffd23..235c2bcbe9 100644 --- a/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt +++ b/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt @@ -59,7 +59,7 @@ class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Sub } private fun createStateMachinesTable(): TableElement { - val table = TableElement(1,2,1,2).overflow(Overflow.HIDDEN).rightCellPadding(1) + val table = TableElement(1, 2, 1, 2).overflow(Overflow.HIDDEN).rightCellPadding(1) val header = RowElement(true).add("Id", "Flow name", "Initiator", "Status").style(Decoration.bold.fg(Color.black).bg(Color.white)) table.add(header) return table @@ -102,12 +102,12 @@ class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Sub } private fun formatFlowId(flowId: StateMachineRunId): String { - return flowId.toString().removeSurrounding("[","]") + return flowId.toString().removeSurrounding("[", "]") } private fun formatFlowInitiator(flowInitiator: FlowInitiator): String { return when (flowInitiator) { - is FlowInitiator.Scheduled -> flowInitiator.scheduledState.ref.toString() + is FlowInitiator.Scheduled -> flowInitiator.scheduledState.ref.toString() is FlowInitiator.Shell -> "Shell" // TODO Change when we will have more information on shell user. is FlowInitiator.Peer -> flowInitiator.party.name.organisation is FlowInitiator.RPC -> "RPC: " + flowInitiator.username @@ -117,7 +117,7 @@ class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Sub private fun formatFlowResult(flowResult: Try<*>): String { fun successFormat(value: Any?): String { - return when(value) { + return when (value) { is SignedTransaction -> "Tx ID: " + value.id.toString() is kotlin.Unit -> "No return value" null -> "No return value" diff --git a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt b/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt index 07ee7e3f96..a671fdb1dd 100644 --- a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt +++ b/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt @@ -247,11 +247,11 @@ object InteractiveShell { // 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. latch.await() - } catch(e: InterruptedException) { + } catch (e: InterruptedException) { ANSIProgressRenderer.progressTracker = null // TODO: When the flow framework allows us to kill flows mid-flight, do so here. } - } catch(e: NoApplicableConstructor) { + } catch (e: NoApplicableConstructor) { output.println("No matching constructor found:", Color.red) e.errors.forEach { output.println("- $it", Color.red) } } finally { @@ -305,14 +305,14 @@ object InteractiveShell { continue } return invoke(flow) - } catch(e: StringToMethodCallParser.UnparseableCallException.MissingParameter) { + } catch (e: StringToMethodCallParser.UnparseableCallException.MissingParameter) { errors.add("${getPrototype()}: missing parameter ${e.paramName}") - } catch(e: StringToMethodCallParser.UnparseableCallException.TooManyParameters) { + } catch (e: StringToMethodCallParser.UnparseableCallException.TooManyParameters) { errors.add("${getPrototype()}: too many parameters") - } catch(e: StringToMethodCallParser.UnparseableCallException.ReflectionDataMissing) { + } catch (e: StringToMethodCallParser.UnparseableCallException.ReflectionDataMissing) { val argTypes = ctor.parameterTypes.map { it.simpleName } errors.add("$argTypes: ") - } catch(e: StringToMethodCallParser.UnparseableCallException) { + } catch (e: StringToMethodCallParser.UnparseableCallException) { val argTypes = ctor.parameterTypes.map { it.simpleName } errors.add("$argTypes: ${e.message}") } @@ -506,7 +506,7 @@ object InteractiveShell { } finally { try { value.close() - } catch(e: IOException) { + } catch (e: IOException) { // Ignore. } } 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 ab9f83e884..62508f0e2b 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt @@ -9,9 +9,9 @@ import java.util.* * behaviour is unpredictable! There is a best-effort check for double inserts, but this should *not* be relied on, so * ONLY USE THIS IF YOUR TABLE IS APPEND-ONLY */ -class AppendOnlyPersistentMap ( +class AppendOnlyPersistentMap( val toPersistentEntityKey: (K) -> EK, - val fromPersistentEntity: (E) -> Pair, + val fromPersistentEntity: (E) -> Pair, val toPersistentEntity: (key: K, value: V) -> E, val persistentEntityClass: Class, cacheBound: Long = 1024 @@ -48,10 +48,11 @@ class AppendOnlyPersistentMap ( return result.map { x -> fromPersistentEntity(x) }.asSequence() } - private tailrec fun set(key: K, value: V, logWarning: Boolean, store: (K,V) -> V?): Boolean { + private tailrec fun set(key: K, value: V, logWarning: Boolean, store: (K, V) -> V?): Boolean { var insertionAttempt = false var isUnique = true - val existingInCache = cache.get(key) { // Thread safe, if multiple threads may wait until the first one has loaded. + val existingInCache = cache.get(key) { + // Thread safe, if multiple threads may wait until the first one has loaded. insertionAttempt = true // Key wasn't in the cache and might be in the underlying storage. // Depending on 'store' method, this may insert without checking key duplication or it may avoid inserting a duplicated key. @@ -85,8 +86,8 @@ class AppendOnlyPersistentMap ( * If the map previously contained a mapping for the key, the behaviour is unpredictable and may throw an error from the underlying storage. */ operator fun set(key: K, value: V) = - set(key, value, logWarning = false) { - k, v -> DatabaseTransactionManager.current().session.save(toPersistentEntity(k, v)) + set(key, value, logWarning = false) { k, v -> + DatabaseTransactionManager.current().session.save(toPersistentEntity(k, v)) null } @@ -96,8 +97,7 @@ class AppendOnlyPersistentMap ( * @return true if added key was unique, otherwise false */ fun addWithDuplicatesAllowed(key: K, value: V, logWarning: Boolean = true): Boolean = - set(key, value, logWarning) { - k, v -> + set(key, value, logWarning) { k, v -> val existingEntry = DatabaseTransactionManager.current().session.find(persistentEntityClass, toPersistentEntityKey(k)) if (existingEntry == null) { DatabaseTransactionManager.current().session.save(toPersistentEntity(k, v)) @@ -107,7 +107,7 @@ class AppendOnlyPersistentMap ( } } - fun putAll(entries: Map) { + fun putAll(entries: Map) { entries.forEach { set(it.key, it.value) } diff --git a/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt b/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt index c1d6d8d0b7..0bf9885945 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt @@ -24,14 +24,14 @@ const val NODE_DATABASE_PREFIX = "node_" //HikariDataSource implements Closeable which allows CordaPersistence to be Closeable class CordaPersistence(var dataSource: HikariDataSource, private val schemaService: SchemaService, - private val createIdentityService: ()-> IdentityService, databaseProperties: Properties): Closeable { + private val createIdentityService: () -> IdentityService, databaseProperties: Properties) : Closeable { var transactionIsolationLevel = parserTransactionIsolationLevel(databaseProperties.getProperty("transactionIsolationLevel")) - val hibernateConfig: HibernateConfiguration by lazy { + val hibernateConfig: HibernateConfiguration by lazy { transaction { HibernateConfiguration(schemaService, databaseProperties, createIdentityService) } - } + } val entityManagerFactory: SessionFactory by lazy { transaction { @@ -70,8 +70,7 @@ class CordaPersistence(var dataSource: HikariDataSource, private val schemaServi return if (outer != null) { outer.statement() - } - else { + } else { inTopLevelTransaction(transactionIsolation, repetitionAttempts, statement) } } @@ -84,19 +83,16 @@ class CordaPersistence(var dataSource: HikariDataSource, private val schemaServi val answer = transaction.statement() transaction.commit() return answer - } - catch (e: SQLException) { + } catch (e: SQLException) { transaction.rollback() repetitions++ if (repetitions >= repetitionAttempts) { throw e } - } - catch (e: Throwable) { + } catch (e: Throwable) { transaction.rollback() throw e - } - finally { + } finally { transaction.close() } } @@ -170,7 +166,7 @@ private class DatabaseTransactionWrappingSubscriber(val db: CordaPersistence? } // A subscriber that wraps another but does not pass on observations to it. -private class NoOpSubscriber(t: Subscriber): Subscriber(t) { +private class NoOpSubscriber(t: Subscriber) : Subscriber(t) { override fun onCompleted() { } diff --git a/node/src/main/kotlin/net/corda/node/utilities/DatabaseTransactionManager.kt b/node/src/main/kotlin/net/corda/node/utilities/DatabaseTransactionManager.kt index f847080433..9016112f7b 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/DatabaseTransactionManager.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/DatabaseTransactionManager.kt @@ -30,7 +30,7 @@ class DatabaseTransaction(isolation: Int, val threadLocal: ThreadLocal private constructor( val cache: LoadingCache -): LoadingCache by cache { +) : LoadingCache by cache { constructor(bound: Long, concurrencyLevel: Int, loadFunction: (K) -> V) : this(buildCache(bound, concurrencyLevel, loadFunction)) @@ -25,6 +25,7 @@ class NonInvalidatingCache private constructor( override fun reload(key: K, oldValue: V): ListenableFuture { throw IllegalStateException("Non invalidating cache refreshed") } + override fun load(key: K) = loadFunction(key) override fun loadAll(keys: Iterable): MutableMap { return super.loadAll(keys) diff --git a/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingUnboundCache.kt b/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingUnboundCache.kt index 44caaa9adb..8719a4e448 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingUnboundCache.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingUnboundCache.kt @@ -6,10 +6,10 @@ import com.google.common.util.concurrent.ListenableFuture class NonInvalidatingUnboundCache private constructor( val cache: LoadingCache -): LoadingCache by cache { +) : LoadingCache by cache { constructor(concurrencyLevel: Int, loadFunction: (K) -> V, removalListener: RemovalListener = RemovalListener {}, - keysToPreload: () -> Iterable = { emptyList() } ) : + keysToPreload: () -> Iterable = { emptyList() }) : this(buildCache(concurrencyLevel, loadFunction, removalListener, keysToPreload)) private companion object { @@ -27,6 +27,7 @@ class NonInvalidatingUnboundCache private constructor( override fun reload(key: K, oldValue: V): ListenableFuture { throw IllegalStateException("Non invalidating cache refreshed") } + override fun load(key: K) = loadFunction(key) override fun loadAll(keys: Iterable): MutableMap { return super.loadAll(keys) diff --git a/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt b/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt index 55acb14efc..c24ce3c229 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt @@ -11,9 +11,9 @@ import java.util.* /** * Implements an unbound caching layer on top of a table accessed via Hibernate mapping. */ -class PersistentMap ( +class PersistentMap( val toPersistentEntityKey: (K) -> EK, - val fromPersistentEntity: (E) -> Pair, + val fromPersistentEntity: (E) -> Pair, val toPersistentEntity: (key: K, value: V) -> E, val persistentEntityClass: Class ) : MutableMap, AbstractMap() { @@ -34,7 +34,7 @@ class PersistentMap ( getAll(session.createQuery(criteriaQuery).resultList.map { e -> fromPersistentEntity(e as E).first }.asIterable()) } - class ExplicitRemoval(private val toPersistentEntityKey: (K) -> EK, private val persistentEntityClass: Class): RemovalListener { + class ExplicitRemoval(private val toPersistentEntityKey: (K) -> EK, private val persistentEntityClass: Class) : RemovalListener { override fun onRemoval(notification: RemovalNotification?) { when (notification?.cause) { RemovalCause.EXPLICIT -> { @@ -47,7 +47,8 @@ class PersistentMap ( RemovalCause.EXPIRED, RemovalCause.SIZE, RemovalCause.COLLECTED -> { log.error("Entry was removed from cache!!!") } - RemovalCause.REPLACED -> {} + RemovalCause.REPLACED -> { + } } } } @@ -62,10 +63,11 @@ class PersistentMap ( override val size get() = cache.size().toInt() - private tailrec fun set(key: K, value: V, logWarning: Boolean = true, store: (K,V) -> V?, replace: (K, V) -> Unit) : Boolean { + private tailrec fun set(key: K, value: V, logWarning: Boolean = true, store: (K, V) -> V?, replace: (K, V) -> Unit): Boolean { var insertionAttempt = false var isUnique = true - val existingInCache = cache.get(key) { // Thread safe, if multiple threads may wait until the first one has loaded. + val existingInCache = cache.get(key) { + // Thread safe, if multiple threads may wait until the first one has loaded. insertionAttempt = true // Value wasn't in the cache and wasn't in DB (because the cache is unbound). // Store the value, depending on store implementation this may replace existing entry in DB. @@ -98,7 +100,7 @@ class PersistentMap ( operator fun set(key: K, value: V) = set(key, value, logWarning = false, - store = { k: K, v: V -> + store = { k: K, v: V -> DatabaseTransactionManager.current().session.save(toPersistentEntity(k, v)) null }, @@ -145,10 +147,10 @@ class PersistentMap ( private fun merge(key: K, value: V): V? { val existingEntry = DatabaseTransactionManager.current().session.find(persistentEntityClass, toPersistentEntityKey(key)) return if (existingEntry != null) { - DatabaseTransactionManager.current().session.merge(toPersistentEntity(key,value)) + DatabaseTransactionManager.current().session.merge(toPersistentEntity(key, value)) fromPersistentEntity(existingEntry).second } else { - DatabaseTransactionManager.current().session.save(toPersistentEntity(key,value)) + DatabaseTransactionManager.current().session.save(toPersistentEntity(key, value)) null } } @@ -193,56 +195,59 @@ class PersistentMap ( } } - override val keys: MutableSet get() { - return object : AbstractSet() { - override val size: Int get() = this@PersistentMap.size - override fun iterator(): MutableIterator { - return object : MutableIterator { - private val entryIterator = EntryIterator() + override val keys: MutableSet + get() { + return object : AbstractSet() { + override val size: Int get() = this@PersistentMap.size + override fun iterator(): MutableIterator { + return object : MutableIterator { + private val entryIterator = EntryIterator() - override fun hasNext(): Boolean = entryIterator.hasNext() - override fun next(): K = entryIterator.next().key - override fun remove() { - entryIterator.remove() + override fun hasNext(): Boolean = entryIterator.hasNext() + override fun next(): K = entryIterator.next().key + override fun remove() { + entryIterator.remove() + } } } } } - } - override val values: MutableCollection get() { - return object : AbstractCollection() { - override val size: Int get() = this@PersistentMap.size - override fun iterator(): MutableIterator { - return object : MutableIterator { - private val entryIterator = EntryIterator() + override val values: MutableCollection + get() { + return object : AbstractCollection() { + override val size: Int get() = this@PersistentMap.size + override fun iterator(): MutableIterator { + return object : MutableIterator { + private val entryIterator = EntryIterator() - override fun hasNext(): Boolean = entryIterator.hasNext() - override fun next(): V = entryIterator.next().value - override fun remove() { - entryIterator.remove() + override fun hasNext(): Boolean = entryIterator.hasNext() + override fun next(): V = entryIterator.next().value + override fun remove() { + entryIterator.remove() + } } } } } - } - override val entries: MutableSet> get() { - return object : AbstractSet>() { - override val size: Int get() = this@PersistentMap.size - override fun iterator(): MutableIterator> { - return object : MutableIterator> { - private val entryIterator = EntryIterator() + override val entries: MutableSet> + get() { + return object : AbstractSet>() { + override val size: Int get() = this@PersistentMap.size + override fun iterator(): MutableIterator> { + return object : MutableIterator> { + private val entryIterator = EntryIterator() - override fun hasNext(): Boolean = entryIterator.hasNext() - override fun next(): MutableMap.MutableEntry = entryIterator.next() - override fun remove() { - entryIterator.remove() + override fun hasNext(): Boolean = entryIterator.hasNext() + override fun next(): MutableMap.MutableEntry = entryIterator.next() + override fun remove() { + entryIterator.remove() + } } } } } - } override fun put(key: K, value: V): V? { val old = cache.get(key) diff --git a/node/src/main/kotlin/net/corda/node/utilities/TestClock.kt b/node/src/main/kotlin/net/corda/node/utilities/TestClock.kt index e582f45147..60af87f895 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/TestClock.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/TestClock.kt @@ -21,7 +21,8 @@ class TestClock(private var delegateClock: Clock = Clock.systemUTC()) : MutableC override fun toToken(context: SerializeAsTokenContext) = token.registerWithContext(context, this) - @Synchronized fun updateDate(date: LocalDate): Boolean { + @Synchronized + fun updateDate(date: LocalDate): Boolean { val currentDate = LocalDate.now(this) if (currentDate.isBefore(date)) { // It's ok to increment diff --git a/node/src/main/kotlin/net/corda/node/utilities/X509Utilities.kt b/node/src/main/kotlin/net/corda/node/utilities/X509Utilities.kt index a9685fd910..4dfa07ec09 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/X509Utilities.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/X509Utilities.kt @@ -115,7 +115,7 @@ object X509Utilities { subjectPublicKey: PublicKey, validityWindow: Pair = DEFAULT_VALIDITY_WINDOW, nameConstraints: NameConstraints? = null): X509CertificateHolder - = createCertificate(certificateType, issuerCertificate, issuerKeyPair, subject.x500Name, subjectPublicKey, validityWindow, nameConstraints) + = createCertificate(certificateType, issuerCertificate, issuerKeyPair, subject.x500Name, subjectPublicKey, validityWindow, nameConstraints) /** * Create a X509 v3 certificate for use as a CA or for TLS. This does not require a [CordaX500Name] because the @@ -267,9 +267,9 @@ object X509Utilities { * @param nameConstraints any name constraints to impose on certificates signed by the generated certificate. */ internal fun createCertificate(certificateType: CertificateType, issuer: X500Name, issuerKeyPair: KeyPair, - subject: X500Name, subjectPublicKey: PublicKey, - validityWindow: Pair, - nameConstraints: NameConstraints? = null): X509CertificateHolder { + subject: X500Name, subjectPublicKey: PublicKey, + validityWindow: Pair, + nameConstraints: NameConstraints? = null): X509CertificateHolder { val signatureScheme = Crypto.findSignatureScheme(issuerKeyPair.private) val provider = Crypto.findProvider(signatureScheme.providerName) diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index 6a38dde99a..97dab2aaa7 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -91,10 +91,10 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { @Test public void unconsumedStatesForStateRefsSortedByTxnId() { Vault issuedStates = - database.transaction(tx -> { - VaultFiller.fillWithSomeTestLinearStates(services, 8); - return VaultFiller.fillWithSomeTestLinearStates(services, 2); - }); + database.transaction(tx -> { + VaultFiller.fillWithSomeTestLinearStates(services, 8); + return VaultFiller.fillWithSomeTestLinearStates(services, 2); + }); database.transaction(tx -> { Stream stateRefsStream = StreamSupport.stream(issuedStates.getStates().spliterator(), false).map(StateAndRef::getRef); List stateRefs = stateRefsStream.collect(Collectors.toList()); @@ -120,7 +120,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { database.transaction(tx -> { VaultFiller.fillWithSomeTestCash(services, new Amount(100, Currency.getInstance("USD")), - issuerServices, + issuerServices, TestConstants.getDUMMY_NOTARY(), 3, 3, @@ -151,14 +151,14 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { List dealIds = Arrays.asList("123", "456", "789"); @SuppressWarnings("unchecked") Triple, UniqueIdentifier, Vault> ids = - database.transaction((DatabaseTransaction tx) -> { - Vault states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null); - StateAndRef linearState = states.getStates().iterator().next(); - UniqueIdentifier uid = linearState.component1().getData().getLinearId(); + database.transaction((DatabaseTransaction tx) -> { + Vault states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null); + StateAndRef linearState = states.getStates().iterator().next(); + UniqueIdentifier uid = linearState.component1().getData().getLinearId(); - Vault dealStates = VaultFiller.fillWithSomeTestDeals(services, dealIds); - return new Triple(linearState,uid,dealStates); - }); + Vault dealStates = VaultFiller.fillWithSomeTestDeals(services, dealIds); + return new Triple(linearState, uid, dealStates); + }); database.transaction(tx -> { // consume states VaultFiller.consumeDeals(services, (List>) ids.getThird().getStates(), getDUMMY_NOTARY()); @@ -276,13 +276,13 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { public void trackDealStatesPagedSorted() { List dealIds = Arrays.asList("123", "456", "789"); UniqueIdentifier uid = - database.transaction(tx -> { - Vault states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null); - UniqueIdentifier _uid = states.getStates().iterator().next().component1().getData().getLinearId(); + database.transaction(tx -> { + Vault states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null); + UniqueIdentifier _uid = states.getStates().iterator().next().component1().getData().getLinearId(); - VaultFiller.fillWithSomeTestDeals(services, dealIds); - return _uid; - }); + VaultFiller.fillWithSomeTestDeals(services, dealIds); + return _uid; + }); database.transaction(tx -> { // DOCSTART VaultJavaQueryExample5 @SuppressWarnings("unchecked") diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index a33dadfcea..e03fd6972c 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -272,6 +272,6 @@ class CordaRPCOpsImplTest { @StartableByRPC class VoidRPCFlow : FlowLogic() { @Suspendable - override fun call() : Void? = null + override fun call(): Void? = null } } diff --git a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt index d13fda0a4c..f694eec66c 100644 --- a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt +++ b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt @@ -84,4 +84,4 @@ class InteractiveShellTest { fun party() = check("party: \"${MEGA_CORP.name}\"", MEGA_CORP.name.toString()) class DummyFSM(val logic: FlowA) : FlowStateMachine by mock() - } +} diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt index c1a1b6e5b0..6e8fab1620 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt @@ -9,25 +9,29 @@ import java.nio.file.Paths @InitiatingFlow class DummyFlow : FlowLogic() { @Suspendable - override fun call() { } + override fun call() { + } } @InitiatedBy(DummyFlow::class) class LoaderTestFlow(unusedSession: FlowSession) : FlowLogic() { @Suspendable - override fun call() { } + override fun call() { + } } @SchedulableFlow class DummySchedulableFlow : FlowLogic() { @Suspendable - override fun call() { } + override fun call() { + } } @StartableByRPC class DummyRPCFlow : FlowLogic() { @Suspendable - override fun call() { } + override fun call() { + } } class CordappLoaderTest { 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 10dcd4a3ad..877037c387 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 @@ -37,7 +37,7 @@ class CordappProviderImplTests { val provider = CordappProviderImpl(loader) provider.start(attachmentStore) - + Assert.assertNull(provider.getCordappAttachmentId(provider.cordapps.first())) } diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 294d758cf4..ffaafa6246 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -80,6 +80,7 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { return listOf(true, false) } } + private lateinit var mockNet: MockNetwork @Before @@ -164,8 +165,8 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { val cashStates = bobNode.database.transaction { bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, notary, 3, 3, - issuedBy = issuer) - } + issuedBy = issuer) + } val alicesFakePaper = aliceNode.database.transaction { fillUpForSeller(false, issuer, aliceNode.info.chooseIdentity(), diff --git a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt index bb7c0d3e92..013eea5d74 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt @@ -153,7 +153,7 @@ class ScheduledFlowTests { val statesFromB: List> = nodeB.database.transaction { queryStatesWithPaging(nodeB.services.vaultService) } - assertEquals("Expect all states to be present",2 * N, statesFromA.count()) + assertEquals("Expect all states to be present", 2 * N, statesFromA.count()) statesFromA.forEach { ref -> if (ref !in statesFromB) { throw IllegalStateException("State $ref is only present on node A.") diff --git a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt index 3d1942c01c..6a1adee86a 100644 --- a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt @@ -40,7 +40,9 @@ import kotlin.test.assertNull //TODO This needs to be merged into P2PMessagingTest as that creates a more realistic environment class ArtemisMessagingTests : TestDependencyInjectionBase() { - @Rule @JvmField val temporaryFolder = TemporaryFolder() + @Rule + @JvmField + val temporaryFolder = TemporaryFolder() val serverPort = freePort() val rpcPort = freePort() diff --git a/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt index a8646caad0..5e0f97e770 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt @@ -44,7 +44,7 @@ abstract class AbstractNetworkMapServiceTest lateinit var alice: StartedNode companion object { - val subscriberLegalName = CordaX500Name(organisation ="Subscriber", locality ="New York", country ="US") + val subscriberLegalName = CordaX500Name(organisation = "Subscriber", locality = "New York", country = "US") } @Before @@ -205,7 +205,7 @@ abstract class AbstractNetworkMapServiceTest private var lastSerial = Long.MIN_VALUE private fun StartedNode<*>.registration(addOrRemove: AddOrRemove, - serial: Long? = null): CordaFuture { + serial: Long? = null): CordaFuture { val distinctSerial = if (serial == null) { ++lastSerial } else { 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 f043a4c675..3a3148c0db 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 @@ -42,11 +42,12 @@ class DBTransactionStorageTests : TestDependencyInjectionBase() { database.transaction { services = object : MockServices(BOB_KEY) { - override val vaultService: VaultServiceInternal get() { - val vaultService = NodeVaultService(clock, keyManagementService, stateLoader, database.hibernateConfig) - hibernatePersister = HibernateObserver(vaultService.rawUpdates, database.hibernateConfig) - return vaultService - } + override val vaultService: VaultServiceInternal + get() { + val vaultService = NodeVaultService(clock, keyManagementService, stateLoader, database.hibernateConfig) + hibernatePersister = HibernateObserver(vaultService.rawUpdates, database.hibernateConfig) + return vaultService + } override fun recordTransactions(txs: Iterable) { for (stx in txs) { diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt index 7cb537575b..e554ea59be 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt @@ -89,6 +89,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. vaultService.notifyAll(txs.map { it.tx }) } + override fun jdbcSession() = database.createSession() } hibernatePersister = services.hibernatePersister @@ -146,10 +147,10 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { @Test fun `select by composite primary key`() { val issuedStates = - database.transaction { - services.fillWithSomeTestLinearStates(8) - services.fillWithSomeTestLinearStates(2) - } + database.transaction { + services.fillWithSomeTestLinearStates(8) + services.fillWithSomeTestLinearStates(2) + } val persistentStateRefs = issuedStates.states.map { PersistentStateRef(it.ref) }.toList() // structure query @@ -383,7 +384,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { // aggregate function criteriaQuery.multiselect(cashStates.get("currency"), - criteriaBuilder.sum(cashStates.get("pennies"))) + criteriaBuilder.sum(cashStates.get("pennies"))) // group by criteriaQuery.groupBy(cashStates.get("currency")) @@ -627,7 +628,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 2, 2, Random(0L), ownedBy = ALICE) val cashStates = services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 2, 2, Random(0L), - issuedBy = BOB.ref(0), ownedBy = (BOB)).states + issuedBy = BOB.ref(0), ownedBy = (BOB)).states // persist additional cash states explicitly with V3 schema cashStates.forEach { val cashState = it.state.data @@ -662,7 +663,8 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { queryResults.forEach { val contractState = it.contractState.deserialize>(context = SerializationDefaults.STORAGE_CONTEXT) val cashState = contractState.data as Cash.State - println("${it.stateRef} with owner: ${cashState.owner.owningKey.toBase58String()}") } + println("${it.stateRef} with owner: ${cashState.owner.owningKey.toBase58String()}") + } assertThat(queryResults).hasSize(12) } @@ -697,32 +699,32 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { @Test fun `query fungible states by participants`() { val firstCashState = - database.transaction { - // persist original cash states explicitly with V3 schema - cashStates.forEach { - val cashState = it.state.data - val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) - hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) - } + database.transaction { + // persist original cash states explicitly with V3 schema + cashStates.forEach { + val cashState = it.state.data + val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) + hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) + } - val moreCash = services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 2, 2, Random(0L), - issuedBy = BOB.ref(0), ownedBy = BOB).states - // persist additional cash states explicitly with V3 schema - moreCash.forEach { - val cashState = it.state.data - val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) - hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) - } + val moreCash = services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 2, 2, Random(0L), + issuedBy = BOB.ref(0), ownedBy = BOB).states + // persist additional cash states explicitly with V3 schema + moreCash.forEach { + val cashState = it.state.data + val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) + hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) + } - val cashStates = services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 2, 2, Random(0L), ownedBy = (ALICE)).states - // persist additional cash states explicitly with V3 schema - cashStates.forEach { - val cashState = it.state.data - val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) - hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) + val cashStates = services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 2, 2, Random(0L), ownedBy = (ALICE)).states + // persist additional cash states explicitly with V3 schema + cashStates.forEach { + val cashState = it.state.data + val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) + hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) + } + cashStates.first() } - cashStates.first() - } // structure query val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) @@ -874,7 +876,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { val jdbcSession = services.jdbcSession() val prepStatement = jdbcSession.prepareStatement(nativeQuery) val rs = prepStatement.executeQuery() - // DOCEND JdbcSession + // DOCEND JdbcSession var count = 0 while (rs.next()) { val stateRef = StateRef(SecureHash.parse(rs.getString(1)), rs.getInt(2)) diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt index 0481ca51cd..46e954ecde 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt @@ -99,18 +99,18 @@ class NodeAttachmentStorageTest { fun `corrupt entry throws exception`() { val testJar = makeTestJar() val id = - database.transaction { - val storage = NodeAttachmentService(MetricRegistry()) - val id = testJar.read { storage.importAttachment(it) } + database.transaction { + val storage = NodeAttachmentService(MetricRegistry()) + val id = testJar.read { storage.importAttachment(it) } - // Corrupt the file in the store. - val bytes = testJar.readAll() - val corruptBytes = "arggghhhh".toByteArray() - System.arraycopy(corruptBytes, 0, bytes, 0, corruptBytes.size) - val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = bytes) - DatabaseTransactionManager.current().session.merge(corruptAttachment) - id - } + // Corrupt the file in the store. + val bytes = testJar.readAll() + val corruptBytes = "arggghhhh".toByteArray() + System.arraycopy(corruptBytes, 0, bytes, 0, corruptBytes.size) + val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = bytes) + DatabaseTransactionManager.current().session.merge(corruptAttachment) + id + } database.transaction { val storage = NodeAttachmentService(MetricRegistry()) val e = assertFailsWith { diff --git a/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt index d7b90b8880..73bb252af5 100644 --- a/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt @@ -41,7 +41,7 @@ class NodeSchemaServiceTest { */ @Test fun `auto scanning of custom schemas for testing with Driver`() { - driver (startNodesInProcess = true) { + driver(startNodesInProcess = true) { val node = startNode() val nodeHandle = node.getOrThrow() val result = nodeHandle.rpc.startFlow(::MappedSchemasFlow) @@ -53,7 +53,7 @@ class NodeSchemaServiceTest { @StartableByRPC class MappedSchemasFlow : FlowLogic>() { @Suspendable - override fun call() : List { + override fun call(): List { // returning MappedSchema's as String'ified family names to avoid whitelist serialization errors return (this.serviceHub as ServiceHubInternal).schemaService.schemaOptions.keys.map { it.name } } diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 42b504a65e..57ca2cebba 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -702,8 +702,7 @@ class FlowFrameworkTests { private inline fun > StartedNode<*>.registerFlowFactory( initiatingFlowClass: KClass>, initiatedFlowVersion: Int = 1, - noinline flowFactory: (FlowSession) -> P): CordaFuture

- { + noinline flowFactory: (FlowSession) -> P): CordaFuture

{ val observable = internals.internalRegisterFlowFactory( initiatingFlowClass.java, InitiatedFlowFactory.CorDapp(initiatedFlowVersion, "", flowFactory), @@ -715,6 +714,7 @@ class FlowFrameworkTests { private fun sessionInit(clientFlowClass: KClass>, flowVersion: Int = 1, payload: Any? = null): SessionInit { return SessionInit(0, clientFlowClass.java.name, flowVersion, "", payload) } + private fun sessionConfirm(flowVersion: Int = 1) = SessionConfirm(0, 0, flowVersion, "") private fun sessionData(payload: Any) = SessionData(0, payload) private val normalEnd = NormalSessionEnd(0) @@ -766,14 +766,15 @@ class FlowFrameworkTests { private infix fun StartedNode.sent(message: SessionMessage): Pair = Pair(internals.id, message) private infix fun Pair.to(node: StartedNode<*>): SessionTransfer = SessionTransfer(first, second, node.network.myAddress) - private val FlowLogic<*>.progressSteps: CordaFuture>> get() { - return progressTracker!!.changes - .ofType(Change.Position::class.java) - .map { it.newStep } - .materialize() - .toList() - .toFuture() - } + private val FlowLogic<*>.progressSteps: CordaFuture>> + get() { + return progressTracker!!.changes + .ofType(Change.Position::class.java) + .map { it.newStep } + .materialize() + .toList() + .toFuture() + } private class LazyServiceHubAccessFlow : FlowLogic() { val lazyTime: Instant by lazy { serviceHub.clock.instant() } @@ -782,7 +783,8 @@ class FlowFrameworkTests { } private class NoOpFlow(val nonTerminating: Boolean = false) : FlowLogic() { - @Transient var flowStarted = false + @Transient + var flowStarted = false @Suspendable override fun call() { @@ -833,7 +835,8 @@ class FlowFrameworkTests { override val progressTracker: ProgressTracker = ProgressTracker(START_STEP, RECEIVED_STEP) private var nonTerminating: Boolean = false - @Transient var receivedPayloads: List = emptyList() + @Transient + var receivedPayloads: List = emptyList() @Suspendable override fun call() { @@ -879,6 +882,7 @@ class FlowFrameworkTests { @InitiatingFlow private class SendAndReceiveFlow(val otherParty: Party, val payload: Any, val otherPartySession: FlowSession? = null) : FlowLogic() { constructor(otherPartySession: FlowSession, payload: Any) : this(otherPartySession.counterparty, payload, otherPartySession) + @Suspendable override fun call(): Any = (otherPartySession ?: initiateFlow(otherParty)).sendAndReceive(payload).unwrap { it } } @@ -891,8 +895,11 @@ class FlowFrameworkTests { @InitiatingFlow private class PingPongFlow(val otherParty: Party, val payload: Long, val otherPartySession: FlowSession? = null) : FlowLogic() { constructor(otherPartySession: FlowSession, payload: Long) : this(otherPartySession.counterparty, payload, otherPartySession) - @Transient var receivedPayload: Long? = null - @Transient var receivedPayload2: Long? = null + + @Transient + var receivedPayload: Long? = null + @Transient + var receivedPayload2: Long? = null @Suspendable override fun call() { @@ -959,6 +966,7 @@ class FlowFrameworkTests { @InitiatingFlow(version = 2) private class UpgradedFlow(val otherParty: Party, val otherPartySession: FlowSession? = null) : FlowLogic>() { constructor(otherPartySession: FlowSession) : this(otherPartySession.counterparty, otherPartySession) + @Suspendable override fun call(): Pair { val otherPartySession = this.otherPartySession ?: initiateFlow(otherParty) 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 1c1124cb1f..491931ab2a 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 @@ -32,7 +32,8 @@ class PersistentUniquenessProviderTests : TestDependencyInjectionBase() { LogHelper.reset(PersistentUniquenessProvider::class) } - @Test fun `should commit a transaction with unused inputs without exception`() { + @Test + fun `should commit a transaction with unused inputs without exception`() { database.transaction { val provider = PersistentUniquenessProvider() val inputState = generateStateRef() @@ -41,7 +42,8 @@ class PersistentUniquenessProviderTests : TestDependencyInjectionBase() { } } - @Test fun `should report a conflict for a transaction with previously used inputs`() { + @Test + fun `should report a conflict for a transaction with previously used inputs`() { database.transaction { val provider = PersistentUniquenessProvider() val inputState = generateStateRef() 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 8b364eb12b..5250e7f500 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 @@ -55,7 +55,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { setCordappPackages("net.corda.finance.contracts.asset") LogHelper.setLevel(NodeVaultService::class) val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(BOC_KEY, DUMMY_CASH_ISSUER_KEY), - customSchemas = setOf(CashSchemaV1)) + customSchemas = setOf(CashSchemaV1)) database = databaseAndServices.first services = databaseAndServices.second issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY, BOC_KEY) @@ -195,7 +195,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { assertThat(vaultService.queryBy(criteriaByLockId1).states).hasSize(3) } println("SOFT LOCK STATES #1 succeeded") - } catch(e: Throwable) { + } catch (e: Throwable) { println("SOFT LOCK STATES #1 failed") } finally { countDown.countDown() @@ -211,7 +211,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { assertThat(vaultService.queryBy(criteriaByLockId2).states).hasSize(3) } println("SOFT LOCK STATES #2 succeeded") - } catch(e: Throwable) { + } catch (e: Throwable) { println("SOFT LOCK STATES #2 failed") } finally { countDown.countDown() 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 74c799fc57..9e484833d6 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 @@ -65,8 +65,8 @@ class VaultQueryTests : TestDependencyInjectionBase() { identitySvc.verifyAndRegisterIdentity(CASH_NOTARY_IDENTITY) identitySvc.verifyAndRegisterIdentity(BOC_IDENTITY) val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(MEGA_CORP_KEY, DUMMY_NOTARY_KEY), - createIdentityService = { identitySvc }, - customSchemas = setOf(CashSchemaV1, CommercialPaperSchemaV1, DummyLinearStateSchemaV1)) + createIdentityService = { identitySvc }, + customSchemas = setOf(CashSchemaV1, CommercialPaperSchemaV1, DummyLinearStateSchemaV1)) database = databaseAndServices.first services = databaseAndServices.second notaryServices = MockServices(DUMMY_NOTARY_KEY, DUMMY_CASH_ISSUER_KEY, BOC_KEY, MEGA_CORP_KEY) @@ -1064,7 +1064,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { @Suppress("EXPECTED_CONDITION") - val pagingSpec = PageSpecification(DEFAULT_PAGE_NUM, @Suppress("INTEGER_OVERFLOW")MAX_PAGE_SIZE + 1) // overflow = -2147483648 + val pagingSpec = PageSpecification(DEFAULT_PAGE_NUM, @Suppress("INTEGER_OVERFLOW") MAX_PAGE_SIZE + 1) // overflow = -2147483648 val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) vaultService.queryBy(criteria, paging = pagingSpec) } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index a1450132a5..60e853d023 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -46,7 +46,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { LogHelper.setLevel(VaultWithCashTest::class) val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(DUMMY_CASH_ISSUER_KEY, DUMMY_NOTARY_KEY), - customSchemas = setOf(CashSchemaV1)) + customSchemas = setOf(CashSchemaV1)) database = databaseAndServices.first services = databaseAndServices.second issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY, MEGA_CORP_KEY) @@ -85,24 +85,24 @@ class VaultWithCashTest : TestDependencyInjectionBase() { val freshKey = services.keyManagementService.freshKey() val usefulTX = - database.transaction { - // A tx that sends us money. - val usefulBuilder = TransactionBuilder(null) - Cash().generateIssue(usefulBuilder, 100.DOLLARS `issued by` MEGA_CORP.ref(1), AnonymousParty(freshKey), DUMMY_NOTARY) - megaCorpServices.signInitialTransaction(usefulBuilder) - } + database.transaction { + // A tx that sends us money. + val usefulBuilder = TransactionBuilder(null) + Cash().generateIssue(usefulBuilder, 100.DOLLARS `issued by` MEGA_CORP.ref(1), AnonymousParty(freshKey), DUMMY_NOTARY) + megaCorpServices.signInitialTransaction(usefulBuilder) + } database.transaction { assertEquals(0.DOLLARS, services.getCashBalance(USD)) services.recordTransactions(usefulTX) } val spendTX = - database.transaction { - // A tx that spends our money. - val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY) - Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB) - val spendPTX = services.signInitialTransaction(spendTXBuilder, freshKey) - notaryServices.addSignature(spendPTX) - } + database.transaction { + // A tx that spends our money. + val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY) + Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB) + val spendPTX = services.signInitialTransaction(spendTXBuilder, freshKey) + notaryServices.addSignature(spendPTX) + } database.transaction { assertEquals(100.DOLLARS, services.getCashBalance(USD)) } @@ -174,7 +174,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { LOCKED: ${lockedStates2.count()} : $lockedStates2 """) txn1 - } catch(e: Exception) { + } catch (e: Exception) { println(e) } } @@ -210,7 +210,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { LOCKED: ${lockedStates2.count()} : $lockedStates2 """) txn2 - } catch(e: Exception) { + } catch (e: Exception) { println(e) } } @@ -254,18 +254,19 @@ class VaultWithCashTest : TestDependencyInjectionBase() { val linearId = UniqueIdentifier() val dummyIssue = - database.transaction { // Issue a linear state - val dummyIssueBuilder = TransactionBuilder(notary = DUMMY_NOTARY) - .addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity)), DUMMY_LINEAR_CONTRACT_PROGRAM_ID) - .addCommand(dummyCommand(notaryServices.myInfo.chooseIdentity().owningKey)) - val dummyIssuePtx = notaryServices.signInitialTransaction(dummyIssueBuilder) - val dummyIssue = services.addSignature(dummyIssuePtx) + database.transaction { + // Issue a linear state + val dummyIssueBuilder = TransactionBuilder(notary = DUMMY_NOTARY) + .addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity)), DUMMY_LINEAR_CONTRACT_PROGRAM_ID) + .addCommand(dummyCommand(notaryServices.myInfo.chooseIdentity().owningKey)) + val dummyIssuePtx = notaryServices.signInitialTransaction(dummyIssueBuilder) + val dummyIssue = services.addSignature(dummyIssuePtx) - dummyIssue.toLedgerTransaction(services).verify() + dummyIssue.toLedgerTransaction(services).verify() - services.recordTransactions(dummyIssue) - dummyIssue - } + services.recordTransactions(dummyIssue) + dummyIssue + } database.transaction { assertThat(vaultService.queryBy().states).hasSize(1) @@ -333,9 +334,9 @@ class VaultWithCashTest : TestDependencyInjectionBase() { services.fillWithSomeTestDeals(listOf("123", "456", "789")) } val deals = - database.transaction { - vaultService.queryBy().states - } + database.transaction { + vaultService.queryBy().states + } database.transaction { services.fillWithSomeTestLinearStates(3) } diff --git a/node/src/test/kotlin/net/corda/node/utilities/AffinityExecutorTests.kt b/node/src/test/kotlin/net/corda/node/utilities/AffinityExecutorTests.kt index 4187645e33..b1994a3d0c 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/AffinityExecutorTests.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/AffinityExecutorTests.kt @@ -11,12 +11,14 @@ class AffinityExecutorTests { var _executor: AffinityExecutor.ServiceAffinityExecutor? = null val executor: AffinityExecutor.ServiceAffinityExecutor get() = _executor!! - @After fun shutdown() { + @After + fun shutdown() { _executor?.shutdown() _executor = null } - @Test fun `flush handles nested executes`() { + @Test + fun `flush handles nested executes`() { _executor = AffinityExecutor.ServiceAffinityExecutor("test4", 1) var nestedRan = false val latch = CountDownLatch(1) @@ -29,7 +31,8 @@ class AffinityExecutorTests { assertTrue(nestedRan) } - @Test fun `single threaded affinity executor runs on correct thread`() { + @Test + fun `single threaded affinity executor runs on correct thread`() { val thisThread = Thread.currentThread() _executor = AffinityExecutor.ServiceAffinityExecutor("test thread", 1) assertTrue(!executor.isOnThread) @@ -50,7 +53,8 @@ class AffinityExecutorTests { assertEquals(thread2.get(), thread.get()) } - @Test fun `pooled executor`() { + @Test + fun `pooled executor`() { _executor = AffinityExecutor.ServiceAffinityExecutor("test2", 3) assertFalse(executor.isOnThread) From f98942d7adcfec6c21761fd60d6421f9d26adb62 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Fri, 6 Oct 2017 15:27:39 +0100 Subject: [PATCH 126/185] Reformat files in `node-api` --- .../main/kotlin/net/corda/nodeapi/RPCApi.kt | 4 +- .../corda/nodeapi/config/ConfigUtilities.kt | 1 + .../internal/AttachmentsClassLoader.kt | 4 +- .../internal/serialization/ClientContexts.kt | 1 + .../serialization/CordaClassResolver.kt | 8 +- .../serialization/DefaultKryoCustomizer.kt | 2 + .../nodeapi/internal/serialization/Kryo.kt | 2 +- .../serialization/SerializationScheme.kt | 4 +- .../internal/serialization/ServerContexts.kt | 1 + .../serialization/UseCaseAwareness.kt | 2 +- .../serialization/amqp/ArraySerializer.kt | 15 ++-- .../amqp/CollectionSerializer.kt | 15 ++-- .../serialization/amqp/CustomSerializer.kt | 15 ++-- .../amqp/DeserializationInput.kt | 16 ++-- .../serialization/amqp/MapSerializer.kt | 23 +++-- .../serialization/amqp/ObjectSerializer.kt | 4 +- .../serialization/amqp/PropertySerializer.kt | 12 +-- .../serialization/amqp/SerializationOutput.kt | 5 +- .../amqp/custom/ThrowableSerializer.kt | 2 +- .../amqp/custom/ZonedDateTimeSerializer.kt | 2 + .../carpenter/AMQPSchemaExtensions.kt | 7 +- .../serialization/carpenter/Schema.kt | 8 +- .../amqp/ListsSerializationJavaTest.java | 5 +- ...tachmentsClassLoaderStaticContractTests.kt | 4 +- .../internal/AttachmentsClassLoaderTests.kt | 7 +- .../serialization/CordaClassResolverTests.kt | 5 ++ .../internal/serialization/KryoTests.kt | 10 +-- .../serialization/ListsSerializationTest.kt | 20 ++--- .../serialization/SerializationTokenTest.kt | 15 ++-- .../serialization/amqp/DeserializeMapTests.kt | 51 ++++++----- .../amqp/DeserializeSimpleTypesTests.kt | 30 +++++-- .../internal/serialization/amqp/EnumTests.kt | 40 ++++----- .../serialization/amqp/EvolvabilityTests.kt | 86 ++++++++++--------- .../amqp/SerializationOutputTests.kt | 25 +++--- .../amqp/SerializeAndReturnSchemaTest.kt | 1 + ...ticInitialisationOfSerializedObjectTest.kt | 16 ++-- 36 files changed, 256 insertions(+), 212 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt b/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt index d2de3777f6..aa062e3195 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt @@ -68,7 +68,7 @@ object RPCApi { val RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION = "${ManagementHelper.HDR_NOTIFICATION_TYPE} = '${CoreNotificationType.BINDING_REMOVED.name}' AND " + - "${ManagementHelper.HDR_ROUTING_NAME} LIKE '$RPC_CLIENT_QUEUE_NAME_PREFIX.%'" + "${ManagementHelper.HDR_ROUTING_NAME} LIKE '$RPC_CLIENT_QUEUE_NAME_PREFIX.%'" val RPC_CLIENT_BINDING_ADDITION_FILTER_EXPRESSION = "${ManagementHelper.HDR_NOTIFICATION_TYPE} = '${CoreNotificationType.BINDING_ADDED.name}' AND " + "${ManagementHelper.HDR_ROUTING_NAME} LIKE '$RPC_CLIENT_QUEUE_NAME_PREFIX.%'" @@ -141,7 +141,7 @@ object RPCApi { val ids = ArrayList() val buffer = message.bodyBuffer val numberOfIds = buffer.readInt() - for (i in 1 .. numberOfIds) { + for (i in 1..numberOfIds) { ids.add(ObservableId(buffer.readLong())) } ObservablesClosed(ids) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt index 78283ea654..a67a55db37 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt @@ -1,4 +1,5 @@ @file:JvmName("ConfigUtilities") + package net.corda.nodeapi.config import com.typesafe.config.Config diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoader.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoader.kt index 749c82b7ef..20d2efca63 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoader.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoader.kt @@ -72,7 +72,7 @@ class AttachmentsClassLoader(attachments: List, parent: ClassLoader val stream = ByteArrayOutputStream() try { attachment.extractFile(path, stream) - } catch(e: FileNotFoundException) { + } catch (e: FileNotFoundException) { throw ClassNotFoundException(name) } val bytes = stream.toByteArray() @@ -99,7 +99,7 @@ class AttachmentsClassLoader(attachments: List, parent: ClassLoader val stream = ByteArrayOutputStream() attachment.extractFile(path, stream) return ByteArrayInputStream(stream.toByteArray()) - } catch(e: FileNotFoundException) { + } catch (e: FileNotFoundException) { return null } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/ClientContexts.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/ClientContexts.kt index add6b1465e..bd6135522a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/ClientContexts.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/ClientContexts.kt @@ -1,4 +1,5 @@ @file:JvmName("ClientContexts") + package net.corda.nodeapi.internal.serialization import net.corda.core.serialization.SerializationContext diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt index afcdcbf0c8..04c43122ec 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt @@ -31,9 +31,9 @@ class CordaClassResolver(serializationContext: SerializationContext) : DefaultCl * The point is that we do not want to send Kotlin types "over the wire" via RPC. */ private val javaAliases: Map, Class<*>> = mapOf( - listOf().javaClass to Collections.emptyList().javaClass, - setOf().javaClass to Collections.emptySet().javaClass, - mapOf().javaClass to Collections.emptyMap().javaClass + listOf().javaClass to Collections.emptyList().javaClass, + setOf().javaClass to Collections.emptySet().javaClass, + mapOf().javaClass to Collections.emptyMap().javaClass ) private fun typeForSerializationOf(type: Class<*>): Class<*> = javaAliases[type] ?: type @@ -194,7 +194,7 @@ class LoggingWhitelist(val delegate: ClassWhitelist, val global: Boolean = true) if (fileName != null && fileName.isNotEmpty()) { try { return PrintWriter(Files.newBufferedWriter(Paths.get(fileName), StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND, StandardOpenOption.WRITE), true) - } catch(ioEx: Exception) { + } catch (ioEx: Exception) { log.error("Could not open/create whitelist journal file for append: $fileName", ioEx) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt index 04104533ee..7532ecffe9 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt @@ -140,6 +140,7 @@ object DefaultKryoCustomizer { // Use this to allow construction of objects using a JVM backdoor that skips invoking the constructors, if there // is no no-arg constructor available. private val defaultStrategy = Kryo.DefaultInstantiatorStrategy(fallbackStrategy) + override fun newInstantiatorOf(type: Class): ObjectInstantiator { // However this doesn't work for non-public classes in the java. namespace val strat = if (type.name.startsWith("java.") && !isPublic(type.modifiers)) fallbackStrategy else defaultStrategy @@ -151,6 +152,7 @@ object DefaultKryoCustomizer { override fun write(kryo: Kryo, output: Output, obj: PartyAndCertificate) { kryo.writeClassAndObject(output, obj.certPath) } + override fun read(kryo: Kryo, input: Input, type: Class): PartyAndCertificate { return PartyAndCertificate(kryo.readClassAndObject(input) as CertPath) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt index 7b998d5cd7..b4d0dc9c8c 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt @@ -546,7 +546,7 @@ class ThrowableSerializer(kryo: Kryo, type: Class) : Serializer override fun read(kryo: Kryo, input: Input, type: Class): Throwable { val throwableRead = delegate.read(kryo, input, type) - if(throwableRead.suppressed.isEmpty()) { + if (throwableRead.suppressed.isEmpty()) { throwableRead.setSuppressedToSentinel() } return throwableRead diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt index ca6c4c5292..e4deaf2c55 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt @@ -142,8 +142,8 @@ open class SerializationFactoryImpl : SerializationFactory() { private object AutoCloseableSerialisationDetector : Serializer() { override fun write(kryo: Kryo, output: Output, closeable: AutoCloseable) { val message = "${closeable.javaClass.name}, which is a closeable resource, has been detected during flow checkpointing. " + - "Restoring such resources across node restarts is not supported. Make sure code accessing it is " + - "confined to a private method or the reference is nulled out." + "Restoring such resources across node restarts is not supported. Make sure code accessing it is " + + "confined to a private method or the reference is nulled out." throw UnsupportedOperationException(message) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/ServerContexts.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/ServerContexts.kt index 89dd6b524d..baab649669 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/ServerContexts.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/ServerContexts.kt @@ -1,4 +1,5 @@ @file:JvmName("ServerContexts") + package net.corda.nodeapi.internal.serialization import net.corda.core.serialization.ClassWhitelist diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/UseCaseAwareness.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/UseCaseAwareness.kt index fdf57d28be..7f1d8c17b4 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/UseCaseAwareness.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/UseCaseAwareness.kt @@ -6,7 +6,7 @@ import java.util.* internal fun checkUseCase(allowedUseCases: EnumSet) { val currentContext: SerializationContext = SerializationFactory.currentFactory?.currentContext ?: throw IllegalStateException("Current context is not set") - if(!allowedUseCases.contains(currentContext.useCase)) { + if (!allowedUseCases.contains(currentContext.useCase)) { throw IllegalStateException("UseCase '${currentContext.useCase}' is not within '$allowedUseCases'") } } \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt index e49eafaa96..10f320a55a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt @@ -23,14 +23,13 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory) // // We *need* to retain knowledge for AMQP deserialisation weather that lowest primitive // was boxed or unboxed so just infer it recursively - private fun calcTypeName(type: Type) : String = - if (type.componentType().isArray()) { - val typeName = calcTypeName(type.componentType()); "$typeName[]" - } - else { - val arrayType = if (type.asClass()!!.componentType.isPrimitive) "[p]" else "[]" - "${type.componentType().typeName}$arrayType" - } + private fun calcTypeName(type: Type): String = + if (type.componentType().isArray()) { + val typeName = calcTypeName(type.componentType()); "$typeName[]" + } else { + val arrayType = if (type.asClass()!!.componentType.isPrimitive) "[p]" else "[]" + "${type.componentType().typeName}$arrayType" + } override val typeDescriptor by lazy { Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}") } internal val elementType: Type by lazy { type.componentType() } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt index 43a0234f95..fc176cfda7 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt @@ -35,11 +35,10 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali } fun deriveParameterizedType(declaredType: Type, declaredClass: Class<*>, actualClass: Class<*>?): ParameterizedType { - if(supportedTypes.containsKey(declaredClass)) { + if (supportedTypes.containsKey(declaredClass)) { // Simple case - it is already known to be a collection. return deriveParametrizedType(declaredType, uncheckedCast(declaredClass)) - } - else if (actualClass != null && Collection::class.java.isAssignableFrom(actualClass)) { + } else if (actualClass != null && Collection::class.java.isAssignableFrom(actualClass)) { // Declared class is not collection, but [actualClass] is - represent it accordingly. val collectionClass = findMostSuitableCollectionType(actualClass) return deriveParametrizedType(declaredType, collectionClass) @@ -49,11 +48,11 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali } private fun deriveParametrizedType(declaredType: Type, collectionClass: Class>): ParameterizedType = - (declaredType as? ParameterizedType) ?: DeserializedParameterizedType(collectionClass, arrayOf(SerializerFactory.AnyType)) + (declaredType as? ParameterizedType) ?: DeserializedParameterizedType(collectionClass, arrayOf(SerializerFactory.AnyType)) private fun findMostSuitableCollectionType(actualClass: Class<*>): Class> = - supportedTypes.keys.findLast { it.isAssignableFrom(actualClass) }!! + supportedTypes.keys.findLast { it.isAssignableFrom(actualClass) }!! } @@ -61,13 +60,13 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "list", Descriptor(typeDescriptor), emptyList()) - override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({declaredType.typeName}) { + override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({ declaredType.typeName }) { if (output.writeTypeNotations(typeNotation)) { output.requireSerializer(declaredType.actualTypeArguments[0]) } } - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) = ifThrowsAppend({declaredType.typeName}) { + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) = ifThrowsAppend({ declaredType.typeName }) { // Write described data.withDescribed(typeNotation.descriptor) { withList { @@ -78,7 +77,7 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali } } - override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({declaredType.typeName}) { + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({ declaredType.typeName }) { // TODO: Can we verify the entries in the list? concreteBuilder((obj as List<*>).map { input.readObjectOrNull(it, schema, declaredType.actualTypeArguments[0]) }) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt index 242f829511..e039911ec0 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt @@ -102,9 +102,9 @@ abstract class CustomSerializer : AMQPSerializer { * custom serializers. */ abstract class Proxy(clazz: Class, - protected val proxyClass: Class

, - protected val factory: SerializerFactory, - withInheritance: Boolean = true) : CustomSerializerImp(clazz, withInheritance) { + protected val proxyClass: Class

, + protected val factory: SerializerFactory, + withInheritance: Boolean = true) : CustomSerializerImp(clazz, withInheritance) { override fun isSerializerFor(clazz: Class<*>): Boolean = if (withInheritance) this.clazz.isAssignableFrom(clazz) else this.clazz == clazz private val proxySerializer: ObjectSerializer by lazy { ObjectSerializer(proxyClass, factory) } @@ -151,11 +151,10 @@ abstract class CustomSerializer : AMQPSerializer { * @param unmake A lambda that extracts the string value for an instance, that defaults to the [toString] method. */ abstract class ToString(clazz: Class, withInheritance: Boolean = false, - private val maker: (String) -> T = clazz.getConstructor(String::class.java).let { - `constructor` -> - { string -> `constructor`.newInstance(string) } - }, - private val unmaker: (T) -> String = { obj -> obj.toString() }) + private val maker: (String) -> T = clazz.getConstructor(String::class.java).let { `constructor` -> + { string -> `constructor`.newInstance(string) } + }, + private val unmaker: (T) -> String = { obj -> obj.toString() }) : CustomSerializerImp(clazz, withInheritance) { override val schemaForDocumentation = Schema( diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt index 648f11eeae..7269aa8c40 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt @@ -80,9 +80,9 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) { private fun des(generator: () -> R): R { try { return generator() - } catch(nse: NotSerializableException) { + } catch (nse: NotSerializableException) { throw nse - } catch(t: Throwable) { + } catch (t: Throwable) { throw NotSerializableException("Unexpected throwable: ${t.message} ${t.getStackTraceAsString()}") } finally { objectHistory.clear() @@ -152,10 +152,10 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) { * In the future tighter control might be needed */ private fun Type.materiallyEquivalentTo(that: Type): Boolean = - when(that) { - is ParameterizedType -> asClass() == that.asClass() - is TypeVariable<*> -> isSubClassOf(that.bounds.first()) - is WildcardType -> isSubClassOf(that.upperBounds.first()) - else -> false - } + when (that) { + is ParameterizedType -> asClass() == that.asClass() + is TypeVariable<*> -> isSubClassOf(that.bounds.first()) + is WildcardType -> isSubClassOf(that.upperBounds.first()) + else -> false + } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt index 1fa710d59a..932b641ad2 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt @@ -42,11 +42,10 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial fun deriveParameterizedType(declaredType: Type, declaredClass: Class<*>, actualClass: Class<*>?): ParameterizedType { declaredClass.checkSupportedMapType() - if(supportedTypes.containsKey(declaredClass)) { + if (supportedTypes.containsKey(declaredClass)) { // Simple case - it is already known to be a map. return deriveParametrizedType(declaredType, uncheckedCast(declaredClass)) - } - else if (actualClass != null && Map::class.java.isAssignableFrom(actualClass)) { + } else if (actualClass != null && Map::class.java.isAssignableFrom(actualClass)) { // Declared class is not map, but [actualClass] is - represent it accordingly. val mapClass = findMostSuitableMapType(actualClass) return deriveParametrizedType(declaredType, mapClass) @@ -67,14 +66,14 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "map", Descriptor(typeDescriptor), emptyList()) - override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({declaredType.typeName}) { + override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({ declaredType.typeName }) { if (output.writeTypeNotations(typeNotation)) { output.requireSerializer(declaredType.actualTypeArguments[0]) output.requireSerializer(declaredType.actualTypeArguments[1]) } } - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) = ifThrowsAppend({declaredType.typeName}) { + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) = ifThrowsAppend({ declaredType.typeName }) { obj.javaClass.checkSupportedMapType() // Write described data.withDescribed(typeNotation.descriptor) { @@ -89,7 +88,7 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial } } - override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({declaredType.typeName}) { + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({ declaredType.typeName }) { // TODO: General generics question. Do we need to validate that entries in Maps and Collections match the generic type? Is it a security hole? val entries: Iterable> = (obj as Map<*, *>).map { readEntry(schema, input, it) } concreteBuilder(entries.toMap()) @@ -108,13 +107,11 @@ internal fun Class<*>.checkSupportedMapType() { if (HashMap::class.java.isAssignableFrom(this) && !LinkedHashMap::class.java.isAssignableFrom(this)) { throw IllegalArgumentException( "Map type $this is unstable under iteration. Suggested fix: use java.util.LinkedHashMap instead.") - } - else if (WeakHashMap::class.java.isAssignableFrom(this)) { - throw IllegalArgumentException ("Weak references with map types not supported. Suggested fix: " - + "use java.util.LinkedHashMap instead.") - } - else if (Dictionary::class.java.isAssignableFrom(this)) { - throw IllegalArgumentException ( + } else if (WeakHashMap::class.java.isAssignableFrom(this)) { + throw IllegalArgumentException("Weak references with map types not supported. Suggested fix: " + + "use java.util.LinkedHashMap instead.") + } else if (Dictionary::class.java.isAssignableFrom(this)) { + throw IllegalArgumentException( "Unable to serialise deprecated type $this. Suggested fix: prefer java.util.map implementations") } } \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt index c483e8e126..7dcb450803 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt @@ -41,7 +41,7 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS } } - override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) = ifThrowsAppend({clazz.typeName}) { + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) = ifThrowsAppend({ clazz.typeName }) { // Write described data.withDescribed(typeNotation.descriptor) { // Write list @@ -53,7 +53,7 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS } } - override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({clazz.typeName}) { + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({ clazz.typeName }) { if (obj is List<*>) { if (obj.size > propertySerializers.size) throw NotSerializableException("Too many properties in described type $typeName") val params = obj.zip(propertySerializers).map { it.second.readProperty(it.first, schema, input) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt index 7c7714b9e9..feb06e123e 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt @@ -52,7 +52,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r try { val returnTypeString = this.declaringClass.kotlin.memberProperties.firstOrNull { it.javaGetter == this }?.returnType?.toString() ?: "?" return returnTypeString.endsWith('?') || returnTypeString.endsWith('!') - } catch(e: kotlin.reflect.jvm.internal.KotlinReflectionInternalError) { + } catch (e: kotlin.reflect.jvm.internal.KotlinReflectionInternalError) { // This might happen for some types, e.g. kotlin.Throwable? - the root cause of the issue is: https://youtrack.jetbrains.com/issue/KT-13077 // TODO: Revisit this when Kotlin issue is fixed. logger.error("Unexpected internal Kotlin error", e) @@ -66,7 +66,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r fun make(name: String, readMethod: Method?, resolvedType: Type, factory: SerializerFactory): PropertySerializer { readMethod?.isAccessible = true if (SerializerFactory.isPrimitive(resolvedType)) { - return when(resolvedType) { + return when (resolvedType) { Char::class.java, Character::class.java -> AMQPCharPropertySerializer(name, readMethod) else -> AMQPPrimitivePropertySerializer(name, readMethod, resolvedType) } @@ -86,17 +86,17 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r // This is lazy so we don't get an infinite loop when a method returns an instance of the class. private val typeSerializer: AMQPSerializer<*> by lazy { lazyTypeSerializer() } - override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({nameForDebug}) { + override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({ nameForDebug }) { if (resolvedType != Any::class.java) { typeSerializer.writeClassInfo(output) } } - override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? = ifThrowsAppend({nameForDebug}) { + override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? = ifThrowsAppend({ nameForDebug }) { input.readObjectOrNull(obj, schema, resolvedType) } - override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) = ifThrowsAppend({nameForDebug}) { + override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) = ifThrowsAppend({ nameForDebug }) { output.writeObjectOrNull(readMethod!!.invoke(obj), data, resolvedType) } @@ -133,7 +133,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r override fun writeClassInfo(output: SerializationOutput) {} override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? { - return if(obj == null) null else (obj as Short).toChar() + return if (obj == null) null else (obj as Short).toChar() } override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt index 5379b5ab84..87a19376c9 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt @@ -82,14 +82,13 @@ open class SerializationOutput(internal val serializerFactory: SerializerFactory } val retrievedRefCount = objectHistory[obj] - if(retrievedRefCount == null) { + if (retrievedRefCount == null) { serializer.writeObject(obj, data, type, this) // Important to do it after serialization such that dependent object will have preceding reference numbers // assigned to them first as they will be first read from the stream on receiving end. // Skip for primitive types as they are too small and overhead of referencing them will be much higher than their content if (suitableForObjectReference(obj.javaClass)) objectHistory.put(obj, objectHistory.size) - } - else { + } else { data.writeReferencedObject(ReferencedObject(retrievedRefCount)) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ThrowableSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ThrowableSerializer.kt index ad8394db2d..8697cd98c7 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ThrowableSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ThrowableSerializer.kt @@ -27,7 +27,7 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy> = listOf(LocalDateTimeSerializer(factory), ZoneIdSerializer(factory)) override fun toProxy(obj: ZonedDateTime): ZonedDateTimeProxy = ZonedDateTimeProxy(obj.toLocalDateTime(), obj.offset, obj.zone) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt index 611dbb9788..0bbdf01033 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt @@ -132,5 +132,10 @@ fun AMQPField.validateType(classloader: ClassLoader) = when (type) { } private fun ClassLoader.exists(clazz: String) = run { - try { this.loadClass(clazz); true } catch (e: ClassNotFoundException) { false } } + try { + this.loadClass(clazz); true + } catch (e: ClassNotFoundException) { + false + } +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Schema.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Schema.kt index e3a3bfab56..352ad498da 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Schema.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/Schema.kt @@ -24,7 +24,7 @@ abstract class Schema( updater: (String, Field) -> Unit) { private fun Map.descriptors() = LinkedHashMap(this.mapValues { it.value.descriptor }) - var flags : EnumMap = EnumMap(SchemaFlags::class.java) + var flags: EnumMap = EnumMap(SchemaFlags::class.java) init { fields.forEach { updater(it.key, it.value) } @@ -49,15 +49,15 @@ abstract class Schema( get() = "[L$jvmName;" fun unsetCordaSerializable() { - flags.replace (SchemaFlags.CordaSerializable, false) + flags.replace(SchemaFlags.CordaSerializable, false) } } -fun EnumMap.cordaSerializable() : Boolean { +fun EnumMap.cordaSerializable(): Boolean { return this.getOrDefault(SchemaFlags.CordaSerializable, true) == true } -fun EnumMap.simpleFieldAccess() : Boolean { +fun EnumMap.simpleFieldAccess(): Boolean { return this.getOrDefault(SchemaFlags.SimpleFieldAccess, true) == true } diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ListsSerializationJavaTest.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ListsSerializationJavaTest.java index bcd1d1f1f7..6085546c37 100644 --- a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ListsSerializationJavaTest.java +++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ListsSerializationJavaTest.java @@ -12,7 +12,8 @@ import java.util.List; public class ListsSerializationJavaTest { @CordaSerializable - interface Parent {} + interface Parent { + } public static class Child implements Parent { private final int value; @@ -123,7 +124,7 @@ public class ListsSerializationJavaTest { } // Have to have own version as Kotlin inline functions cannot be easily called from Java - private static void assertEqualAfterRoundTripSerialization(T container, Class clazz) throws Exception { + private static void assertEqualAfterRoundTripSerialization(T container, Class clazz) throws Exception { SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader()); SerializationOutput ser = new SerializationOutput(factory1); SerializedBytes bytes = ser.serialize(container); 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 77733ec060..ce95ac21d8 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 @@ -37,7 +37,7 @@ class AttachmentsClassLoaderStaticContractTests : TestDependencyInjectionBase() fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder { val state = State(magicNumber) return TransactionBuilder(notary) - .withItems(StateAndContract(state, ATTACHMENT_PROGRAM_ID), Command(Commands.Create(), owner.party.owningKey)) + .withItems(StateAndContract(state, ATTACHMENT_PROGRAM_ID), Command(Commands.Create(), owner.party.owningKey)) } } @@ -45,7 +45,7 @@ class AttachmentsClassLoaderStaticContractTests : TestDependencyInjectionBase() @Before fun `create service hub`() { - serviceHub = MockServices(cordappPackages=listOf("net.corda.nodeapi.internal")) + serviceHub = MockServices(cordappPackages = listOf("net.corda.nodeapi.internal")) } @After diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt index e51a974936..bf4138379d 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt @@ -45,6 +45,7 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() { whenever(serviceHub.attachments).thenReturn(attachmentStorage) return this.withServiceHub(serviceHub) } + private fun SerializationContext.withServiceHub(serviceHub: ServiceHub): SerializationContext { return this.withTokenContext(SerializeAsTokenContextImpl(serviceHub) {}).withProperty(attachmentsClassLoaderEnabledPropertyName, true) } @@ -266,7 +267,7 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() { @Test fun `test serialization of sub-sequence OpaqueBytes`() { - val bytesSequence = ByteSequence.of("0123456789".toByteArray(), 3 ,2) + val bytesSequence = ByteSequence.of("0123456789".toByteArray(), 3, 2) val bytes = bytesSequence.serialize() val copiedBytesSequence = bytes.deserialize() @@ -310,8 +311,8 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() { val attachmentRef = serviceHub.attachmentId val bytes = run { val outboundContext = SerializationFactory.defaultFactory.defaultContext - .withServiceHub(serviceHub) - .withClassLoader(child) + .withServiceHub(serviceHub) + .withClassLoader(child) val wireTransaction = tx.toWireTransaction(serviceHub, outboundContext) wireTransaction.serialize(context = outboundContext) } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt index 3a67501191..74a3d574ae 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt @@ -271,6 +271,7 @@ class CordaClassResolverTests { } open class SubHashSet : HashSet() + @Test fun `Check blacklisted subclass`() { expectedEx.expect(IllegalStateException::class.java) @@ -281,6 +282,7 @@ class CordaClassResolverTests { } class SubSubHashSet : SubHashSet() + @Test fun `Check blacklisted subsubclass`() { expectedEx.expect(IllegalStateException::class.java) @@ -291,6 +293,7 @@ class CordaClassResolverTests { } class ConnectionImpl(private val connection: Connection) : Connection by connection + @Test fun `Check blacklisted interface impl`() { expectedEx.expect(IllegalStateException::class.java) @@ -302,6 +305,7 @@ class CordaClassResolverTests { interface SubConnection : Connection class SubConnectionImpl(private val subConnection: SubConnection) : SubConnection by subConnection + @Test fun `Check blacklisted super-interface impl`() { expectedEx.expect(IllegalStateException::class.java) @@ -320,6 +324,7 @@ class CordaClassResolverTests { @CordaSerializable class CordaSerializableHashSet : HashSet() + @Test fun `Check blacklist precedes CordaSerializable`() { expectedEx.expect(IllegalStateException::class.java) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt index bf9c157c86..d6cb313f88 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt @@ -188,10 +188,10 @@ class KryoTests : TestDependencyInjectionBase() { @Test fun `serialize - deserialize PrivacySalt`() { val expected = PrivacySalt(byteArrayOf( - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, - 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, - 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, - 31, 32 + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, 32 )) val serializedBytes = expected.serialize(factory, context) val actual = serializedBytes.deserialize(factory, context) @@ -267,7 +267,7 @@ class KryoTests : TestDependencyInjectionBase() { assertEquals(exception.message, exception2.message) assertEquals(1, exception2.suppressed.size) - assertNotNull({ exception2.suppressed.find { it.message == toBeSuppressedOnSenderSide.message }}) + assertNotNull({ exception2.suppressed.find { it.message == toBeSuppressedOnSenderSide.message } }) val toBeSuppressedOnReceiverSide = IllegalStateException("bazz2") exception2.addSuppressed(toBeSuppressedOnReceiverSide) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt index f8d281392b..ede793b2be 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt @@ -22,12 +22,12 @@ class ListsSerializationTest : TestDependencyInjectionBase() { private companion object { val javaEmptyListClass = Collections.emptyList().javaClass - fun verifyEnvelope(serBytes: SerializedBytes, envVerBody: (Envelope) -> Unit) = - amqpSpecific("AMQP specific envelope verification") { - val context = SerializationFactory.defaultFactory.defaultContext - val envelope = DeserializationInput(SerializerFactory(context.whitelist, context.deserializationClassLoader)).getEnvelope(serBytes) - envVerBody(envelope) - } + fun verifyEnvelope(serBytes: SerializedBytes, envVerBody: (Envelope) -> Unit) = + amqpSpecific("AMQP specific envelope verification") { + val context = SerializationFactory.defaultFactory.defaultContext + val envelope = DeserializationInput(SerializerFactory(context.whitelist, context.deserializationClassLoader)).getEnvelope(serBytes) + envVerBody(envelope) + } } @Test @@ -54,7 +54,7 @@ class ListsSerializationTest : TestDependencyInjectionBase() { } @Test - fun `check empty list serialises as Java emptyList`() = kryoSpecific("Kryo specific test"){ + fun `check empty list serialises as Java emptyList`() = kryoSpecific("Kryo specific test") { val nameID = 0 val serializedForm = emptyList().serialize() val output = ByteArrayOutputStream().apply { @@ -86,7 +86,7 @@ class ListsSerializationTest : TestDependencyInjectionBase() { data class Child(val value: Int) : Parent @CordaSerializable - data class CovariantContainer(val payload: List) + data class CovariantContainer(val payload: List) @Test fun `check covariance`() { @@ -99,11 +99,11 @@ class ListsSerializationTest : TestDependencyInjectionBase() { envelope.schema.types.single { typeNotation -> typeNotation.name == java.util.List::class.java.name + "" } } - assertEqualAfterRoundTripSerialization(container, {bytes -> verifyEnvelope(bytes, ::verifyEnvelopeBody)}) + assertEqualAfterRoundTripSerialization(container, { bytes -> verifyEnvelope(bytes, ::verifyEnvelopeBody) }) } } -internal inline fun assertEqualAfterRoundTripSerialization(obj: T, noinline streamValidation: ((SerializedBytes) -> Unit)? = null) { +internal inline fun assertEqualAfterRoundTripSerialization(obj: T, noinline streamValidation: ((SerializedBytes) -> Unit)? = null) { val serializedForm: SerializedBytes = obj.serialize() streamValidation?.invoke(serializedForm) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt index 2c8a337035..909f672fde 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt @@ -12,7 +12,7 @@ import org.junit.Before import org.junit.Test import java.io.ByteArrayOutputStream -class SerializationTokenTest : TestDependencyInjectionBase() { +class SerializationTokenTest : TestDependencyInjectionBase() { private lateinit var factory: SerializationFactory private lateinit var context: SerializationContext @@ -58,7 +58,7 @@ class SerializationTokenTest : TestDependencyInjectionBase() { val testContext = this.context.withTokenContext(context) val serializedBytes = tokenizableBefore.serialize(factory, testContext) val tokenizableAfter = serializedBytes.deserialize(factory, testContext) - assertThat(tokenizableAfter).isSameAs(tokenizableBefore) + assertThat(tokenizableAfter).isSameAs(tokenizableBefore) } @Test(expected = UnsupportedOperationException::class) @@ -92,11 +92,11 @@ class SerializationTokenTest : TestDependencyInjectionBase() { val kryo: Kryo = DefaultKryoCustomizer.customize(CordaKryo(CordaClassResolver(this.context))) val stream = ByteArrayOutputStream() - Output(stream).use { - it.write(KryoHeaderV0_1.bytes) - kryo.writeClass(it, SingletonSerializeAsToken::class.java) - kryo.writeObject(it, emptyList()) - } + Output(stream).use { + it.write(KryoHeaderV0_1.bytes) + kryo.writeClass(it, SingletonSerializeAsToken::class.java) + kryo.writeObject(it, emptyList()) + } val serializedBytes = SerializedBytes(stream.toByteArray()) serializedBytes.deserialize(factory, testContext) } @@ -105,6 +105,7 @@ class SerializationTokenTest : TestDependencyInjectionBase() { object UnitSerializationToken : SerializationToken { override fun fromToken(context: SerializeAsTokenContext): Any = UnitSerializeAsToken() } + override fun toToken(context: SerializeAsTokenContext): SerializationToken = UnitSerializationToken } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt index 8314286d12..68e106fd89 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt @@ -17,25 +17,28 @@ class DeserializeMapTests { @Test fun mapTest() { data class C(val c: Map) - val c = C (mapOf("A" to 1, "B" to 2)) + + val c = C(mapOf("A" to 1, "B" to 2)) val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) DeserializationInput(sf).deserialize(serialisedBytes) } - @Test(expected=java.io.NotSerializableException::class) + @Test(expected = java.io.NotSerializableException::class) fun abstractMapFromMapOf() { data class C(val c: AbstractMap) - val c = C (mapOf("A" to 1, "B" to 2) as AbstractMap) + + val c = C(mapOf("A" to 1, "B" to 2) as AbstractMap) val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) DeserializationInput(sf).deserialize(serialisedBytes) } - @Test(expected=java.io.NotSerializableException::class) + @Test(expected = java.io.NotSerializableException::class) fun abstractMapFromTreeMap() { data class C(val c: AbstractMap) - val c = C (TreeMap(mapOf("A" to 1, "B" to 2))) + + val c = C(TreeMap(mapOf("A" to 1, "B" to 2))) val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) DeserializationInput(sf).deserialize(serialisedBytes) @@ -44,7 +47,8 @@ class DeserializeMapTests { @Test fun sortedMapTest() { data class C(val c: SortedMap) - val c = C(sortedMapOf ("A" to 1, "B" to 2)) + + val c = C(sortedMapOf("A" to 1, "B" to 2)) val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) DeserializationInput(sf).deserialize(serialisedBytes) } @@ -52,7 +56,8 @@ class DeserializeMapTests { @Test fun navigableMapTest() { data class C(val c: NavigableMap) - val c = C(TreeMap (mapOf("A" to 1, "B" to 2)).descendingMap()) + + val c = C(TreeMap(mapOf("A" to 1, "B" to 2)).descendingMap()) val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) DeserializationInput(sf).deserialize(serialisedBytes) @@ -61,9 +66,10 @@ class DeserializeMapTests { @Test fun dictionaryTest() { data class C(val c: Dictionary) - val v : Hashtable = Hashtable() - v.put ("a", 1) - v.put ("b", 2) + + val v: Hashtable = Hashtable() + v.put("a", 1) + v.put("b", 2) val c = C(v) // expected to throw @@ -74,9 +80,10 @@ class DeserializeMapTests { @Test fun hashtableTest() { data class C(val c: Hashtable) - val v : Hashtable = Hashtable() - v.put ("a", 1) - v.put ("b", 2) + + val v: Hashtable = Hashtable() + v.put("a", 1) + v.put("b", 2) val c = C(v) // expected to throw @@ -86,8 +93,9 @@ class DeserializeMapTests { @Test fun hashMapTest() { - data class C(val c : HashMap) - val c = C (HashMap (mapOf("A" to 1, "B" to 2))) + data class C(val c: HashMap) + + val c = C(HashMap(mapOf("A" to 1, "B" to 2))) // expect this to throw Assertions.assertThatThrownBy { TestSerializationOutput(VERBOSE, sf).serialize(c) } @@ -96,8 +104,9 @@ class DeserializeMapTests { @Test fun weakHashMapTest() { - data class C(val c : WeakHashMap) - val c = C (WeakHashMap (mapOf("A" to 1, "B" to 2))) + data class C(val c: WeakHashMap) + + val c = C(WeakHashMap(mapOf("A" to 1, "B" to 2))) Assertions.assertThatThrownBy { TestSerializationOutput(VERBOSE, sf).serialize(c) } .isInstanceOf(IllegalArgumentException::class.java).hasMessageContaining("Weak references with map types not supported. Suggested fix: use java.util.LinkedHashMap instead.") @@ -106,7 +115,8 @@ class DeserializeMapTests { @Test fun concreteTreeMapTest() { data class C(val c: TreeMap) - val c = C(TreeMap (mapOf("A" to 1, "B" to 3))) + + val c = C(TreeMap(mapOf("A" to 1, "B" to 3))) val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) DeserializationInput(sf).deserialize(serialisedBytes) @@ -114,8 +124,9 @@ class DeserializeMapTests { @Test fun concreteLinkedHashMapTest() { - data class C(val c : LinkedHashMap) - val c = C (LinkedHashMap (mapOf("A" to 1, "B" to 2))) + data class C(val c: LinkedHashMap) + + val c = C(LinkedHashMap(mapOf("A" to 1, "B" to 2))) val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) DeserializationInput(sf).deserialize(serialisedBytes) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeSimpleTypesTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeSimpleTypesTests.kt index cc05b9b39f..4c5c9311ec 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeSimpleTypesTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeSimpleTypesTests.kt @@ -402,7 +402,8 @@ class DeserializeSimpleTypesTests { @Test fun arrayOfArrayOfInt() { class C(val c: Array>) - val c = C (arrayOf (arrayOf(1,2,3), arrayOf(4,5,6))) + + val c = C(arrayOf(arrayOf(1, 2, 3), arrayOf(4, 5, 6))) val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c) val deserializedC = DeserializationInput(sf1).deserialize(serialisedC) @@ -421,7 +422,8 @@ class DeserializeSimpleTypesTests { @Test fun arrayOfIntArray() { class C(val c: Array) - val c = C (arrayOf (IntArray(3), IntArray(3))) + + val c = C(arrayOf(IntArray(3), IntArray(3))) c.c[0][0] = 1; c.c[0][1] = 2; c.c[0][2] = 3 c.c[1][0] = 4; c.c[1][1] = 5; c.c[1][2] = 6 @@ -444,22 +446,32 @@ class DeserializeSimpleTypesTests { class C(val c: Array>) val c = C(arrayOf(arrayOf(IntArray(3), IntArray(3), IntArray(3)), - arrayOf(IntArray(3), IntArray(3), IntArray(3)), - arrayOf(IntArray(3), IntArray(3), IntArray(3)))) + arrayOf(IntArray(3), IntArray(3), IntArray(3)), + arrayOf(IntArray(3), IntArray(3), IntArray(3)))) - for (i in 0..2) { for (j in 0..2) { for (k in 0..2) { c.c[i][j][k] = i + j + k } } } + for (i in 0..2) { + for (j in 0..2) { + for (k in 0..2) { + c.c[i][j][k] = i + j + k + } + } + } val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c) val deserializedC = DeserializationInput(sf1).deserialize(serialisedC) - for (i in 0..2) { for (j in 0..2) { for (k in 0..2) { - assertEquals(c.c[i][j][k], deserializedC.c[i][j][k]) - }}} + for (i in 0..2) { + for (j in 0..2) { + for (k in 0..2) { + assertEquals(c.c[i][j][k], deserializedC.c[i][j][k]) + } + } + } } @Test fun nestedRepeatedTypes() { - class A(val a : A?, val b: Int) + class A(val a: A?, val b: Int) var a = A(A(A(A(A(null, 1), 2), 3), 4), 5) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumTests.kt index d10152baf0..1f20ac013f 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumTests.kt @@ -49,7 +49,7 @@ class EnumTests { } - enum class BrasWithInit (val someList: List) { + enum class BrasWithInit(val someList: List) { TSHIRT(emptyList()), UNDERWIRE(listOf(1, 2, 3)), PUSHUP(listOf(100, 200)), @@ -90,7 +90,7 @@ class EnumTests { assertEquals(8, schema_bras.choices.size) Bras.values().forEach { val bra = it - assertNotNull (schema_bras.choices.find { it.name == bra.name }) + assertNotNull(schema_bras.choices.find { it.name == bra.name }) } } @@ -115,7 +115,7 @@ class EnumTests { assertEquals(8, schema_bras.choices.size) Bras.values().forEach { val bra = it - assertNotNull (schema_bras.choices.find { it.name == bra.name }) + assertNotNull(schema_bras.choices.find { it.name == bra.name }) } // Test the actual deserialised object @@ -124,13 +124,13 @@ class EnumTests { @Test fun multiEnum() { - data class Support (val top: Bras, val day : DayOfWeek) - data class WeeklySupport (val tops: List) + data class Support(val top: Bras, val day: DayOfWeek) + data class WeeklySupport(val tops: List) - val week = WeeklySupport (listOf( - Support (Bras.PUSHUP, DayOfWeek.MONDAY), - Support (Bras.UNDERWIRE, DayOfWeek.WEDNESDAY), - Support (Bras.PADDED, DayOfWeek.SUNDAY))) + val week = WeeklySupport(listOf( + Support(Bras.PUSHUP, DayOfWeek.MONDAY), + Support(Bras.UNDERWIRE, DayOfWeek.WEDNESDAY), + Support(Bras.PADDED, DayOfWeek.SUNDAY))) val obj = DeserializationInput(sf1).deserialize(TestSerializationOutput(VERBOSE, sf1).serialize(week)) @@ -146,7 +146,7 @@ class EnumTests { fun enumWithInit() { data class C(val c: BrasWithInit) - val c = C (BrasWithInit.PUSHUP) + val c = C(BrasWithInit.PUSHUP) val obj = DeserializationInput(sf1).deserialize(TestSerializationOutput(VERBOSE, sf1).serialize(c)) assertEquals(c.c, obj.c) @@ -157,7 +157,7 @@ class EnumTests { val path = EnumTests::class.java.getResource("EnumTests.changedEnum1") val f = File(path.toURI()) - data class C (val a: OldBras) + data class C(val a: OldBras) // Original version of the class for the serialised version of this class // @@ -177,7 +177,7 @@ class EnumTests { val path = EnumTests::class.java.getResource("EnumTests.changedEnum2") val f = File(path.toURI()) - data class C (val a: OldBras2) + data class C(val a: OldBras2) // DO NOT CHANGE THIS, it's important we serialise with a value that doesn't // change position in the upated enum class @@ -197,9 +197,9 @@ class EnumTests { @Test fun enumNotWhitelistedFails() { - data class C (val c: Bras) + data class C(val c: Bras) - class WL (val allowed: String): ClassWhitelist { + class WL(val allowed: String) : ClassWhitelist { override fun hasListed(type: Class<*>): Boolean { return type.name == allowed } @@ -214,7 +214,7 @@ class EnumTests { @Test fun enumWhitelisted() { - data class C (val c: Bras) + data class C(val c: Bras) class WL : ClassWhitelist { override fun hasListed(type: Class<*>): Boolean { @@ -231,7 +231,7 @@ class EnumTests { @Test fun enumAnnotated() { - @CordaSerializable data class C (val c: AnnotatedBras) + @CordaSerializable data class C(val c: AnnotatedBras) class WL : ClassWhitelist { override fun hasListed(type: Class<*>) = false @@ -245,21 +245,21 @@ class EnumTests { @Test fun deserializeNonWhitlistedEnum() { - data class C (val c: Bras) + data class C(val c: Bras) - class WL (val allowed: List) : ClassWhitelist { + class WL(val allowed: List) : ClassWhitelist { override fun hasListed(type: Class<*>) = type.name in allowed } // first serialise the class using a context in which Bras are whitelisted - val factory = SerializerFactory(WL(listOf (classTestName("C"), + val factory = SerializerFactory(WL(listOf(classTestName("C"), "net.corda.nodeapi.internal.serialization.amqp.EnumTests\$Bras")), ClassLoader.getSystemClassLoader()) val bytes = TestSerializationOutput(VERBOSE, factory).serialize(C(Bras.UNDERWIRE)) // then take that serialised object and attempt to deserialize it in a context that // disallows the Bras enum - val factory2 = SerializerFactory(WL(listOf (classTestName("C"))), ClassLoader.getSystemClassLoader()) + val factory2 = SerializerFactory(WL(listOf(classTestName("C"))), ClassLoader.getSystemClassLoader()) Assertions.assertThatThrownBy { DeserializationInput(factory2).deserialize(bytes) }.isInstanceOf(NotSerializableException::class.java) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt index 0bf23ae85d..cdf690029c 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt @@ -32,7 +32,7 @@ class EvolvabilityTests { // f.writeBytes(sc.bytes) // new version of the class, in this case the order of the parameters has been swapped - data class C (val b: Int, val a: Int) + data class C(val b: Int, val a: Int) val sc2 = f.readBytes() val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) @@ -56,7 +56,7 @@ class EvolvabilityTests { // f.writeBytes(sc.bytes) // new version of the class, in this case the order of the parameters has been swapped - data class C (val b: String, val a: Int) + data class C(val b: String, val a: Int) val sc2 = f.readBytes() val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) @@ -79,13 +79,13 @@ class EvolvabilityTests { // f.writeBytes(sc.bytes) // println ("Path = $path") - data class C (val a: Int, val b: Int?) + data class C(val a: Int, val b: Int?) val sc2 = f.readBytes() val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) - assertEquals (A, deserializedC.a) - assertEquals (null, deserializedC.b) + assertEquals(A, deserializedC.a) + assertEquals(null, deserializedC.b) } @Test(expected = NotSerializableException::class) @@ -104,7 +104,7 @@ class EvolvabilityTests { // println ("Path = $path") // new version of the class, in this case a new parameter has been added (b) - data class C (val a: Int, val b: Int) + data class C(val a: Int, val b: Int) val sc2 = f.readBytes() @@ -132,13 +132,13 @@ class EvolvabilityTests { // f.writeBytes(scc.bytes) // println ("Path = $path") - data class CC (val b: String, val d: Int) + data class CC(val b: String, val d: Int) val sc2 = f.readBytes() val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) - assertEquals (B, deserializedCC.b) - assertEquals (D, deserializedCC.d) + assertEquals(B, deserializedCC.b) + assertEquals(D, deserializedCC.d) } @Suppress("UNUSED_VARIABLE") @@ -185,16 +185,16 @@ class EvolvabilityTests { // println ("Path = $path") @Suppress("UNUSED") - data class CC (val a: Int, val b: String) { + data class CC(val a: Int, val b: String) { @DeprecatedConstructorForDeserialization(1) - constructor (a: Int) : this (a, "hello") + constructor (a: Int) : this(a, "hello") } val sc2 = f.readBytes() val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) - assertEquals (A, deserializedCC.a) - assertEquals ("hello", deserializedCC.b) + assertEquals(A, deserializedCC.a) + assertEquals("hello", deserializedCC.b) } @Test(expected = NotSerializableException::class) @@ -214,9 +214,9 @@ class EvolvabilityTests { // f.writeBytes(scc.bytes) // println ("Path = $path") - data class CC (val a: Int, val b: String) { + data class CC(val a: Int, val b: String) { // constructor annotation purposefully omitted - constructor (a: Int) : this (a, "hello") + constructor (a: Int) : this(a, "hello") } // we expect this to throw as we should not find any constructors @@ -242,20 +242,20 @@ class EvolvabilityTests { // println ("Path = $path") @Suppress("UNUSED") - data class CC (val a: Int, val b: Int, val c: String, val d: String) { + data class CC(val a: Int, val b: Int, val c: String, val d: String) { // ensure none of the original parameters align with the initial // construction order @DeprecatedConstructorForDeserialization(1) - constructor (c: String, a: Int, b: Int) : this (a, b, c, "wibble") + constructor (c: String, a: Int, b: Int) : this(a, b, c, "wibble") } val sc2 = f.readBytes() val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) - assertEquals (A, deserializedCC.a) - assertEquals (B, deserializedCC.b) - assertEquals (C, deserializedCC.c) - assertEquals ("wibble", deserializedCC.d) + assertEquals(A, deserializedCC.a) + assertEquals(B, deserializedCC.b) + assertEquals(C, deserializedCC.c) + assertEquals("wibble", deserializedCC.d) } @Test @@ -277,20 +277,20 @@ class EvolvabilityTests { // println ("Path = $path") // b is removed, d is added - data class CC (val a: Int, val c: String, val d: String) { + data class CC(val a: Int, val c: String, val d: String) { // ensure none of the original parameters align with the initial // construction order @Suppress("UNUSED") @DeprecatedConstructorForDeserialization(1) - constructor (c: String, a: Int) : this (a, c, "wibble") + constructor (c: String, a: Int) : this(a, c, "wibble") } val sc2 = f.readBytes() val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) - assertEquals (A, deserializedCC.a) - assertEquals (C, deserializedCC.c) - assertEquals ("wibble", deserializedCC.d) + assertEquals(A, deserializedCC.a) + assertEquals(C, deserializedCC.c) + assertEquals("wibble", deserializedCC.d) } @Test @@ -322,13 +322,15 @@ class EvolvabilityTests { // println ("Path = $path1") @Suppress("UNUSED") - data class C (val e: Int, val c: Int, val b: Int, val a: Int, val d: Int) { + data class C(val e: Int, val c: Int, val b: Int, val a: Int, val d: Int) { @DeprecatedConstructorForDeserialization(1) - constructor (b: Int, a: Int) : this (-1, -1, b, a, -1) + constructor (b: Int, a: Int) : this(-1, -1, b, a, -1) + @DeprecatedConstructorForDeserialization(2) - constructor (a: Int, c: Int, b: Int) : this (-1, c, b, a, -1) + constructor (a: Int, c: Int, b: Int) : this(-1, c, b, a, -1) + @DeprecatedConstructorForDeserialization(3) - constructor (a: Int, b: Int, c: Int, d: Int) : this (-1, c, b, a, d) + constructor (a: Int, b: Int, c: Int, d: Int) : this(-1, c, b, a, d) } val sb1 = File(path1.toURI()).readBytes() @@ -376,15 +378,16 @@ class EvolvabilityTests { // println ("Path = $path") // Add a parameter to inner but keep outer unchanged - data class Inner (val a: Int, val b: String?) - data class Outer (val a: Int, val b: Inner) + data class Inner(val a: Int, val b: String?) + + data class Outer(val a: Int, val b: Inner) val sc2 = f.readBytes() val outer = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) - assertEquals (oa, outer.a) - assertEquals (ia, outer.b.a) - assertEquals (null, outer.b.b) + assertEquals(oa, outer.a) + assertEquals(ia, outer.b.a) + assertEquals(null, outer.b.b) } @Test @@ -416,15 +419,18 @@ class EvolvabilityTests { // println ("Path = $path1") @Suppress("UNUSED") - data class C (val b: Int, val c: Int, val d: Int, val e: Int, val f: Int, val g: Int) { + data class C(val b: Int, val c: Int, val d: Int, val e: Int, val f: Int, val g: Int) { @DeprecatedConstructorForDeserialization(1) - constructor (b: Int, c: Int) : this (b, c, -1, -1, -1, -1) + constructor (b: Int, c: Int) : this(b, c, -1, -1, -1, -1) + @DeprecatedConstructorForDeserialization(2) - constructor (b: Int, c: Int, d: Int) : this (b, c, d, -1, -1, -1) + constructor (b: Int, c: Int, d: Int) : this(b, c, d, -1, -1, -1) + @DeprecatedConstructorForDeserialization(3) - constructor (b: Int, c: Int, d: Int, e: Int) : this (b, c, d, e, -1, -1) + constructor (b: Int, c: Int, d: Int, e: Int) : this(b, c, d, e, -1, -1) + @DeprecatedConstructorForDeserialization(4) - constructor (b: Int, c: Int, d: Int, e: Int, f: Int) : this (b, c, d, e, f, -1) + constructor (b: Int, c: Int, d: Int, e: Int, f: Int) : this(b, c, d, e, f, -1) } val sb1 = File(path1.toURI()).readBytes() diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt index 05cd84c934..4b1011ad49 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt @@ -50,7 +50,7 @@ class SerializationOutputTests { data class testShort(val s: Short) - data class testBoolean(val b : Boolean) + data class testBoolean(val b: Boolean) interface FooInterface { val pub: Int @@ -145,13 +145,13 @@ class SerializationOutputTests { data class PolymorphicProperty(val foo: FooInterface?) - private inline fun serdes(obj: T, - factory: SerializerFactory = SerializerFactory ( - AllWhitelist, ClassLoader.getSystemClassLoader()), - freshDeserializationFactory: SerializerFactory = SerializerFactory( - AllWhitelist, ClassLoader.getSystemClassLoader()), - expectedEqual: Boolean = true, - expectDeserializedEqual: Boolean = true): T { + private inline fun serdes(obj: T, + factory: SerializerFactory = SerializerFactory( + AllWhitelist, ClassLoader.getSystemClassLoader()), + freshDeserializationFactory: SerializerFactory = SerializerFactory( + AllWhitelist, ClassLoader.getSystemClassLoader()), + expectedEqual: Boolean = true, + expectDeserializedEqual: Boolean = true): T { val ser = SerializationOutput(factory) val bytes = ser.serialize(obj) @@ -446,10 +446,10 @@ class SerializationOutputTests { try { try { throw IOException("Layer 1") - } catch(t: Throwable) { + } catch (t: Throwable) { throw IllegalStateException("Layer 2", t) } - } catch(t: Throwable) { + } catch (t: Throwable) { val desThrowable = serdesThrowableWithInternalInfo(t, factory, factory2, false) assertSerializedThrowableEquivalent(t, desThrowable) } @@ -476,12 +476,12 @@ class SerializationOutputTests { try { try { throw IOException("Layer 1") - } catch(t: Throwable) { + } catch (t: Throwable) { val e = IllegalStateException("Layer 2") e.addSuppressed(t) throw e } - } catch(t: Throwable) { + } catch (t: Throwable) { val desThrowable = serdesThrowableWithInternalInfo(t, factory, factory2, false) assertSerializedThrowableEquivalent(t, desThrowable) } @@ -535,6 +535,7 @@ class SerializationOutputTests { } val FOO_PROGRAM_ID = "net.corda.nodeapi.internal.serialization.amqp.SerializationOutputTests.FooContract" + class FooState : ContractState { override val participants: List = emptyList() } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializeAndReturnSchemaTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializeAndReturnSchemaTest.kt index 86b769a14f..acd8c5b8fe 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializeAndReturnSchemaTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializeAndReturnSchemaTest.kt @@ -19,6 +19,7 @@ class SerializeAndReturnSchemaTest { @Test fun getSchema() { data class C(val a: Int, val b: Int) + val a = 1 val b = 2 diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/StaticInitialisationOfSerializedObjectTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/StaticInitialisationOfSerializedObjectTest.kt index 47decde9c0..2533bdf182 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/StaticInitialisationOfSerializedObjectTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/StaticInitialisationOfSerializedObjectTest.kt @@ -12,7 +12,7 @@ import java.lang.reflect.Type import java.util.concurrent.ConcurrentHashMap import kotlin.test.assertEquals -class InStatic : Exception ("Help!, help!, I'm being repressed") +class InStatic : Exception("Help!, help!, I'm being repressed") class C { companion object { @@ -28,7 +28,7 @@ class C { // comment out the companion object from here, comment out the test code and uncomment // the generation code, then re-run the test and copy the file shown in the output print // to the resource directory -class C2 (var b: Int) { +class C2(var b: Int) { /* companion object { init { @@ -39,14 +39,14 @@ class C2 (var b: Int) { } class StaticInitialisationOfSerializedObjectTest { - @Test(expected=java.lang.ExceptionInInitializerError::class) + @Test(expected = java.lang.ExceptionInInitializerError::class) fun itBlowsUp() { C() } @Test fun KotlinObjectWithCompanionObject() { - data class D (val c : C) + data class D(val c: C) val sf = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) @@ -71,7 +71,7 @@ class StaticInitialisationOfSerializedObjectTest { @Test fun deserializeTest() { - data class D (val c : C2) + data class D(val c: C2) val path = EvolvabilityTests::class.java.getResource("StaticInitialisationOfSerializedObjectTest.deserializeTest") val f = File(path.toURI()) @@ -86,7 +86,7 @@ class StaticInitialisationOfSerializedObjectTest { class WL : ClassWhitelist { override fun hasListed(type: Class<*>) = type.name == "net.corda.nodeapi.internal.serialization.amqp" + - ".StaticInitialisationOfSerializedObjectTest\$deserializeTest\$D" + ".StaticInitialisationOfSerializedObjectTest\$deserializeTest\$D" } val sf2 = SerializerFactory(WL(), ClassLoader.getSystemClassLoader()) @@ -100,14 +100,14 @@ class StaticInitialisationOfSerializedObjectTest { // Version of a serializer factory that will allow the class carpenter living on the // factory to have a different whitelist applied to it than the factory class TestSerializerFactory(wl1: ClassWhitelist, wl2: ClassWhitelist) : - SerializerFactory (wl1, ClassLoader.getSystemClassLoader()) { + SerializerFactory(wl1, ClassLoader.getSystemClassLoader()) { override val classCarpenter = ClassCarpenter(ClassLoader.getSystemClassLoader(), wl2) } // This time have the serilization factory and the carpenter use different whitelists @Test fun deserializeTest2() { - data class D (val c : C2) + data class D(val c: C2) val path = EvolvabilityTests::class.java.getResource("StaticInitialisationOfSerializedObjectTest.deserializeTest2") val f = File(path.toURI()) From fcc4bdae7e2d1e172b4e8cc57a0a4ba33b10417a Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Fri, 6 Oct 2017 15:32:23 +0100 Subject: [PATCH 127/185] Reformat files in `testing` --- .../attachmentdemo/AttachmentDemoTest.kt | 3 +- .../main/kotlin/net/corda/irs/contract/IRS.kt | 11 +- .../corda/irs/api/NodeInterestRatesTest.kt | 2 +- .../net/corda/netmap/VisualiserViewModel.kt | 2 +- .../corda/netmap/simulation/IRSSimulation.kt | 1 + .../net/corda/netmap/simulation/Simulation.kt | 6 +- .../netmap/simulation/IRSSimulationTest.kt | 3 +- .../kotlin/net/corda/notarydemo/Notarise.kt | 2 +- .../DirectoryMarketDataBuilder.java | 74 +- .../examples/marketdata/ExampleData.java | 69 +- .../marketdata/ExampleMarketData.java | 39 +- .../marketdata/ExampleMarketDataBuilder.java | 635 +++++++++--------- .../marketdata/JarMarketDataBuilder.java | 178 ++--- .../MarkitIndexCreditCurveDataParser.java | 366 +++++----- .../credit/markit/MarkitRedCode.java | 124 ++-- .../markit/MarkitRestructuringClause.java | 175 ++--- .../credit/markit/MarkitSeniorityLevel.java | 127 ++-- ...MarkitSingleNameCreditCurveDataParser.java | 254 +++---- .../markit/MarkitYieldCurveDataParser.java | 179 ++--- .../credit/markit/package-info.java | 4 +- .../examples/marketdata/package-info.java | 4 +- .../kotlin/net/corda/vega/api/PortfolioApi.kt | 7 +- .../kotlin/net/corda/vega/flows/SimmFlow.kt | 3 +- .../net/corda/vega/flows/StateRevisionFlow.kt | 2 +- .../java/net/corda/vega/SwapExampleX.java | 158 ++--- .../corda/traderdemo/TraderDemoClientApi.kt | 22 +- .../flow/CommercialPaperIssueFlow.kt | 2 + .../net/corda/testing/DriverConstants.kt | 2 + .../kotlin/net/corda/testing/NodeTestUtils.kt | 7 +- .../kotlin/net/corda/testing/RPCDriver.kt | 18 +- .../testing/node/InMemoryMessagingNetwork.kt | 7 +- .../corda/testing/node/MockNetworkMapCache.kt | 2 +- .../kotlin/net/corda/testing/node/MockNode.kt | 6 +- .../net/corda/testing/node/MockServices.kt | 13 +- .../net/corda/testing/node/TestClock.kt | 6 +- .../net/corda/smoketesting/NodeProcess.kt | 7 +- .../kotlin/net/corda/testing/CoreTestUtils.kt | 13 +- .../kotlin/net/corda/testing/Eventually.kt | 7 +- .../net/corda/testing/FlowStackSnapshot.kt | 13 +- .../main/kotlin/net/corda/testing/Measure.kt | 7 +- .../corda/testing/SerializationTestHelpers.kt | 12 +- .../main/kotlin/net/corda/testing/TestDSL.kt | 2 +- .../testing/TransactionDSLInterpreter.kt | 4 +- .../testing/contracts/DummyDealContract.kt | 3 +- .../corda/testing/contracts/VaultFiller.kt | 10 +- .../kotlin/net/corda/testing/http/HttpApi.kt | 1 + .../schemas/DummyLinearStateSchemaV1.kt | 4 +- 47 files changed, 1327 insertions(+), 1269 deletions(-) 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 d0cdb64932..b1c0ace017 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 @@ -13,7 +13,8 @@ import java.util.concurrent.CompletableFuture.supplyAsync class AttachmentDemoTest { // run with a 10,000,000 bytes in-memory zip file. In practice, a slightly bigger file will be used (~10,002,000 bytes). - @Test fun `attachment demo using a 10MB zip file`() { + @Test + fun `attachment demo using a 10MB zip file`() { val numOfExpectedBytes = 10_000_000 driver(isDebug = true, portAllocation = PortAllocation.Incremental(20000)) { val demoUser = listOf(User("demo", "demo", setOf(startFlowPermission()))) diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt index 097a33c5e8..c81d80592c 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt @@ -141,11 +141,12 @@ class FloatingRatePaymentEvent(date: LocalDate, val CSVHeader = RatePaymentEvent.CSVHeader + ",FixingDate" } - override val flow: Amount get() { - // TODO: Should an uncalculated amount return a zero ? null ? etc. - val v = rate.ratioUnit?.value ?: return Amount(0, notional.token) - return Amount(dayCountFactor.times(BigDecimal(notional.quantity)).times(v).toLong(), notional.token) - } + override val flow: Amount + get() { + // TODO: Should an uncalculated amount return a zero ? null ? etc. + val v = rate.ratioUnit?.value ?: return Amount(0, notional.token) + return Amount(dayCountFactor.times(BigDecimal(notional.quantity)).times(v).toLong(), notional.token) + } override fun toString(): String = "FloatingPaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate (fix on $fixingDate): $flow" diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index a6b720cf72..ef07a9a5a3 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -242,7 +242,7 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { } private fun makePartialTX() = TransactionBuilder(DUMMY_NOTARY).withItems( - TransactionState(1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE, Cash.PROGRAM_ID, DUMMY_NOTARY)) + TransactionState(1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE, Cash.PROGRAM_ID, DUMMY_NOTARY)) private fun makeFullTx() = makePartialTX().withItems(dummyCommand()) } diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/VisualiserViewModel.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/VisualiserViewModel.kt index f9751e8d6c..f8f9c1f625 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/VisualiserViewModel.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/VisualiserViewModel.kt @@ -86,7 +86,7 @@ class VisualiserViewModel { // top right: 33.0469,64.3209 try { return node.place.coordinate.project(view.mapImage.fitWidth, view.mapImage.fitHeight, 64.3209, 29.8406, -23.2031, 33.0469) - } catch(e: Exception) { + } catch (e: Exception) { throw Exception("Cannot project ${node.started!!.info.chooseIdentity()}", e) } } diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt index af7aae480e..0df1f7cdc2 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt @@ -141,6 +141,7 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten node2.internals.registerInitiatedFlow(FixingFlow.Fixer::class.java) val notaryId = node1.rpcOps.notaryIdentities().first() + @InitiatingFlow class StartDealFlow(val otherParty: Party, val payload: AutoOffer) : FlowLogic() { diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt index 46a38d7f5e..74529cd530 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt @@ -162,8 +162,10 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, // These are used from the network visualiser tool. private val _allFlowSteps = PublishSubject.create>() private val _doneSteps = PublishSubject.create>() - @Suppress("unused") val allFlowSteps: Observable> = _allFlowSteps - @Suppress("unused") val doneSteps: Observable> = _doneSteps + @Suppress("unused") + val allFlowSteps: Observable> = _allFlowSteps + @Suppress("unused") + val doneSteps: Observable> = _doneSteps private var pumpCursor = 0 diff --git a/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt b/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt index 7a329a9ad4..4bd32364a4 100644 --- a/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt +++ b/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt @@ -21,7 +21,8 @@ class IRSSimulationTest { unsetCordappPackages() } - @Test fun `runs to completion`() { + @Test + fun `runs to completion`() { LogHelper.setLevel("+messages") // FIXME: Don't manipulate static state in tests. val sim = IRSSimulation(false, false, null) val future = sim.start() 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 c107c44578..cabacf9476 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 @@ -31,7 +31,7 @@ private class NotaryDemoClientApi(val rpc: CordaRPCOps) { private val counterparty by lazy { val parties = rpc.networkMapSnapshot() parties.fold(ArrayList()) { acc, elem -> - acc.addAll(elem.legalIdentitiesAndCerts.filter { it.name == BOB.name}) + acc.addAll(elem.legalIdentitiesAndCerts.filter { it.name == BOB.name }) acc }.single().party } diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/DirectoryMarketDataBuilder.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/DirectoryMarketDataBuilder.java index fcb4949773..b9c5f02823 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/DirectoryMarketDataBuilder.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/DirectoryMarketDataBuilder.java @@ -1,6 +1,6 @@ /** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. */ package com.opengamma.strata.examples.marketdata; @@ -19,46 +19,46 @@ import java.util.stream.Collectors; */ public class DirectoryMarketDataBuilder extends ExampleMarketDataBuilder { - /** - * The path to the root of the directory structure. - */ - private final Path rootPath; + /** + * The path to the root of the directory structure. + */ + private final Path rootPath; - /** - * Constructs an instance. - * - * @param rootPath the path to the root of the directory structure - */ - public DirectoryMarketDataBuilder(Path rootPath) { - this.rootPath = rootPath; - } - - //------------------------------------------------------------------------- - @Override - protected Collection getAllResources(String subdirectoryName) { - File dir = rootPath.resolve(subdirectoryName).toFile(); - if (!dir.exists()) { - throw new IllegalArgumentException(Messages.format("Directory does not exist: {}", dir)); + /** + * Constructs an instance. + * + * @param rootPath the path to the root of the directory structure + */ + public DirectoryMarketDataBuilder(Path rootPath) { + this.rootPath = rootPath; } - return Arrays.stream(dir.listFiles()) - .filter(f -> !f.isHidden()) - .map(ResourceLocator::ofFile) - .collect(Collectors.toList()); - } - @Override - protected ResourceLocator getResource(String subdirectoryName, String resourceName) { - File file = rootPath.resolve(subdirectoryName).resolve(resourceName).toFile(); - if (!file.exists()) { - return null; + //------------------------------------------------------------------------- + @Override + protected Collection getAllResources(String subdirectoryName) { + File dir = rootPath.resolve(subdirectoryName).toFile(); + if (!dir.exists()) { + throw new IllegalArgumentException(Messages.format("Directory does not exist: {}", dir)); + } + return Arrays.stream(dir.listFiles()) + .filter(f -> !f.isHidden()) + .map(ResourceLocator::ofFile) + .collect(Collectors.toList()); } - return ResourceLocator.ofFile(file); - } - @Override - protected boolean subdirectoryExists(String subdirectoryName) { - File file = rootPath.resolve(subdirectoryName).toFile(); - return file.exists(); - } + @Override + protected ResourceLocator getResource(String subdirectoryName, String resourceName) { + File file = rootPath.resolve(subdirectoryName).resolve(resourceName).toFile(); + if (!file.exists()) { + return null; + } + return ResourceLocator.ofFile(file); + } + + @Override + protected boolean subdirectoryExists(String subdirectoryName) { + File file = rootPath.resolve(subdirectoryName).toFile(); + return file.exists(); + } } diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleData.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleData.java index 5780b6c0d9..7073b361b2 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleData.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleData.java @@ -1,6 +1,6 @@ /** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. */ package com.opengamma.strata.examples.marketdata; @@ -18,40 +18,41 @@ import java.util.Locale; */ public final class ExampleData { - /** - * Restricted constructor. - */ - private ExampleData() { - } - - //------------------------------------------------------------------------- - /** - * Loads a golden copy of expected results from a text file. - * - * @param name the name of the results - * @return the loaded results - */ - public static String loadExpectedResults(String name) { - String classpathResourceName = String.format(Locale.ENGLISH, "classpath:goldencopy/%s.txt", name); - ResourceLocator resourceLocator = ResourceLocator.of(classpathResourceName); - try { - return resourceLocator.getCharSource().read().trim(); - } catch (IOException ex) { - throw new UncheckedIOException(name, ex); + /** + * Restricted constructor. + */ + private ExampleData() { } - } - /** - * Loads a trade report template from the standard INI format. - * - * @param templateName the name of the template - * @return the loaded report template - */ - public static TradeReportTemplate loadTradeReportTemplate(String templateName) { - String resourceName = String.format(Locale.ENGLISH, "classpath:example-reports/%s.ini", templateName); - ResourceLocator resourceLocator = ResourceLocator.of(resourceName); - IniFile ini = IniFile.of(resourceLocator.getCharSource()); - return TradeReportTemplate.load(ini); - } + //------------------------------------------------------------------------- + + /** + * Loads a golden copy of expected results from a text file. + * + * @param name the name of the results + * @return the loaded results + */ + public static String loadExpectedResults(String name) { + String classpathResourceName = String.format(Locale.ENGLISH, "classpath:goldencopy/%s.txt", name); + ResourceLocator resourceLocator = ResourceLocator.of(classpathResourceName); + try { + return resourceLocator.getCharSource().read().trim(); + } catch (IOException ex) { + throw new UncheckedIOException(name, ex); + } + } + + /** + * Loads a trade report template from the standard INI format. + * + * @param templateName the name of the template + * @return the loaded report template + */ + public static TradeReportTemplate loadTradeReportTemplate(String templateName) { + String resourceName = String.format(Locale.ENGLISH, "classpath:example-reports/%s.ini", templateName); + ResourceLocator resourceLocator = ResourceLocator.of(resourceName); + IniFile ini = IniFile.of(resourceLocator.getCharSource()); + return TradeReportTemplate.load(ini); + } } diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleMarketData.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleMarketData.java index a0fb78bed6..9ed74a4b23 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleMarketData.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleMarketData.java @@ -1,6 +1,6 @@ /** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. */ package com.opengamma.strata.examples.marketdata; @@ -10,25 +10,26 @@ package com.opengamma.strata.examples.marketdata; */ public final class ExampleMarketData { - /** - * Root resource directory of the built-in example market data - */ - private static final String EXAMPLE_MARKET_DATA_ROOT = "example-marketdata"; + /** + * Root resource directory of the built-in example market data + */ + private static final String EXAMPLE_MARKET_DATA_ROOT = "example-marketdata"; - /** - * Restricted constructor. - */ - private ExampleMarketData() { - } + /** + * Restricted constructor. + */ + private ExampleMarketData() { + } - //------------------------------------------------------------------------- - /** - * Gets a market data builder for the built-in example market data. - * - * @return the market data builder - */ - public static ExampleMarketDataBuilder builder() { - return ExampleMarketDataBuilder.ofResource(EXAMPLE_MARKET_DATA_ROOT); - } + //------------------------------------------------------------------------- + + /** + * Gets a market data builder for the built-in example market data. + * + * @return the market data builder + */ + public static ExampleMarketDataBuilder builder() { + return ExampleMarketDataBuilder.ofResource(EXAMPLE_MARKET_DATA_ROOT); + } } diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleMarketDataBuilder.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleMarketDataBuilder.java index a59374c7f6..1f81eb3015 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleMarketDataBuilder.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/ExampleMarketDataBuilder.java @@ -1,6 +1,6 @@ /** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. */ package com.opengamma.strata.examples.marketdata; @@ -67,355 +67,358 @@ import static com.opengamma.strata.collect.Guavate.toImmutableList; */ public abstract class ExampleMarketDataBuilder { - private static final Logger log = LoggerFactory.getLogger(ExampleMarketDataBuilder.class); + private static final Logger log = LoggerFactory.getLogger(ExampleMarketDataBuilder.class); - /** The name of the subdirectory containing historical fixings. */ - private static final String HISTORICAL_FIXINGS_DIR = "historical-fixings"; + /** The name of the subdirectory containing historical fixings. */ + private static final String HISTORICAL_FIXINGS_DIR = "historical-fixings"; - /** The name of the subdirectory containing calibrated rates curves. */ - private static final String CURVES_DIR = "curves"; - /** The name of the curve groups file. */ - private static final String CURVES_GROUPS_FILE = "groups.csv"; - /** The name of the curve settings file. */ - private static final String CURVES_SETTINGS_FILE = "settings.csv"; + /** The name of the subdirectory containing calibrated rates curves. */ + private static final String CURVES_DIR = "curves"; + /** The name of the curve groups file. */ + private static final String CURVES_GROUPS_FILE = "groups.csv"; + /** The name of the curve settings file. */ + private static final String CURVES_SETTINGS_FILE = "settings.csv"; - /** The name of the directory containing CDS ISDA yield curve, credit curve and static data. */ - private static final String CREDIT_DIR = "credit"; - private static final String CDS_YIELD_CURVES_FILE = "cds.yieldCurves.csv"; - private static final String SINGLE_NAME_CREDIT_CURVES_FILE = "singleName.creditCurves.csv"; - private static final String SINGLE_NAME_STATIC_DATA_FILE = "singleName.staticData.csv"; - private static final String INDEX_CREDIT_CURVES_FILE = "index.creditCurves.csv"; - private static final String INDEX_STATIC_DATA_FILE = "index.staticData.csv"; + /** The name of the directory containing CDS ISDA yield curve, credit curve and static data. */ + private static final String CREDIT_DIR = "credit"; + private static final String CDS_YIELD_CURVES_FILE = "cds.yieldCurves.csv"; + private static final String SINGLE_NAME_CREDIT_CURVES_FILE = "singleName.creditCurves.csv"; + private static final String SINGLE_NAME_STATIC_DATA_FILE = "singleName.staticData.csv"; + private static final String INDEX_CREDIT_CURVES_FILE = "index.creditCurves.csv"; + private static final String INDEX_STATIC_DATA_FILE = "index.staticData.csv"; - /** The name of the subdirectory containing simple market quotes. */ - private static final String QUOTES_DIR = "quotes"; - /** The name of the quotes file. */ - private static final String QUOTES_FILE = "quotes.csv"; + /** The name of the subdirectory containing simple market quotes. */ + private static final String QUOTES_DIR = "quotes"; + /** The name of the quotes file. */ + private static final String QUOTES_FILE = "quotes.csv"; - //------------------------------------------------------------------------- - /** - * Creates an instance from a given classpath resource root location using the class loader - * which created this class. - *

- * This is designed to handle resource roots which may physically correspond to a directory on - * disk, or be located within a jar file. - * - * @param resourceRoot the resource root path - * @return the market data builder - */ - public static ExampleMarketDataBuilder ofResource(String resourceRoot) { - return ofResource(resourceRoot, ExampleMarketDataBuilder.class.getClassLoader()); - } + //------------------------------------------------------------------------- - /** - * Creates an instance from a given classpath resource root location, using the given class loader - * to find the resource. - *

- * This is designed to handle resource roots which may physically correspond to a directory on - * disk, or be located within a jar file. - * - * @param resourceRoot the resource root path - * @param classLoader the class loader with which to find the resource - * @return the market data builder - */ - public static ExampleMarketDataBuilder ofResource(String resourceRoot, ClassLoader classLoader) { - // classpath resources are forward-slash separated - String qualifiedRoot = resourceRoot; - qualifiedRoot = qualifiedRoot.startsWith("/") ? qualifiedRoot.substring(1) : qualifiedRoot; - qualifiedRoot = qualifiedRoot.startsWith("\\") ? qualifiedRoot.substring(1) : qualifiedRoot; - qualifiedRoot = qualifiedRoot.endsWith("/") ? qualifiedRoot : qualifiedRoot + "/"; - URL url = classLoader.getResource(qualifiedRoot); - if (url == null) { - throw new IllegalArgumentException(Messages.format("Classpath resource not found: {}", qualifiedRoot)); - } - if (url.getProtocol() != null && "jar".equals(url.getProtocol().toLowerCase(Locale.ENGLISH))) { - // Inside a JAR - int classSeparatorIdx = url.getFile().indexOf("!"); - if (classSeparatorIdx == -1) { - throw new IllegalArgumentException(Messages.format("Unexpected JAR file URL: {}", url)); - } - String jarPath = url.getFile().substring("file:".length(), classSeparatorIdx); - File jarFile; - try { - jarFile = new File(jarPath); - } catch (Exception e) { - throw new IllegalArgumentException(Messages.format("Unable to create file for JAR: {}", jarPath), e); - } - return new JarMarketDataBuilder(jarFile, resourceRoot); - } else { - // Resource is on disk - File file; - try { - file = new File(url.toURI()); - } catch (URISyntaxException e) { - throw new IllegalArgumentException(Messages.format("Unexpected file location: {}", url), e); - } - return new DirectoryMarketDataBuilder(file.toPath()); - } - } - - /** - * Creates an instance from a given directory root. - * - * @param rootPath the root directory - * @return the market data builder - */ - public static ExampleMarketDataBuilder ofPath(Path rootPath) { - return new DirectoryMarketDataBuilder(rootPath); - } - - //------------------------------------------------------------------------- - /** - * Builds a market data snapshot from this environment. - * - * @param marketDataDate the date of the market data - * @return the snapshot - */ - public ImmutableMarketData buildSnapshot(LocalDate marketDataDate) { - ImmutableMarketDataBuilder builder = ImmutableMarketData.builder(marketDataDate); - loadFixingSeries(builder); - loadRatesCurves(builder, marketDataDate); - loadQuotes(builder, marketDataDate); - loadFxRates(builder); - loadCreditMarketData(builder, marketDataDate); - return builder.build(); - } - - /** - * Gets the rates market lookup to use with this environment. - * - * @param marketDataDate the date of the market data - * @return the rates lookup - */ - public RatesMarketDataLookup ratesLookup(LocalDate marketDataDate) { - SortedMap curves = loadAllRatesCurves(); - return RatesMarketDataLookup.of(curves.get(marketDataDate)); - } - - /** - * Gets all rates curves. - * - * @return the map of all rates curves - */ - public SortedMap loadAllRatesCurves() { - if (!subdirectoryExists(CURVES_DIR)) { - throw new IllegalArgumentException("No rates curves directory found"); - } - ResourceLocator curveGroupsResource = getResource(CURVES_DIR, CURVES_GROUPS_FILE); - if (curveGroupsResource == null) { - throw new IllegalArgumentException(Messages.format( - "Unable to load rates curves: curve groups file not found at {}/{}", CURVES_DIR, CURVES_GROUPS_FILE)); - } - ResourceLocator curveSettingsResource = getResource(CURVES_DIR, CURVES_SETTINGS_FILE); - if (curveSettingsResource == null) { - throw new IllegalArgumentException(Messages.format( - "Unable to load rates curves: curve settings file not found at {}/{}", CURVES_DIR, CURVES_SETTINGS_FILE)); - } - ListMultimap curveGroups = - RatesCurvesCsvLoader.loadAllDates(curveGroupsResource, curveSettingsResource, getRatesCurvesResources()); - - // There is only one curve group in the market data file so this will always succeed - Map curveGroupMap = Maps.transformValues(curveGroups.asMap(), groups -> groups.iterator().next()); - return new TreeMap<>(curveGroupMap); - } - - //------------------------------------------------------------------------- - private void loadFixingSeries(ImmutableMarketDataBuilder builder) { - if (!subdirectoryExists(HISTORICAL_FIXINGS_DIR)) { - log.debug("No historical fixings directory found"); - return; - } - try { - Collection fixingSeriesResources = getAllResources(HISTORICAL_FIXINGS_DIR); - Map fixingSeries = FixingSeriesCsvLoader.load(fixingSeriesResources); - builder.addTimeSeriesMap(fixingSeries); - } catch (Exception e) { - log.error("Error loading fixing series", e); - } - } - - private void loadRatesCurves(ImmutableMarketDataBuilder builder, LocalDate marketDataDate) { - if (!subdirectoryExists(CURVES_DIR)) { - log.debug("No rates curves directory found"); - return; + /** + * Creates an instance from a given classpath resource root location using the class loader + * which created this class. + *

+ * This is designed to handle resource roots which may physically correspond to a directory on + * disk, or be located within a jar file. + * + * @param resourceRoot the resource root path + * @return the market data builder + */ + public static ExampleMarketDataBuilder ofResource(String resourceRoot) { + return ofResource(resourceRoot, ExampleMarketDataBuilder.class.getClassLoader()); } - ResourceLocator curveGroupsResource = getResource(CURVES_DIR, CURVES_GROUPS_FILE); - if (curveGroupsResource == null) { - log.error("Unable to load rates curves: curve groups file not found at {}/{}", CURVES_DIR, CURVES_GROUPS_FILE); - return; + /** + * Creates an instance from a given classpath resource root location, using the given class loader + * to find the resource. + *

+ * This is designed to handle resource roots which may physically correspond to a directory on + * disk, or be located within a jar file. + * + * @param resourceRoot the resource root path + * @param classLoader the class loader with which to find the resource + * @return the market data builder + */ + public static ExampleMarketDataBuilder ofResource(String resourceRoot, ClassLoader classLoader) { + // classpath resources are forward-slash separated + String qualifiedRoot = resourceRoot; + qualifiedRoot = qualifiedRoot.startsWith("/") ? qualifiedRoot.substring(1) : qualifiedRoot; + qualifiedRoot = qualifiedRoot.startsWith("\\") ? qualifiedRoot.substring(1) : qualifiedRoot; + qualifiedRoot = qualifiedRoot.endsWith("/") ? qualifiedRoot : qualifiedRoot + "/"; + URL url = classLoader.getResource(qualifiedRoot); + if (url == null) { + throw new IllegalArgumentException(Messages.format("Classpath resource not found: {}", qualifiedRoot)); + } + if (url.getProtocol() != null && "jar".equals(url.getProtocol().toLowerCase(Locale.ENGLISH))) { + // Inside a JAR + int classSeparatorIdx = url.getFile().indexOf("!"); + if (classSeparatorIdx == -1) { + throw new IllegalArgumentException(Messages.format("Unexpected JAR file URL: {}", url)); + } + String jarPath = url.getFile().substring("file:".length(), classSeparatorIdx); + File jarFile; + try { + jarFile = new File(jarPath); + } catch (Exception e) { + throw new IllegalArgumentException(Messages.format("Unable to create file for JAR: {}", jarPath), e); + } + return new JarMarketDataBuilder(jarFile, resourceRoot); + } else { + // Resource is on disk + File file; + try { + file = new File(url.toURI()); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(Messages.format("Unexpected file location: {}", url), e); + } + return new DirectoryMarketDataBuilder(file.toPath()); + } } - ResourceLocator curveSettingsResource = getResource(CURVES_DIR, CURVES_SETTINGS_FILE); - if (curveSettingsResource == null) { - log.error("Unable to load rates curves: curve settings file not found at {}/{}", CURVES_DIR, CURVES_SETTINGS_FILE); - return; - } - try { - Collection curvesResources = getRatesCurvesResources(); - List ratesCurves = - RatesCurvesCsvLoader.load(marketDataDate, curveGroupsResource, curveSettingsResource, curvesResources); - - for (CurveGroup group : ratesCurves) { - // add entry for higher level discount curve name - group.getDiscountCurves().forEach( - (ccy, curve) -> builder.addValue(CurveId.of(group.getName(), curve.getName()), curve)); - // add entry for higher level forward curve name - group.getForwardCurves().forEach( - (idx, curve) -> builder.addValue(CurveId.of(group.getName(), curve.getName()), curve)); - } - - } catch (Exception e) { - log.error("Error loading rates curves", e); - } - } - - // load quotes - private void loadQuotes(ImmutableMarketDataBuilder builder, LocalDate marketDataDate) { - if (!subdirectoryExists(QUOTES_DIR)) { - log.debug("No quotes directory found"); - return; + /** + * Creates an instance from a given directory root. + * + * @param rootPath the root directory + * @return the market data builder + */ + public static ExampleMarketDataBuilder ofPath(Path rootPath) { + return new DirectoryMarketDataBuilder(rootPath); } - ResourceLocator quotesResource = getResource(QUOTES_DIR, QUOTES_FILE); - if (quotesResource == null) { - log.error("Unable to load quotes: quotes file not found at {}/{}", QUOTES_DIR, QUOTES_FILE); - return; + //------------------------------------------------------------------------- + + /** + * Builds a market data snapshot from this environment. + * + * @param marketDataDate the date of the market data + * @return the snapshot + */ + public ImmutableMarketData buildSnapshot(LocalDate marketDataDate) { + ImmutableMarketDataBuilder builder = ImmutableMarketData.builder(marketDataDate); + loadFixingSeries(builder); + loadRatesCurves(builder, marketDataDate); + loadQuotes(builder, marketDataDate); + loadFxRates(builder); + loadCreditMarketData(builder, marketDataDate); + return builder.build(); } - try { - Map quotes = QuotesCsvLoader.load(marketDataDate, quotesResource); - builder.addValueMap(quotes); - - } catch (Exception ex) { - log.error("Error loading quotes", ex); - } - } - - private void loadFxRates(ImmutableMarketDataBuilder builder) { - // TODO - load from CSV file - format to be defined - builder.addValue(FxRateId.of(Currency.GBP, Currency.USD), FxRate.of(Currency.GBP, Currency.USD, 1.61)); - } - - //------------------------------------------------------------------------- - private Collection getRatesCurvesResources() { - return getAllResources(CURVES_DIR).stream() - .filter(res -> !res.getLocator().endsWith(CURVES_GROUPS_FILE)) - .filter(res -> !res.getLocator().endsWith(CURVES_SETTINGS_FILE)) - .collect(toImmutableList()); - } - - private void loadCreditMarketData(ImmutableMarketDataBuilder builder, LocalDate marketDataDate) { - if (!subdirectoryExists(CREDIT_DIR)) { - log.debug("No credit curves directory found"); - return; + /** + * Gets the rates market lookup to use with this environment. + * + * @param marketDataDate the date of the market data + * @return the rates lookup + */ + public RatesMarketDataLookup ratesLookup(LocalDate marketDataDate) { + SortedMap curves = loadAllRatesCurves(); + return RatesMarketDataLookup.of(curves.get(marketDataDate)); } - String creditMarketDataDateDirectory = String.format( - Locale.ENGLISH, - "%s/%s", - CREDIT_DIR, - marketDataDate.format(DateTimeFormatter.ISO_LOCAL_DATE)); + /** + * Gets all rates curves. + * + * @return the map of all rates curves + */ + public SortedMap loadAllRatesCurves() { + if (!subdirectoryExists(CURVES_DIR)) { + throw new IllegalArgumentException("No rates curves directory found"); + } + ResourceLocator curveGroupsResource = getResource(CURVES_DIR, CURVES_GROUPS_FILE); + if (curveGroupsResource == null) { + throw new IllegalArgumentException(Messages.format( + "Unable to load rates curves: curve groups file not found at {}/{}", CURVES_DIR, CURVES_GROUPS_FILE)); + } + ResourceLocator curveSettingsResource = getResource(CURVES_DIR, CURVES_SETTINGS_FILE); + if (curveSettingsResource == null) { + throw new IllegalArgumentException(Messages.format( + "Unable to load rates curves: curve settings file not found at {}/{}", CURVES_DIR, CURVES_SETTINGS_FILE)); + } + ListMultimap curveGroups = + RatesCurvesCsvLoader.loadAllDates(curveGroupsResource, curveSettingsResource, getRatesCurvesResources()); - if (!subdirectoryExists(creditMarketDataDateDirectory)) { - log.debug("Unable to load market data: directory not found at {}", creditMarketDataDateDirectory); - return; + // There is only one curve group in the market data file so this will always succeed + Map curveGroupMap = Maps.transformValues(curveGroups.asMap(), groups -> groups.iterator().next()); + return new TreeMap<>(curveGroupMap); } - loadCdsYieldCurves(builder, creditMarketDataDateDirectory); - loadCdsSingleNameSpreadCurves(builder, creditMarketDataDateDirectory); - loadCdsIndexSpreadCurves(builder, creditMarketDataDateDirectory); - } - - private void loadCdsYieldCurves(ImmutableMarketDataBuilder builder, String creditMarketDataDateDirectory) { - ResourceLocator cdsYieldCurvesResource = getResource(creditMarketDataDateDirectory, CDS_YIELD_CURVES_FILE); - if (cdsYieldCurvesResource == null) { - log.debug("Unable to load cds yield curves: file not found at {}/{}", creditMarketDataDateDirectory, - CDS_YIELD_CURVES_FILE); - return; + //------------------------------------------------------------------------- + private void loadFixingSeries(ImmutableMarketDataBuilder builder) { + if (!subdirectoryExists(HISTORICAL_FIXINGS_DIR)) { + log.debug("No historical fixings directory found"); + return; + } + try { + Collection fixingSeriesResources = getAllResources(HISTORICAL_FIXINGS_DIR); + Map fixingSeries = FixingSeriesCsvLoader.load(fixingSeriesResources); + builder.addTimeSeriesMap(fixingSeries); + } catch (Exception e) { + log.error("Error loading fixing series", e); + } } - CharSource inputSource = cdsYieldCurvesResource.getCharSource(); - Map yieldCuves = MarkitYieldCurveDataParser.parse(inputSource); + private void loadRatesCurves(ImmutableMarketDataBuilder builder, LocalDate marketDataDate) { + if (!subdirectoryExists(CURVES_DIR)) { + log.debug("No rates curves directory found"); + return; + } - for (IsdaYieldCurveInputsId id : yieldCuves.keySet()) { - IsdaYieldCurveInputs curveInputs = yieldCuves.get(id); - builder.addValue(id, curveInputs); - } - } + ResourceLocator curveGroupsResource = getResource(CURVES_DIR, CURVES_GROUPS_FILE); + if (curveGroupsResource == null) { + log.error("Unable to load rates curves: curve groups file not found at {}/{}", CURVES_DIR, CURVES_GROUPS_FILE); + return; + } - private void loadCdsSingleNameSpreadCurves(ImmutableMarketDataBuilder builder, String creditMarketDataDateDirectory) { - ResourceLocator singleNameCurvesResource = getResource(creditMarketDataDateDirectory, SINGLE_NAME_CREDIT_CURVES_FILE); - if (singleNameCurvesResource == null) { - log.debug("Unable to load single name spread curves: file not found at {}/{}", creditMarketDataDateDirectory, - SINGLE_NAME_CREDIT_CURVES_FILE); - return; + ResourceLocator curveSettingsResource = getResource(CURVES_DIR, CURVES_SETTINGS_FILE); + if (curveSettingsResource == null) { + log.error("Unable to load rates curves: curve settings file not found at {}/{}", CURVES_DIR, CURVES_SETTINGS_FILE); + return; + } + try { + Collection curvesResources = getRatesCurvesResources(); + List ratesCurves = + RatesCurvesCsvLoader.load(marketDataDate, curveGroupsResource, curveSettingsResource, curvesResources); + + for (CurveGroup group : ratesCurves) { + // add entry for higher level discount curve name + group.getDiscountCurves().forEach( + (ccy, curve) -> builder.addValue(CurveId.of(group.getName(), curve.getName()), curve)); + // add entry for higher level forward curve name + group.getForwardCurves().forEach( + (idx, curve) -> builder.addValue(CurveId.of(group.getName(), curve.getName()), curve)); + } + + } catch (Exception e) { + log.error("Error loading rates curves", e); + } } - ResourceLocator singleNameStaticDataResource = getResource(creditMarketDataDateDirectory, SINGLE_NAME_STATIC_DATA_FILE); - if (singleNameStaticDataResource == null) { - log.debug("Unable to load single name static data: file not found at {}/{}", creditMarketDataDateDirectory, - SINGLE_NAME_STATIC_DATA_FILE); - return; + // load quotes + private void loadQuotes(ImmutableMarketDataBuilder builder, LocalDate marketDataDate) { + if (!subdirectoryExists(QUOTES_DIR)) { + log.debug("No quotes directory found"); + return; + } + + ResourceLocator quotesResource = getResource(QUOTES_DIR, QUOTES_FILE); + if (quotesResource == null) { + log.error("Unable to load quotes: quotes file not found at {}/{}", QUOTES_DIR, QUOTES_FILE); + return; + } + + try { + Map quotes = QuotesCsvLoader.load(marketDataDate, quotesResource); + builder.addValueMap(quotes); + + } catch (Exception ex) { + log.error("Error loading quotes", ex); + } } - try { - CharSource inputCreditCurvesSource = singleNameCurvesResource.getCharSource(); - CharSource inputStaticDataSource = singleNameStaticDataResource.getCharSource(); - MarkitSingleNameCreditCurveDataParser.parse(builder, inputCreditCurvesSource, inputStaticDataSource); - } catch (Exception ex) { - throw new RuntimeException(String.format( - Locale.ENGLISH, - "Unable to read single name spread curves: exception at %s/%s", - creditMarketDataDateDirectory, SINGLE_NAME_CREDIT_CURVES_FILE), ex); - } - } - - private void loadCdsIndexSpreadCurves(ImmutableMarketDataBuilder builder, String creditMarketDataDateDirectory) { - ResourceLocator inputCurvesResource = getResource(creditMarketDataDateDirectory, INDEX_CREDIT_CURVES_FILE); - if (inputCurvesResource == null) { - log.debug("Unable to load single name spread curves: file not found at {}/{}", creditMarketDataDateDirectory, - INDEX_CREDIT_CURVES_FILE); - return; + private void loadFxRates(ImmutableMarketDataBuilder builder) { + // TODO - load from CSV file - format to be defined + builder.addValue(FxRateId.of(Currency.GBP, Currency.USD), FxRate.of(Currency.GBP, Currency.USD, 1.61)); } - ResourceLocator inputStaticDataResource = getResource(creditMarketDataDateDirectory, INDEX_STATIC_DATA_FILE); - if (inputStaticDataResource == null) { - log.debug("Unable to load index static data: file not found at {}/{}", creditMarketDataDateDirectory, - INDEX_STATIC_DATA_FILE); - return; + //------------------------------------------------------------------------- + private Collection getRatesCurvesResources() { + return getAllResources(CURVES_DIR).stream() + .filter(res -> !res.getLocator().endsWith(CURVES_GROUPS_FILE)) + .filter(res -> !res.getLocator().endsWith(CURVES_SETTINGS_FILE)) + .collect(toImmutableList()); } - CharSource indexCreditCurvesSource = inputCurvesResource.getCharSource(); - CharSource indexStaticDataSource = inputStaticDataResource.getCharSource(); - MarkitIndexCreditCurveDataParser.parse(builder, indexCreditCurvesSource, indexStaticDataSource); + private void loadCreditMarketData(ImmutableMarketDataBuilder builder, LocalDate marketDataDate) { + if (!subdirectoryExists(CREDIT_DIR)) { + log.debug("No credit curves directory found"); + return; + } - } + String creditMarketDataDateDirectory = String.format( + Locale.ENGLISH, + "%s/%s", + CREDIT_DIR, + marketDataDate.format(DateTimeFormatter.ISO_LOCAL_DATE)); - //------------------------------------------------------------------------- - /** - * Gets all available resources from a given subdirectory. - * - * @param subdirectoryName the name of the subdirectory - * @return a collection of locators for the resources in the subdirectory - */ - protected abstract Collection getAllResources(String subdirectoryName); + if (!subdirectoryExists(creditMarketDataDateDirectory)) { + log.debug("Unable to load market data: directory not found at {}", creditMarketDataDateDirectory); + return; + } - /** - * Gets a specific resource from a given subdirectory. - * - * @param subdirectoryName the name of the subdirectory - * @param resourceName the name of the resource - * @return a locator for the requested resource - */ - protected abstract ResourceLocator getResource(String subdirectoryName, String resourceName); + loadCdsYieldCurves(builder, creditMarketDataDateDirectory); + loadCdsSingleNameSpreadCurves(builder, creditMarketDataDateDirectory); + loadCdsIndexSpreadCurves(builder, creditMarketDataDateDirectory); + } - /** - * Checks whether a specific subdirectory exists. - * - * @param subdirectoryName the name of the subdirectory - * @return whether the subdirectory exists - */ - protected abstract boolean subdirectoryExists(String subdirectoryName); + private void loadCdsYieldCurves(ImmutableMarketDataBuilder builder, String creditMarketDataDateDirectory) { + ResourceLocator cdsYieldCurvesResource = getResource(creditMarketDataDateDirectory, CDS_YIELD_CURVES_FILE); + if (cdsYieldCurvesResource == null) { + log.debug("Unable to load cds yield curves: file not found at {}/{}", creditMarketDataDateDirectory, + CDS_YIELD_CURVES_FILE); + return; + } + + CharSource inputSource = cdsYieldCurvesResource.getCharSource(); + Map yieldCuves = MarkitYieldCurveDataParser.parse(inputSource); + + for (IsdaYieldCurveInputsId id : yieldCuves.keySet()) { + IsdaYieldCurveInputs curveInputs = yieldCuves.get(id); + builder.addValue(id, curveInputs); + } + } + + private void loadCdsSingleNameSpreadCurves(ImmutableMarketDataBuilder builder, String creditMarketDataDateDirectory) { + ResourceLocator singleNameCurvesResource = getResource(creditMarketDataDateDirectory, SINGLE_NAME_CREDIT_CURVES_FILE); + if (singleNameCurvesResource == null) { + log.debug("Unable to load single name spread curves: file not found at {}/{}", creditMarketDataDateDirectory, + SINGLE_NAME_CREDIT_CURVES_FILE); + return; + } + + ResourceLocator singleNameStaticDataResource = getResource(creditMarketDataDateDirectory, SINGLE_NAME_STATIC_DATA_FILE); + if (singleNameStaticDataResource == null) { + log.debug("Unable to load single name static data: file not found at {}/{}", creditMarketDataDateDirectory, + SINGLE_NAME_STATIC_DATA_FILE); + return; + } + + try { + CharSource inputCreditCurvesSource = singleNameCurvesResource.getCharSource(); + CharSource inputStaticDataSource = singleNameStaticDataResource.getCharSource(); + MarkitSingleNameCreditCurveDataParser.parse(builder, inputCreditCurvesSource, inputStaticDataSource); + } catch (Exception ex) { + throw new RuntimeException(String.format( + Locale.ENGLISH, + "Unable to read single name spread curves: exception at %s/%s", + creditMarketDataDateDirectory, SINGLE_NAME_CREDIT_CURVES_FILE), ex); + } + } + + private void loadCdsIndexSpreadCurves(ImmutableMarketDataBuilder builder, String creditMarketDataDateDirectory) { + ResourceLocator inputCurvesResource = getResource(creditMarketDataDateDirectory, INDEX_CREDIT_CURVES_FILE); + if (inputCurvesResource == null) { + log.debug("Unable to load single name spread curves: file not found at {}/{}", creditMarketDataDateDirectory, + INDEX_CREDIT_CURVES_FILE); + return; + } + + ResourceLocator inputStaticDataResource = getResource(creditMarketDataDateDirectory, INDEX_STATIC_DATA_FILE); + if (inputStaticDataResource == null) { + log.debug("Unable to load index static data: file not found at {}/{}", creditMarketDataDateDirectory, + INDEX_STATIC_DATA_FILE); + return; + } + + CharSource indexCreditCurvesSource = inputCurvesResource.getCharSource(); + CharSource indexStaticDataSource = inputStaticDataResource.getCharSource(); + MarkitIndexCreditCurveDataParser.parse(builder, indexCreditCurvesSource, indexStaticDataSource); + + } + + //------------------------------------------------------------------------- + + /** + * Gets all available resources from a given subdirectory. + * + * @param subdirectoryName the name of the subdirectory + * @return a collection of locators for the resources in the subdirectory + */ + protected abstract Collection getAllResources(String subdirectoryName); + + /** + * Gets a specific resource from a given subdirectory. + * + * @param subdirectoryName the name of the subdirectory + * @param resourceName the name of the resource + * @return a locator for the requested resource + */ + protected abstract ResourceLocator getResource(String subdirectoryName, String resourceName); + + /** + * Checks whether a specific subdirectory exists. + * + * @param subdirectoryName the name of the subdirectory + * @return whether the subdirectory exists + */ + protected abstract boolean subdirectoryExists(String subdirectoryName); } diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/JarMarketDataBuilder.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/JarMarketDataBuilder.java index cc599609da..34ed9064e4 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/JarMarketDataBuilder.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/JarMarketDataBuilder.java @@ -1,6 +1,6 @@ /** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. */ package com.opengamma.strata.examples.marketdata; @@ -22,96 +22,96 @@ import java.util.stream.Collectors; */ public class JarMarketDataBuilder extends ExampleMarketDataBuilder { - /** - * The JAR file containing the expected structure of resources. - */ - private final File jarFile; - /** - * The root path to the resources within the JAR file. - */ - private final String rootPath; - /** - * A cache of JAR entries under the root path. - */ - private final ImmutableSet entries; + /** + * The JAR file containing the expected structure of resources. + */ + private final File jarFile; + /** + * The root path to the resources within the JAR file. + */ + private final String rootPath; + /** + * A cache of JAR entries under the root path. + */ + private final ImmutableSet entries; - /** - * Constructs an instance. - * - * @param jarFile the JAR file containing the expected structure of resources - * @param rootPath the root path to the resources within the JAR file - */ - public JarMarketDataBuilder(File jarFile, String rootPath) { - // classpath resources are forward-slash separated - String jarRoot = rootPath.startsWith("/") ? rootPath.substring(1) : rootPath; - if (!jarRoot.endsWith("/")) { - jarRoot += "/"; - } - this.jarFile = jarFile; - this.rootPath = jarRoot; - this.entries = getEntries(jarFile, rootPath); - } - - //------------------------------------------------------------------------- - @Override - protected Collection getAllResources(String subdirectoryName) { - String resolvedSubdirectory = subdirectoryName + "/"; - return entries.stream() - .filter(e -> e.startsWith(resolvedSubdirectory) && !e.equals(resolvedSubdirectory)) - .map(e -> getEntryLocator(rootPath + e)) - .collect(Collectors.toSet()); - } - - @Override - protected ResourceLocator getResource(String subdirectoryName, String resourceName) { - String fullLocation = String.format(Locale.ENGLISH, "%s%s/%s", rootPath, subdirectoryName, resourceName); - try (JarFile jar = new JarFile(jarFile)) { - JarEntry entry = jar.getJarEntry(fullLocation); - if (entry == null) { - return null; - } - return getEntryLocator(entry.getName()); - } catch (Exception e) { - throw new IllegalArgumentException( - Messages.format("Error loading resource from JAR file: {}", jarFile), e); - } - } - - @Override - protected boolean subdirectoryExists(String subdirectoryName) { - // classpath resources are forward-slash separated - String resolvedName = subdirectoryName.startsWith("/") ? subdirectoryName.substring(1) : subdirectoryName; - if (!resolvedName.endsWith("/")) { - resolvedName += "/"; - } - return entries.contains(resolvedName); - } - - //------------------------------------------------------------------------- - // Gets the resource locator corresponding to a given entry - private ResourceLocator getEntryLocator(String entryName) { - return ResourceLocator.of(ResourceLocator.CLASSPATH_URL_PREFIX + entryName); - } - - private static ImmutableSet getEntries(File jarFile, String rootPath) { - ImmutableSet.Builder builder = ImmutableSet.builder(); - try (JarFile jar = new JarFile(jarFile)) { - Enumeration jarEntries = jar.entries(); - while (jarEntries.hasMoreElements()) { - JarEntry entry = jarEntries.nextElement(); - String entryName = entry.getName(); - if (entryName.startsWith(rootPath) && !entryName.equals(rootPath)) { - String relativeEntryPath = entryName.substring(rootPath.length() + 1); - if (!relativeEntryPath.trim().isEmpty()) { - builder.add(relativeEntryPath); - } + /** + * Constructs an instance. + * + * @param jarFile the JAR file containing the expected structure of resources + * @param rootPath the root path to the resources within the JAR file + */ + public JarMarketDataBuilder(File jarFile, String rootPath) { + // classpath resources are forward-slash separated + String jarRoot = rootPath.startsWith("/") ? rootPath.substring(1) : rootPath; + if (!jarRoot.endsWith("/")) { + jarRoot += "/"; } - } - } catch (Exception e) { - throw new IllegalArgumentException( - Messages.format("Error scanning entries in JAR file: {}", jarFile), e); + this.jarFile = jarFile; + this.rootPath = jarRoot; + this.entries = getEntries(jarFile, rootPath); + } + + //------------------------------------------------------------------------- + @Override + protected Collection getAllResources(String subdirectoryName) { + String resolvedSubdirectory = subdirectoryName + "/"; + return entries.stream() + .filter(e -> e.startsWith(resolvedSubdirectory) && !e.equals(resolvedSubdirectory)) + .map(e -> getEntryLocator(rootPath + e)) + .collect(Collectors.toSet()); + } + + @Override + protected ResourceLocator getResource(String subdirectoryName, String resourceName) { + String fullLocation = String.format(Locale.ENGLISH, "%s%s/%s", rootPath, subdirectoryName, resourceName); + try (JarFile jar = new JarFile(jarFile)) { + JarEntry entry = jar.getJarEntry(fullLocation); + if (entry == null) { + return null; + } + return getEntryLocator(entry.getName()); + } catch (Exception e) { + throw new IllegalArgumentException( + Messages.format("Error loading resource from JAR file: {}", jarFile), e); + } + } + + @Override + protected boolean subdirectoryExists(String subdirectoryName) { + // classpath resources are forward-slash separated + String resolvedName = subdirectoryName.startsWith("/") ? subdirectoryName.substring(1) : subdirectoryName; + if (!resolvedName.endsWith("/")) { + resolvedName += "/"; + } + return entries.contains(resolvedName); + } + + //------------------------------------------------------------------------- + // Gets the resource locator corresponding to a given entry + private ResourceLocator getEntryLocator(String entryName) { + return ResourceLocator.of(ResourceLocator.CLASSPATH_URL_PREFIX + entryName); + } + + private static ImmutableSet getEntries(File jarFile, String rootPath) { + ImmutableSet.Builder builder = ImmutableSet.builder(); + try (JarFile jar = new JarFile(jarFile)) { + Enumeration jarEntries = jar.entries(); + while (jarEntries.hasMoreElements()) { + JarEntry entry = jarEntries.nextElement(); + String entryName = entry.getName(); + if (entryName.startsWith(rootPath) && !entryName.equals(rootPath)) { + String relativeEntryPath = entryName.substring(rootPath.length() + 1); + if (!relativeEntryPath.trim().isEmpty()) { + builder.add(relativeEntryPath); + } + } + } + } catch (Exception e) { + throw new IllegalArgumentException( + Messages.format("Error scanning entries in JAR file: {}", jarFile), e); + } + return builder.build(); } - return builder.build(); - } } diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitIndexCreditCurveDataParser.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitIndexCreditCurveDataParser.java index 7b3bdd8c86..9f220e2e58 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitIndexCreditCurveDataParser.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitIndexCreditCurveDataParser.java @@ -1,6 +1,6 @@ /** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. */ package com.opengamma.strata.examples.marketdata.credit.markit; @@ -44,217 +44,219 @@ import java.util.Map; */ public class MarkitIndexCreditCurveDataParser { - // Markit date format with the month in full caps. e.g. 11-JUL-14 - private static final DateTimeFormatter DATE_FORMAT = new DateTimeFormatterBuilder() - .parseCaseInsensitive().appendPattern("dd-MMM-uu").toFormatter(Locale.ENGLISH); + // Markit date format with the month in full caps. e.g. 11-JUL-14 + private static final DateTimeFormatter DATE_FORMAT = new DateTimeFormatterBuilder() + .parseCaseInsensitive().appendPattern("dd-MMM-uu").toFormatter(Locale.ENGLISH); - enum Columns { + enum Columns { - Series("Series"), - Version("Version"), - Term("Term"), - RedCode("RED Code"), - Maturity("Maturity"), - CompositeSpread("Composite Spread"), - ModelSpread("Model Spread"); + Series("Series"), + Version("Version"), + Term("Term"), + RedCode("RED Code"), + Maturity("Maturity"), + CompositeSpread("Composite Spread"), + ModelSpread("Model Spread"); - private final String columnName; + private final String columnName; - Columns(String columnName) { - this.columnName = columnName; - } - - public String getColumnName() { - return columnName; - } - } - - /** - * Parses the specified sources. - * - * @param builder the market data builder that the resulting curve and recovery rate items should be loaded into - * @param curveSource the source of curve data to parse - * @param staticDataSource the source of static data to parse - */ - public static void parse( - ImmutableMarketDataBuilder builder, - CharSource curveSource, - CharSource staticDataSource) { - - Map> curveData = Maps.newHashMap(); - Map staticDataMap = parseStaticData(staticDataSource); - - CsvFile csv = CsvFile.of(curveSource, true); - for (CsvRow row : csv.rows()) { - String seriesText = row.getField(Columns.Series.getColumnName()); - String versionText = row.getField(Columns.Version.getColumnName()); - String termText = row.getField(Columns.Term.getColumnName()); - String redCodeText = row.getField(Columns.RedCode.getColumnName()); - String maturityText = row.getField(Columns.Maturity.getColumnName()); - String compositeSpreadText = row.getField(Columns.CompositeSpread.getColumnName()); - String modelSpreadText = row.getField(Columns.ModelSpread.getColumnName()); - - StandardId indexId = MarkitRedCode.id(redCodeText); - int indexSeries = Integer.parseInt(seriesText); - int indexAnnexVersion = Integer.parseInt(versionText); - - IsdaIndexCreditCurveInputsId id = IsdaIndexCreditCurveInputsId.of( - IndexReferenceInformation.of( - indexId, - indexSeries, - indexAnnexVersion)); - - Tenor term = Tenor.parse(termText); - LocalDate maturity = LocalDate.parse(maturityText, DATE_FORMAT); - - double spread; - if (compositeSpreadText.isEmpty()) { - if (modelSpreadText.isEmpty()) { - // there is no rate for this row, continue - continue; + Columns(String columnName) { + this.columnName = columnName; } - // fall back to the model rate is the composite is missing - spread = parseRate(modelSpreadText); - } else { - // prefer the composite rate if it is present - spread = parseRate(compositeSpreadText); - } - List points = curveData.get(id); - if (points == null) { - points = Lists.newArrayList(); - curveData.put(id, points); - } - points.add(new Point(term, maturity, spread)); + public String getColumnName() { + return columnName; + } } - for (IsdaIndexCreditCurveInputsId curveId : curveData.keySet()) { - MarkitRedCode redCode = MarkitRedCode.from(curveId.getReferenceInformation().getIndexId()); - StaticData staticData = staticDataMap.get(redCode); - ArgChecker.notNull(staticData, "Did not find a static data record for " + redCode); - CdsConvention convention = staticData.getConvention(); - double recoveryRate = staticData.getRecoveryRate(); - double indexFactor = staticData.getIndexFactor(); - // TODO add fromDate handling + /** + * Parses the specified sources. + * + * @param builder the market data builder that the resulting curve and recovery rate items should be loaded into + * @param curveSource the source of curve data to parse + * @param staticDataSource the source of static data to parse + */ + public static void parse( + ImmutableMarketDataBuilder builder, + CharSource curveSource, + CharSource staticDataSource) { - String creditCurveName = curveId.toString(); + Map> curveData = Maps.newHashMap(); + Map staticDataMap = parseStaticData(staticDataSource); - List points = curveData.get(curveId); + CsvFile csv = CsvFile.of(curveSource, true); + for (CsvRow row : csv.rows()) { + String seriesText = row.getField(Columns.Series.getColumnName()); + String versionText = row.getField(Columns.Version.getColumnName()); + String termText = row.getField(Columns.Term.getColumnName()); + String redCodeText = row.getField(Columns.RedCode.getColumnName()); + String maturityText = row.getField(Columns.Maturity.getColumnName()); + String compositeSpreadText = row.getField(Columns.CompositeSpread.getColumnName()); + String modelSpreadText = row.getField(Columns.ModelSpread.getColumnName()); - Period[] periods = points.stream().map(s -> s.getTenor().getPeriod()).toArray(Period[]::new); - LocalDate[] endDates = points.stream().map(s -> s.getDate()).toArray(LocalDate[]::new); - double[] rates = points.stream().mapToDouble(s -> s.getRate()).toArray(); + StandardId indexId = MarkitRedCode.id(redCodeText); + int indexSeries = Integer.parseInt(seriesText); + int indexAnnexVersion = Integer.parseInt(versionText); - IsdaCreditCurveInputs curveInputs = IsdaCreditCurveInputs.of( - CurveName.of(creditCurveName), - periods, - endDates, - rates, - convention, - indexFactor); + IsdaIndexCreditCurveInputsId id = IsdaIndexCreditCurveInputsId.of( + IndexReferenceInformation.of( + indexId, + indexSeries, + indexAnnexVersion)); - builder.addValue(curveId, curveInputs); + Tenor term = Tenor.parse(termText); + LocalDate maturity = LocalDate.parse(maturityText, DATE_FORMAT); - IsdaIndexRecoveryRateId recoveryRateId = IsdaIndexRecoveryRateId.of(curveId.getReferenceInformation()); - CdsRecoveryRate cdsRecoveryRate = CdsRecoveryRate.of(recoveryRate); + double spread; + if (compositeSpreadText.isEmpty()) { + if (modelSpreadText.isEmpty()) { + // there is no rate for this row, continue + continue; + } + // fall back to the model rate is the composite is missing + spread = parseRate(modelSpreadText); + } else { + // prefer the composite rate if it is present + spread = parseRate(compositeSpreadText); + } - builder.addValue(recoveryRateId, cdsRecoveryRate); - } - } + List points = curveData.get(id); + if (points == null) { + points = Lists.newArrayList(); + curveData.put(id, points); + } + points.add(new Point(term, maturity, spread)); + } - // parses the static data file - private static Map parseStaticData(CharSource source) { - CsvFile csv = CsvFile.of(source, true); + for (IsdaIndexCreditCurveInputsId curveId : curveData.keySet()) { + MarkitRedCode redCode = MarkitRedCode.from(curveId.getReferenceInformation().getIndexId()); + StaticData staticData = staticDataMap.get(redCode); + ArgChecker.notNull(staticData, "Did not find a static data record for " + redCode); + CdsConvention convention = staticData.getConvention(); + double recoveryRate = staticData.getRecoveryRate(); + double indexFactor = staticData.getIndexFactor(); + // TODO add fromDate handling - Map result = Maps.newHashMap(); - for (CsvRow row : csv.rows()) { - String redCodeText = row.getField("RedCode"); - String fromDateText = row.getField("From Date"); - String conventionText = row.getField("Convention"); - String recoveryRateText = row.getField("Recovery Rate"); - String indexFactorText = row.getField("Index Factor"); + String creditCurveName = curveId.toString(); - MarkitRedCode redCode = MarkitRedCode.of(redCodeText); - LocalDate fromDate = LocalDate.parse(fromDateText, DATE_FORMAT); - CdsConvention convention = CdsConvention.of(conventionText); - double recoveryRate = parseRate(recoveryRateText); - double indexFactor = Double.parseDouble(indexFactorText); + List points = curveData.get(curveId); - result.put(redCode, new StaticData(fromDate, convention, recoveryRate, indexFactor)); - } - return result; - } + Period[] periods = points.stream().map(s -> s.getTenor().getPeriod()).toArray(Period[]::new); + LocalDate[] endDates = points.stream().map(s -> s.getDate()).toArray(LocalDate[]::new); + double[] rates = points.stream().mapToDouble(s -> s.getRate()).toArray(); - //------------------------------------------------------------------------- - /** - * Stores the parsed static data. - */ - private static class StaticData { + IsdaCreditCurveInputs curveInputs = IsdaCreditCurveInputs.of( + CurveName.of(creditCurveName), + periods, + endDates, + rates, + convention, + indexFactor); - private LocalDate fromDate; - private CdsConvention convention; - private double recoveryRate; - private double indexFactor; + builder.addValue(curveId, curveInputs); - private StaticData(LocalDate fromDate, CdsConvention convention, double recoveryRate, double indexFactor) { - this.fromDate = fromDate; - this.convention = convention; - this.recoveryRate = recoveryRate; - this.indexFactor = indexFactor; + IsdaIndexRecoveryRateId recoveryRateId = IsdaIndexRecoveryRateId.of(curveId.getReferenceInformation()); + CdsRecoveryRate cdsRecoveryRate = CdsRecoveryRate.of(recoveryRate); + + builder.addValue(recoveryRateId, cdsRecoveryRate); + } } - @SuppressWarnings("unused") - public LocalDate getFromDate() { - return fromDate; + // parses the static data file + private static Map parseStaticData(CharSource source) { + CsvFile csv = CsvFile.of(source, true); + + Map result = Maps.newHashMap(); + for (CsvRow row : csv.rows()) { + String redCodeText = row.getField("RedCode"); + String fromDateText = row.getField("From Date"); + String conventionText = row.getField("Convention"); + String recoveryRateText = row.getField("Recovery Rate"); + String indexFactorText = row.getField("Index Factor"); + + MarkitRedCode redCode = MarkitRedCode.of(redCodeText); + LocalDate fromDate = LocalDate.parse(fromDateText, DATE_FORMAT); + CdsConvention convention = CdsConvention.of(conventionText); + double recoveryRate = parseRate(recoveryRateText); + double indexFactor = Double.parseDouble(indexFactorText); + + result.put(redCode, new StaticData(fromDate, convention, recoveryRate, indexFactor)); + } + return result; } - public CdsConvention getConvention() { - return convention; + //------------------------------------------------------------------------- + + /** + * Stores the parsed static data. + */ + private static class StaticData { + + private LocalDate fromDate; + private CdsConvention convention; + private double recoveryRate; + private double indexFactor; + + private StaticData(LocalDate fromDate, CdsConvention convention, double recoveryRate, double indexFactor) { + this.fromDate = fromDate; + this.convention = convention; + this.recoveryRate = recoveryRate; + this.indexFactor = indexFactor; + } + + @SuppressWarnings("unused") + public LocalDate getFromDate() { + return fromDate; + } + + public CdsConvention getConvention() { + return convention; + } + + public double getRecoveryRate() { + return recoveryRate; + } + + public double getIndexFactor() { + return indexFactor; + } } - public double getRecoveryRate() { - return recoveryRate; + //------------------------------------------------------------------------- + + /** + * Stores the parsed data points. + */ + private static class Point { + private final Tenor tenor; + + private final LocalDate date; + + private final double rate; + + private Point(Tenor tenor, LocalDate date, double rate) { + this.tenor = tenor; + this.date = date; + this.rate = rate; + } + + public Tenor getTenor() { + return tenor; + } + + public LocalDate getDate() { + return date; + } + + public double getRate() { + return rate; + } } - public double getIndexFactor() { - return indexFactor; + // Converts from a string percentage rate with a percent sign to a double rate + // e.g. 0.12% => 0.0012d + private static double parseRate(String input) { + return Double.parseDouble(input.replace("%", "")) / 100d; } - } - - //------------------------------------------------------------------------- - /** - * Stores the parsed data points. - */ - private static class Point { - private final Tenor tenor; - - private final LocalDate date; - - private final double rate; - - private Point(Tenor tenor, LocalDate date, double rate) { - this.tenor = tenor; - this.date = date; - this.rate = rate; - } - - public Tenor getTenor() { - return tenor; - } - - public LocalDate getDate() { - return date; - } - - public double getRate() { - return rate; - } - } - - // Converts from a string percentage rate with a percent sign to a double rate - // e.g. 0.12% => 0.0012d - private static double parseRate(String input) { - return Double.parseDouble(input.replace("%", "")) / 100d; - } } diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitRedCode.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitRedCode.java index bae17190a3..0581a6c772 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitRedCode.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitRedCode.java @@ -1,6 +1,6 @@ /** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. */ package com.opengamma.strata.examples.marketdata.credit.markit; @@ -19,72 +19,74 @@ import org.joda.convert.FromString; * http://www.markit.com/product/reference-data-cds */ public final class MarkitRedCode - extends TypedString { + extends TypedString { - /** - * Serialization version. - */ - private static final long serialVersionUID = 1L; + /** + * Serialization version. + */ + private static final long serialVersionUID = 1L; - /** - * Scheme used in an OpenGamma {@link StandardId} where the value is a Markit RED code. - */ - public static final String MARKIT_REDCODE_SCHEME = "MarkitRedCode"; + /** + * Scheme used in an OpenGamma {@link StandardId} where the value is a Markit RED code. + */ + public static final String MARKIT_REDCODE_SCHEME = "MarkitRedCode"; - //------------------------------------------------------------------------- - /** - * Obtains an instance from the specified name. - *

- * RED codes must be 6 or 9 characters long. - * - * @param name the name of the field - * @return a RED code - */ - @FromString - public static MarkitRedCode of(String name) { - ArgChecker.isTrue(name.length() == 6 || name.length() == 9, "RED Code must be exactly 6 or 9 characters"); - return new MarkitRedCode(name); - } + //------------------------------------------------------------------------- - /** - * Converts from a standard identifier ensuring the scheme is correct. - * - * @param id standard id identifying a RED code - * @return the equivalent RED code - */ - public static MarkitRedCode from(StandardId id) { - Preconditions.checkArgument(id.getScheme().equals(MARKIT_REDCODE_SCHEME)); - return MarkitRedCode.of(id.getValue()); - } + /** + * Obtains an instance from the specified name. + *

+ * RED codes must be 6 or 9 characters long. + * + * @param name the name of the field + * @return a RED code + */ + @FromString + public static MarkitRedCode of(String name) { + ArgChecker.isTrue(name.length() == 6 || name.length() == 9, "RED Code must be exactly 6 or 9 characters"); + return new MarkitRedCode(name); + } - /** - * Creates a standard identifier using the correct Markit RED code scheme. - * - * @param name the Markit RED code, 6 or 9 characters long - * @return the equivalent standard identifier - */ - public static StandardId id(String name) { - ArgChecker.isTrue(name.length() == 6 || name.length() == 9, "RED Code must be exactly 6 or 9 characters"); - return StandardId.of(MARKIT_REDCODE_SCHEME, name); - } + /** + * Converts from a standard identifier ensuring the scheme is correct. + * + * @param id standard id identifying a RED code + * @return the equivalent RED code + */ + public static MarkitRedCode from(StandardId id) { + Preconditions.checkArgument(id.getScheme().equals(MARKIT_REDCODE_SCHEME)); + return MarkitRedCode.of(id.getValue()); + } - /** - * Creates an instance. - * - * @param name the RED code - */ - private MarkitRedCode(String name) { - super(name); - } + /** + * Creates a standard identifier using the correct Markit RED code scheme. + * + * @param name the Markit RED code, 6 or 9 characters long + * @return the equivalent standard identifier + */ + public static StandardId id(String name) { + ArgChecker.isTrue(name.length() == 6 || name.length() == 9, "RED Code must be exactly 6 or 9 characters"); + return StandardId.of(MARKIT_REDCODE_SCHEME, name); + } - //------------------------------------------------------------------------- - /** - * Converts this RED code to a standard identifier. - * - * @return the standard identifier - */ - public StandardId toStandardId() { - return StandardId.of(MARKIT_REDCODE_SCHEME, getName()); - } + /** + * Creates an instance. + * + * @param name the RED code + */ + private MarkitRedCode(String name) { + super(name); + } + + //------------------------------------------------------------------------- + + /** + * Converts this RED code to a standard identifier. + * + * @return the standard identifier + */ + public StandardId toStandardId() { + return StandardId.of(MARKIT_REDCODE_SCHEME, getName()); + } } diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitRestructuringClause.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitRestructuringClause.java index 35e87365a6..be4cdd1b16 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitRestructuringClause.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitRestructuringClause.java @@ -1,6 +1,6 @@ /** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. */ package com.opengamma.strata.examples.marketdata.credit.markit; @@ -14,96 +14,97 @@ import com.opengamma.strata.product.credit.RestructuringClause; */ public enum MarkitRestructuringClause { - /** - * Modified-Modified Restructuring 2003. - */ - MM, - /** - * Modified-Modified Restructuring 2014. - */ - MM14, - /** - * Modified Restructuring 2003. - */ - MR, - /** - * Modified Restructuring 2014. - */ - MR14, - /** - * Cum/Old/Full Restructuring 2003. - */ - CR, - /** - * Cum/Old/Full Restructuring 2014. - */ - CR14, - /** - * Ex/No restructuring 2003. - */ - XR, - /** - * Ex/No restructuring 2014. - */ - XR14; + /** + * Modified-Modified Restructuring 2003. + */ + MM, + /** + * Modified-Modified Restructuring 2014. + */ + MM14, + /** + * Modified Restructuring 2003. + */ + MR, + /** + * Modified Restructuring 2014. + */ + MR14, + /** + * Cum/Old/Full Restructuring 2003. + */ + CR, + /** + * Cum/Old/Full Restructuring 2014. + */ + CR14, + /** + * Ex/No restructuring 2003. + */ + XR, + /** + * Ex/No restructuring 2014. + */ + XR14; - //------------------------------------------------------------------------- - /** - * Converts Markit code to standard restructuring clause. - * - * @return the converted clause - */ - public RestructuringClause translate() { - switch (this) { - case MM: - return RestructuringClause.MOD_MOD_RESTRUCTURING_2003; - case MM14: - return RestructuringClause.MOD_MOD_RESTRUCTURING_2014; - case MR: - return RestructuringClause.MODIFIED_RESTRUCTURING_2003; - case MR14: - return RestructuringClause.MODIFIED_RESTRUCTURING_2014; - case CR: - return RestructuringClause.CUM_RESTRUCTURING_2003; - case CR14: - return RestructuringClause.CUM_RESTRUCTURING_2014; - case XR: - return RestructuringClause.NO_RESTRUCTURING_2003; - case XR14: - return RestructuringClause.NO_RESTRUCTURING_2014; - default: - throw new IllegalStateException("Unmapped restructuring clause. Do not have mapping for " + this); + //------------------------------------------------------------------------- + + /** + * Converts Markit code to standard restructuring clause. + * + * @return the converted clause + */ + public RestructuringClause translate() { + switch (this) { + case MM: + return RestructuringClause.MOD_MOD_RESTRUCTURING_2003; + case MM14: + return RestructuringClause.MOD_MOD_RESTRUCTURING_2014; + case MR: + return RestructuringClause.MODIFIED_RESTRUCTURING_2003; + case MR14: + return RestructuringClause.MODIFIED_RESTRUCTURING_2014; + case CR: + return RestructuringClause.CUM_RESTRUCTURING_2003; + case CR14: + return RestructuringClause.CUM_RESTRUCTURING_2014; + case XR: + return RestructuringClause.NO_RESTRUCTURING_2003; + case XR14: + return RestructuringClause.NO_RESTRUCTURING_2014; + default: + throw new IllegalStateException("Unmapped restructuring clause. Do not have mapping for " + this); + } } - } - /** - * Converts restructuring clause to Markit equivalent. - * - * @param restructuringClause the clause to convert - * @return the converted clause - */ - public static MarkitRestructuringClause from(RestructuringClause restructuringClause) { - switch (restructuringClause) { - case MOD_MOD_RESTRUCTURING_2003: - return MM; - case MOD_MOD_RESTRUCTURING_2014: - return MM14; - case MODIFIED_RESTRUCTURING_2003: - return MR; - case MODIFIED_RESTRUCTURING_2014: - return MR14; - case CUM_RESTRUCTURING_2003: - return CR; - case CUM_RESTRUCTURING_2014: - return CR14; - case NO_RESTRUCTURING_2003: - return XR; - case NO_RESTRUCTURING_2014: - return XR14; - default: - throw new UnsupportedOperationException("Unknown restructuring clause. Do not have mapping for " + restructuringClause); + /** + * Converts restructuring clause to Markit equivalent. + * + * @param restructuringClause the clause to convert + * @return the converted clause + */ + public static MarkitRestructuringClause from(RestructuringClause restructuringClause) { + switch (restructuringClause) { + case MOD_MOD_RESTRUCTURING_2003: + return MM; + case MOD_MOD_RESTRUCTURING_2014: + return MM14; + case MODIFIED_RESTRUCTURING_2003: + return MR; + case MODIFIED_RESTRUCTURING_2014: + return MR14; + case CUM_RESTRUCTURING_2003: + return CR; + case CUM_RESTRUCTURING_2014: + return CR14; + case NO_RESTRUCTURING_2003: + return XR; + case NO_RESTRUCTURING_2014: + return XR14; + default: + throw new UnsupportedOperationException("Unknown restructuring clause. Do not have mapping for " + restructuringClause); + } } - } } diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitSeniorityLevel.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitSeniorityLevel.java index 53829372d4..8aca3087d0 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitSeniorityLevel.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitSeniorityLevel.java @@ -1,6 +1,6 @@ /** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. */ package com.opengamma.strata.examples.marketdata.credit.markit; @@ -14,75 +14,76 @@ import com.opengamma.strata.product.credit.SeniorityLevel; */ public enum MarkitSeniorityLevel { - /** - * Senior domestic. - */ - SECDOM, + /** + * Senior domestic. + */ + SECDOM, - /** - * Senior foreign. - */ - SNRFOR, + /** + * Senior foreign. + */ + SNRFOR, - /** - * Subordinate, Lower Tier 2. - */ - SUBLT2, + /** + * Subordinate, Lower Tier 2. + */ + SUBLT2, - /** - * Subordinate Tier 1. - */ - PREFT1, + /** + * Subordinate Tier 1. + */ + PREFT1, - /** - * Subordinate, Upper Tier 2. - */ - JRSUBUT2; + /** + * Subordinate, Upper Tier 2. + */ + JRSUBUT2; - //------------------------------------------------------------------------- - /** - * Converts Markit code to standard seniority level. - * - * @return the converted level - */ - public SeniorityLevel translate() { - switch (this) { - case SECDOM: - return SeniorityLevel.SENIOR_SECURED_DOMESTIC; - case SNRFOR: - return SeniorityLevel.SENIOR_UNSECURED_FOREIGN; - case SUBLT2: - return SeniorityLevel.SUBORDINATE_LOWER_TIER_2; - case PREFT1: - return SeniorityLevel.SUBORDINATE_TIER_1; - case JRSUBUT2: - return SeniorityLevel.SUBORDINATE_UPPER_TIER_2; - default: - throw new IllegalStateException("Unmapped seniority level. Do not have mapping for " + this); + //------------------------------------------------------------------------- + + /** + * Converts Markit code to standard seniority level. + * + * @return the converted level + */ + public SeniorityLevel translate() { + switch (this) { + case SECDOM: + return SeniorityLevel.SENIOR_SECURED_DOMESTIC; + case SNRFOR: + return SeniorityLevel.SENIOR_UNSECURED_FOREIGN; + case SUBLT2: + return SeniorityLevel.SUBORDINATE_LOWER_TIER_2; + case PREFT1: + return SeniorityLevel.SUBORDINATE_TIER_1; + case JRSUBUT2: + return SeniorityLevel.SUBORDINATE_UPPER_TIER_2; + default: + throw new IllegalStateException("Unmapped seniority level. Do not have mapping for " + this); + } } - } - /** - * Converts seniority level to Markit equivalent. - * - * @param seniorityLevel the level to convert - * @return the converted level - */ - public static MarkitSeniorityLevel from(SeniorityLevel seniorityLevel) { - switch (seniorityLevel) { - case SENIOR_SECURED_DOMESTIC: - return SECDOM; - case SENIOR_UNSECURED_FOREIGN: - return SNRFOR; - case SUBORDINATE_LOWER_TIER_2: - return SUBLT2; - case SUBORDINATE_TIER_1: - return PREFT1; - case SUBORDINATE_UPPER_TIER_2: - return JRSUBUT2; - default: - throw new IllegalArgumentException("Unknown seniority level. Do not have mapping for " + seniorityLevel); + /** + * Converts seniority level to Markit equivalent. + * + * @param seniorityLevel the level to convert + * @return the converted level + */ + public static MarkitSeniorityLevel from(SeniorityLevel seniorityLevel) { + switch (seniorityLevel) { + case SENIOR_SECURED_DOMESTIC: + return SECDOM; + case SENIOR_UNSECURED_FOREIGN: + return SNRFOR; + case SUBORDINATE_LOWER_TIER_2: + return SUBLT2; + case SUBORDINATE_TIER_1: + return PREFT1; + case SUBORDINATE_UPPER_TIER_2: + return JRSUBUT2; + default: + throw new IllegalArgumentException("Unknown seniority level. Do not have mapping for " + seniorityLevel); + } } - } } diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitSingleNameCreditCurveDataParser.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitSingleNameCreditCurveDataParser.java index 3ad85682a9..1da9114513 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitSingleNameCreditCurveDataParser.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitSingleNameCreditCurveDataParser.java @@ -1,6 +1,6 @@ /** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. */ package com.opengamma.strata.examples.marketdata.credit.markit; @@ -56,141 +56,141 @@ import java.util.Scanner; */ public class MarkitSingleNameCreditCurveDataParser { - // Markit date format with the month in full caps. e.g. 11-JUL-14 - private static final DateTimeFormatter DATE_FORMAT = new DateTimeFormatterBuilder() - .parseCaseInsensitive().appendPattern("dd-MMM-uu").toFormatter(Locale.ENGLISH); + // Markit date format with the month in full caps. e.g. 11-JUL-14 + private static final DateTimeFormatter DATE_FORMAT = new DateTimeFormatterBuilder() + .parseCaseInsensitive().appendPattern("dd-MMM-uu").toFormatter(Locale.ENGLISH); - // Index used to access the specified columns of string data in the file - private static final int DATE = 0; - private static final int RED_CODE = 3; - private static final int TIER = 4; - private static final int CURRENCY = 5; - private static final int DOCS_CLAUSE = 6; - private static final int FIRST_SPREAD_COLUMN = 8; - private static final int RECOVERY = 19; + // Index used to access the specified columns of string data in the file + private static final int DATE = 0; + private static final int RED_CODE = 3; + private static final int TIER = 4; + private static final int CURRENCY = 5; + private static final int DOCS_CLAUSE = 6; + private static final int FIRST_SPREAD_COLUMN = 8; + private static final int RECOVERY = 19; - private static final List TENORS = ImmutableList.of( - Tenor.TENOR_6M, - Tenor.TENOR_1Y, - Tenor.TENOR_2Y, - Tenor.TENOR_3Y, - Tenor.TENOR_4Y, - Tenor.TENOR_5Y, - Tenor.TENOR_7Y, - Tenor.TENOR_10Y, - Tenor.TENOR_15Y, - Tenor.TENOR_20Y, - Tenor.TENOR_30Y); + private static final List TENORS = ImmutableList.of( + Tenor.TENOR_6M, + Tenor.TENOR_1Y, + Tenor.TENOR_2Y, + Tenor.TENOR_3Y, + Tenor.TENOR_4Y, + Tenor.TENOR_5Y, + Tenor.TENOR_7Y, + Tenor.TENOR_10Y, + Tenor.TENOR_15Y, + Tenor.TENOR_20Y, + Tenor.TENOR_30Y); - /** - * Parses the specified sources. - * - * @param builder the market data builder that the resulting curve and recovery rate items should be loaded into - * @param curveSource the source of curve data to parse - * @param staticDataSource the source of static data to parse - */ - public static void parse( - ImmutableMarketDataBuilder builder, - CharSource curveSource, - CharSource staticDataSource) { + /** + * Parses the specified sources. + * + * @param builder the market data builder that the resulting curve and recovery rate items should be loaded into + * @param curveSource the source of curve data to parse + * @param staticDataSource the source of static data to parse + */ + public static void parse( + ImmutableMarketDataBuilder builder, + CharSource curveSource, + CharSource staticDataSource) { - Map conventions = parseStaticData(staticDataSource); - try (Scanner scanner = new Scanner(curveSource.openStream())) { - while (scanner.hasNextLine()) { + Map conventions = parseStaticData(staticDataSource); + try (Scanner scanner = new Scanner(curveSource.openStream())) { + while (scanner.hasNextLine()) { - String line = scanner.nextLine(); - // skip over header rows - if (line.startsWith("V5 CDS Composites by Convention") || - line.trim().isEmpty() || - line.startsWith("\"Date\",")) { - continue; + String line = scanner.nextLine(); + // skip over header rows + if (line.startsWith("V5 CDS Composites by Convention") || + line.trim().isEmpty() || + line.startsWith("\"Date\",")) { + continue; + } + String[] columns = line.split(","); + for (int i = 0; i < columns.length; i++) { + // get rid of quotes and trim the string + columns[i] = columns[i].replaceFirst("^\"", "").replaceFirst("\"$", "").trim(); + } + + LocalDate valuationDate = LocalDate.parse(columns[DATE], DATE_FORMAT); + + MarkitRedCode redCode = MarkitRedCode.of(columns[RED_CODE]); + SeniorityLevel seniorityLevel = MarkitSeniorityLevel.valueOf(columns[TIER]).translate(); + Currency currency = Currency.parse(columns[CURRENCY]); + RestructuringClause restructuringClause = MarkitRestructuringClause.valueOf(columns[DOCS_CLAUSE]).translate(); + + double recoveryRate = parseRate(columns[RECOVERY]); + + SingleNameReferenceInformation referenceInformation = SingleNameReferenceInformation.of( + redCode.toStandardId(), + seniorityLevel, + currency, + restructuringClause); + + IsdaSingleNameCreditCurveInputsId curveId = IsdaSingleNameCreditCurveInputsId.of(referenceInformation); + + List periodsList = Lists.newArrayList(); + List ratesList = Lists.newArrayList(); + for (int i = 0; i < TENORS.size(); i++) { + String rateString = columns[FIRST_SPREAD_COLUMN + i]; + if (rateString.isEmpty()) { + // no data at this point + continue; + } + periodsList.add(TENORS.get(i).getPeriod()); + ratesList.add(parseRate(rateString)); + } + + String creditCurveName = curveId.toString(); + + CdsConvention cdsConvention = conventions.get(redCode); + + Period[] periods = periodsList.stream().toArray(Period[]::new); + LocalDate[] endDates = Lists + .newArrayList(periods) + .stream() + .map(p -> cdsConvention.calculateUnadjustedMaturityDateFromValuationDate(valuationDate, p)) + .toArray(LocalDate[]::new); + + double[] rates = ratesList.stream().mapToDouble(s -> s).toArray(); + double unitScalingFactor = 1d; // for single name, we don't do any scaling (no index factor) + + IsdaCreditCurveInputs curveInputs = IsdaCreditCurveInputs.of( + CurveName.of(creditCurveName), + periods, + endDates, + rates, + cdsConvention, + unitScalingFactor); + + builder.addValue(curveId, curveInputs); + + IsdaSingleNameRecoveryRateId recoveryRateId = IsdaSingleNameRecoveryRateId.of(referenceInformation); + CdsRecoveryRate cdsRecoveryRate = CdsRecoveryRate.of(recoveryRate); + + builder.addValue(recoveryRateId, cdsRecoveryRate); + + } + } catch (IOException ex) { + throw new UncheckedIOException(ex); } - String[] columns = line.split(","); - for (int i = 0; i < columns.length; i++) { - // get rid of quotes and trim the string - columns[i] = columns[i].replaceFirst("^\"", "").replaceFirst("\"$", "").trim(); - } - - LocalDate valuationDate = LocalDate.parse(columns[DATE], DATE_FORMAT); - - MarkitRedCode redCode = MarkitRedCode.of(columns[RED_CODE]); - SeniorityLevel seniorityLevel = MarkitSeniorityLevel.valueOf(columns[TIER]).translate(); - Currency currency = Currency.parse(columns[CURRENCY]); - RestructuringClause restructuringClause = MarkitRestructuringClause.valueOf(columns[DOCS_CLAUSE]).translate(); - - double recoveryRate = parseRate(columns[RECOVERY]); - - SingleNameReferenceInformation referenceInformation = SingleNameReferenceInformation.of( - redCode.toStandardId(), - seniorityLevel, - currency, - restructuringClause); - - IsdaSingleNameCreditCurveInputsId curveId = IsdaSingleNameCreditCurveInputsId.of(referenceInformation); - - List periodsList = Lists.newArrayList(); - List ratesList = Lists.newArrayList(); - for (int i = 0; i < TENORS.size(); i++) { - String rateString = columns[FIRST_SPREAD_COLUMN + i]; - if (rateString.isEmpty()) { - // no data at this point - continue; - } - periodsList.add(TENORS.get(i).getPeriod()); - ratesList.add(parseRate(rateString)); - } - - String creditCurveName = curveId.toString(); - - CdsConvention cdsConvention = conventions.get(redCode); - - Period[] periods = periodsList.stream().toArray(Period[]::new); - LocalDate[] endDates = Lists - .newArrayList(periods) - .stream() - .map(p -> cdsConvention.calculateUnadjustedMaturityDateFromValuationDate(valuationDate, p)) - .toArray(LocalDate[]::new); - - double[] rates = ratesList.stream().mapToDouble(s -> s).toArray(); - double unitScalingFactor = 1d; // for single name, we don't do any scaling (no index factor) - - IsdaCreditCurveInputs curveInputs = IsdaCreditCurveInputs.of( - CurveName.of(creditCurveName), - periods, - endDates, - rates, - cdsConvention, - unitScalingFactor); - - builder.addValue(curveId, curveInputs); - - IsdaSingleNameRecoveryRateId recoveryRateId = IsdaSingleNameRecoveryRateId.of(referenceInformation); - CdsRecoveryRate cdsRecoveryRate = CdsRecoveryRate.of(recoveryRate); - - builder.addValue(recoveryRateId, cdsRecoveryRate); - - } - } catch (IOException ex) { - throw new UncheckedIOException(ex); } - } - // parses the static data file of RED code to convention - private static Map parseStaticData(CharSource source) { - CsvFile csv = CsvFile.of(source, true); - Map result = Maps.newHashMap(); - for (CsvRow row : csv.rows()) { - String redCodeText = row.getField("RedCode"); - String conventionText = row.getField("Convention"); - result.put(MarkitRedCode.of(redCodeText), CdsConvention.of(conventionText)); + // parses the static data file of RED code to convention + private static Map parseStaticData(CharSource source) { + CsvFile csv = CsvFile.of(source, true); + Map result = Maps.newHashMap(); + for (CsvRow row : csv.rows()) { + String redCodeText = row.getField("RedCode"); + String conventionText = row.getField("Convention"); + result.put(MarkitRedCode.of(redCodeText), CdsConvention.of(conventionText)); + } + return result; } - return result; - } - // Converts from a string percentage rate with a percent sign to a double rate - // e.g. 0.12% => 0.0012d - private static double parseRate(String input) { - return Double.parseDouble(input.replace("%", "")) / 100d; - } + // Converts from a string percentage rate with a percent sign to a double rate + // e.g. 0.12% => 0.0012d + private static double parseRate(String input) { + return Double.parseDouble(input.replace("%", "")) / 100d; + } } diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitYieldCurveDataParser.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitYieldCurveDataParser.java index a928a36a07..c47caacf54 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitYieldCurveDataParser.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/MarkitYieldCurveDataParser.java @@ -1,6 +1,6 @@ /** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. */ package com.opengamma.strata.examples.marketdata.credit.markit; @@ -31,104 +31,105 @@ import java.util.Map; */ public class MarkitYieldCurveDataParser { - private static final String DATE = "Valuation Date"; - private static final String TENOR = "Tenor"; - private static final String INSTRUMENT = "Instrument Type"; - private static final String RATE = "Rate"; - private static final String CONVENTION = "Curve Convention"; + private static final String DATE = "Valuation Date"; + private static final String TENOR = "Tenor"; + private static final String INSTRUMENT = "Instrument Type"; + private static final String RATE = "Rate"; + private static final String CONVENTION = "Curve Convention"; - /** - * Parses the specified source. - * - * @param source the source to parse - * @return the map of parsed yield curve par rates - */ - public static Map parse(CharSource source) { - // parse the curve data - Map> curveData = Maps.newHashMap(); - CsvFile csv = CsvFile.of(source, true); - for (CsvRow row : csv.rows()) { - String dateText = row.getField(DATE); - String tenorText = row.getField(TENOR); - String instrumentText = row.getField(INSTRUMENT); - String rateText = row.getField(RATE); - String conventionText = row.getField(CONVENTION); + /** + * Parses the specified source. + * + * @param source the source to parse + * @return the map of parsed yield curve par rates + */ + public static Map parse(CharSource source) { + // parse the curve data + Map> curveData = Maps.newHashMap(); + CsvFile csv = CsvFile.of(source, true); + for (CsvRow row : csv.rows()) { + String dateText = row.getField(DATE); + String tenorText = row.getField(TENOR); + String instrumentText = row.getField(INSTRUMENT); + String rateText = row.getField(RATE); + String conventionText = row.getField(CONVENTION); - Point point = new Point( - Tenor.parse(tenorText), - LocalDate.parse(dateText, DateTimeFormatter.ISO_LOCAL_DATE), - mapUnderlyingType(instrumentText), - Double.parseDouble(rateText)); - IsdaYieldCurveConvention convention = IsdaYieldCurveConvention.of(conventionText); + Point point = new Point( + Tenor.parse(tenorText), + LocalDate.parse(dateText, DateTimeFormatter.ISO_LOCAL_DATE), + mapUnderlyingType(instrumentText), + Double.parseDouble(rateText)); + IsdaYieldCurveConvention convention = IsdaYieldCurveConvention.of(conventionText); - List points = curveData.get(convention); - if (points == null) { - points = Lists.newArrayList(); - curveData.put(convention, points); - } - points.add(point); + List points = curveData.get(convention); + if (points == null) { + points = Lists.newArrayList(); + curveData.put(convention, points); + } + points.add(point); + } + + // convert the curve data into the result map + Map result = Maps.newHashMap(); + for (IsdaYieldCurveConvention convention : curveData.keySet()) { + List points = curveData.get(convention); + result.put(IsdaYieldCurveInputsId.of(convention.getCurrency()), + IsdaYieldCurveInputs.of( + CurveName.of(convention.getName()), + points.stream().map(s -> s.getTenor().getPeriod()).toArray(Period[]::new), + points.stream().map(s -> s.getDate()).toArray(LocalDate[]::new), + points.stream().map(s -> s.getInstrumentType()).toArray(IsdaYieldCurveUnderlyingType[]::new), + points.stream().mapToDouble(s -> s.getRate()).toArray(), + convention)); + } + return result; } - // convert the curve data into the result map - Map result = Maps.newHashMap(); - for (IsdaYieldCurveConvention convention : curveData.keySet()) { - List points = curveData.get(convention); - result.put(IsdaYieldCurveInputsId.of(convention.getCurrency()), - IsdaYieldCurveInputs.of( - CurveName.of(convention.getName()), - points.stream().map(s -> s.getTenor().getPeriod()).toArray(Period[]::new), - points.stream().map(s -> s.getDate()).toArray(LocalDate[]::new), - points.stream().map(s -> s.getInstrumentType()).toArray(IsdaYieldCurveUnderlyingType[]::new), - points.stream().mapToDouble(s -> s.getRate()).toArray(), - convention)); - } - return result; - } - - // parse the M/S instrument type flag - private static IsdaYieldCurveUnderlyingType mapUnderlyingType(String type) { - switch (type) { - case "M": - return IsdaYieldCurveUnderlyingType.ISDA_MONEY_MARKET; - case "S": - return IsdaYieldCurveUnderlyingType.ISDA_SWAP; - default: - throw new IllegalStateException("Unknown underlying type, only M or S allowed: " + type); - } - } - - //------------------------------------------------------------------------- - /** - * Stores the parsed data points. - */ - private static class Point { - private final Tenor tenor; - private final LocalDate date; - private final IsdaYieldCurveUnderlyingType instrumentType; - private final double rate; - - private Point(Tenor tenor, LocalDate baseDate, IsdaYieldCurveUnderlyingType instrumentType, double rate) { - this.tenor = tenor; - this.date = baseDate.plus(tenor.getPeriod()); - this.instrumentType = instrumentType; - this.rate = rate; + // parse the M/S instrument type flag + private static IsdaYieldCurveUnderlyingType mapUnderlyingType(String type) { + switch (type) { + case "M": + return IsdaYieldCurveUnderlyingType.ISDA_MONEY_MARKET; + case "S": + return IsdaYieldCurveUnderlyingType.ISDA_SWAP; + default: + throw new IllegalStateException("Unknown underlying type, only M or S allowed: " + type); + } } - public Tenor getTenor() { - return tenor; - } + //------------------------------------------------------------------------- - public LocalDate getDate() { - return date; - } + /** + * Stores the parsed data points. + */ + private static class Point { + private final Tenor tenor; + private final LocalDate date; + private final IsdaYieldCurveUnderlyingType instrumentType; + private final double rate; - public IsdaYieldCurveUnderlyingType getInstrumentType() { - return instrumentType; - } + private Point(Tenor tenor, LocalDate baseDate, IsdaYieldCurveUnderlyingType instrumentType, double rate) { + this.tenor = tenor; + this.date = baseDate.plus(tenor.getPeriod()); + this.instrumentType = instrumentType; + this.rate = rate; + } - public double getRate() { - return rate; + public Tenor getTenor() { + return tenor; + } + + public LocalDate getDate() { + return date; + } + + public IsdaYieldCurveUnderlyingType getInstrumentType() { + return instrumentType; + } + + public double getRate() { + return rate; + } } - } } diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/package-info.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/package-info.java index e05d86d71e..9e92d72356 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/package-info.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/credit/markit/package-info.java @@ -1,7 +1,9 @@ /** * Copyright (C) 2016 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. + *

+ * Credit market data for examples. */ /** diff --git a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/package-info.java b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/package-info.java index a0688333c4..c286a8db4b 100644 --- a/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/package-info.java +++ b/samples/simm-valuation-demo/src/main/java/com/opengamma/strata/examples/marketdata/package-info.java @@ -1,7 +1,9 @@ /** * Copyright (C) 2016 - present by OpenGamma Inc. and the OpenGamma group of companies - * + *

* Please see distribution for license. + *

+ * Market data for examples. */ /** diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt index a10b756319..3fb9118fd5 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt @@ -254,13 +254,14 @@ class PortfolioApi(val rpc: CordaRPCOps) { val parties = rpc.networkMapSnapshot() val notaries = rpc.notaryIdentities() // TODO We are not able to filter by network map node now - val counterParties = parties.filterNot { it.legalIdentities.any { it in notaries } - || ownParty in it.legalIdentities + val counterParties = parties.filterNot { + it.legalIdentities.any { it in notaries } + || ownParty in it.legalIdentities } return AvailableParties( self = ApiParty(ownParty.owningKey.toBase58String(), ownParty.name), // TODO It will show all identities including service identities. - counterparties = counterParties.flatMap { it.legalIdentitiesAndCerts.map { ApiParty(it.owningKey.toBase58String(), it.name) }} + counterparties = counterParties.flatMap { it.legalIdentitiesAndCerts.map { ApiParty(it.owningKey.toBase58String(), it.name) } } ) } diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt index eb1198a881..cfccafbc4d 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt @@ -70,6 +70,7 @@ object SimmFlow { private val existing: StateAndRef?) : FlowLogic>() { constructor(otherParty: Party, valuationDate: LocalDate) : this(otherParty, valuationDate, null) + lateinit var notary: Party lateinit var otherPartySession: FlowSession @@ -318,7 +319,7 @@ object SimmFlow { logger.info("Handshake finished, awaiting Simm update") replyToSession.send(Ack) // Hack to state that this party is ready. subFlow(object : StateRevisionFlow.Receiver(replyToSession) { - override fun verifyProposal(stx:SignedTransaction, proposal: Proposal) { + override fun verifyProposal(stx: SignedTransaction, proposal: Proposal) { super.verifyProposal(stx, proposal) if (proposal.modification.portfolio != portfolio.refs) throw StateReplacementException() } diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt index 49918f85c3..7616b9e932 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt @@ -15,7 +15,7 @@ import net.corda.vega.contracts.RevisionedState */ object StateRevisionFlow { open class Requester(curStateRef: StateAndRef>, - updatedData: T) : AbstractStateReplacementFlow.Instigator, RevisionedState, T>(curStateRef, updatedData) { + updatedData: T) : AbstractStateReplacementFlow.Instigator, RevisionedState, T>(curStateRef, updatedData) { override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx { val state = originalState.state.data val tx = state.generateRevision(originalState.state.notary, originalState, modification) diff --git a/samples/simm-valuation-demo/src/test/java/net/corda/vega/SwapExampleX.java b/samples/simm-valuation-demo/src/test/java/net/corda/vega/SwapExampleX.java index 56a2dd2e6e..dfd18bbbd8 100644 --- a/samples/simm-valuation-demo/src/test/java/net/corda/vega/SwapExampleX.java +++ b/samples/simm-valuation-demo/src/test/java/net/corda/vega/SwapExampleX.java @@ -48,97 +48,97 @@ import static java.util.stream.Collectors.toList; */ public class SwapExampleX { - public static final LocalDate VALUATION_DATE = LocalDate.of(2016, 6, 6); + public static final LocalDate VALUATION_DATE = LocalDate.of(2016, 6, 6); - public static void main(String[] args) { - CurveGroupDefinition curveGroupDefinition = loadCurveGroup(); - MarketData marketData = loadMarketData(); - List trades = ImmutableList.of(createVanillaFixedVsLibor3mSwap(), createVanillaFixedVsLibor6mSwap()); - CurveCalibrator calibrator = CurveCalibrator.of(1e-9, 1e-9, 100, CalibrationMeasures.PAR_SPREAD); - ImmutableRatesProvider ratesProvider = calibrator.calibrate(curveGroupDefinition, marketData, ReferenceData.standard()); - MarketDataFxRateProvider fxRateProvider = MarketDataFxRateProvider.of(marketData); - ImmutableRatesProvider combinedRatesProvider = ImmutableRatesProvider.combined(fxRateProvider, ratesProvider); + public static void main(String[] args) { + CurveGroupDefinition curveGroupDefinition = loadCurveGroup(); + MarketData marketData = loadMarketData(); + List trades = ImmutableList.of(createVanillaFixedVsLibor3mSwap(), createVanillaFixedVsLibor6mSwap()); + CurveCalibrator calibrator = CurveCalibrator.of(1e-9, 1e-9, 100, CalibrationMeasures.PAR_SPREAD); + ImmutableRatesProvider ratesProvider = calibrator.calibrate(curveGroupDefinition, marketData, ReferenceData.standard()); + MarketDataFxRateProvider fxRateProvider = MarketDataFxRateProvider.of(marketData); + ImmutableRatesProvider combinedRatesProvider = ImmutableRatesProvider.combined(fxRateProvider, ratesProvider); - List resolvedTrades = trades.stream().map(trade -> trade.resolve(ReferenceData.standard())).collect(toList()); - DiscountingSwapProductPricer pricer = DiscountingSwapProductPricer.DEFAULT; + List resolvedTrades = trades.stream().map(trade -> trade.resolve(ReferenceData.standard())).collect(toList()); + DiscountingSwapProductPricer pricer = DiscountingSwapProductPricer.DEFAULT; - CurrencyParameterSensitivities totalSensitivities = CurrencyParameterSensitivities.empty(); - MultiCurrencyAmount totalCurrencyExposure = MultiCurrencyAmount.empty(); + CurrencyParameterSensitivities totalSensitivities = CurrencyParameterSensitivities.empty(); + MultiCurrencyAmount totalCurrencyExposure = MultiCurrencyAmount.empty(); - for (ResolvedSwapTrade resolvedTrade : resolvedTrades) { - ResolvedSwap swap = resolvedTrade.getProduct(); + for (ResolvedSwapTrade resolvedTrade : resolvedTrades) { + ResolvedSwap swap = resolvedTrade.getProduct(); - PointSensitivities pointSensitivities = pricer.presentValueSensitivity(swap, combinedRatesProvider).build(); - CurrencyParameterSensitivities sensitivities = combinedRatesProvider.parameterSensitivity(pointSensitivities); - MultiCurrencyAmount currencyExposure = pricer.currencyExposure(swap, combinedRatesProvider); + PointSensitivities pointSensitivities = pricer.presentValueSensitivity(swap, combinedRatesProvider).build(); + CurrencyParameterSensitivities sensitivities = combinedRatesProvider.parameterSensitivity(pointSensitivities); + MultiCurrencyAmount currencyExposure = pricer.currencyExposure(swap, combinedRatesProvider); - totalSensitivities = totalSensitivities.combinedWith(sensitivities); - totalCurrencyExposure = totalCurrencyExposure.plus(currencyExposure); + totalSensitivities = totalSensitivities.combinedWith(sensitivities); + totalCurrencyExposure = totalCurrencyExposure.plus(currencyExposure); + } + //PortfolioNormalizer normalizer = new PortfolioNormalizer(Currency.EUR, combinedRatesProvider); + //RwamBimmNotProductClassesCalculator calculatorTotal = new RwamBimmNotProductClassesCalculator( + // fxRateProvider, + // Currency.EUR, + // IsdaConfiguration.INSTANCE); +// + //Triple margin = BimmAnalysisUtils.computeMargin( + // combinedRatesProvider, + // normalizer, + // calculatorTotal, + // totalSensitivities, + // totalCurrencyExposure); +// + //System.out.println(margin); } - //PortfolioNormalizer normalizer = new PortfolioNormalizer(Currency.EUR, combinedRatesProvider); - //RwamBimmNotProductClassesCalculator calculatorTotal = new RwamBimmNotProductClassesCalculator( - // fxRateProvider, - // Currency.EUR, - // IsdaConfiguration.INSTANCE); -// - //Triple margin = BimmAnalysisUtils.computeMargin( - // combinedRatesProvider, - // normalizer, - // calculatorTotal, - // totalSensitivities, - // totalCurrencyExposure); -// - //System.out.println(margin); - } - //-------------------------------------------------------------------------------------------------- + //-------------------------------------------------------------------------------------------------- - /** - * Load the market quotes and FX rates from data files. - */ - private static MarketData loadMarketData() { - Path dataDir = Paths.get("src/test/resources/data"); - Path quotesFile = dataDir.resolve("BIMM-MARKET-QUOTES-20160606.csv"); - Path fxFile = dataDir.resolve("BIMM-FX-RATES-20160606.csv"); + /** + * Load the market quotes and FX rates from data files. + */ + private static MarketData loadMarketData() { + Path dataDir = Paths.get("src/test/resources/data"); + Path quotesFile = dataDir.resolve("BIMM-MARKET-QUOTES-20160606.csv"); + Path fxFile = dataDir.resolve("BIMM-FX-RATES-20160606.csv"); - Map quotes = QuotesCsvLoader.load(VALUATION_DATE, ImmutableList.of(ResourceLocator.ofPath(quotesFile))); - Map fxRates = FxRatesCsvLoader.load(VALUATION_DATE, ResourceLocator.ofPath(fxFile)); - return ImmutableMarketData.builder(VALUATION_DATE).addValueMap(quotes).addValueMap(fxRates).build(); - } + Map quotes = QuotesCsvLoader.load(VALUATION_DATE, ImmutableList.of(ResourceLocator.ofPath(quotesFile))); + Map fxRates = FxRatesCsvLoader.load(VALUATION_DATE, ResourceLocator.ofPath(fxFile)); + return ImmutableMarketData.builder(VALUATION_DATE).addValueMap(quotes).addValueMap(fxRates).build(); + } - /** - * Loads the curve group definition from data files. - * - * A curve group maps from curve name to index for forward curves and curve name to currency for discount curves. - */ - private static CurveGroupDefinition loadCurveGroup() { - Path settingsDir = Paths.get("src/test/resources/settings"); - Map curveGroups = RatesCalibrationCsvLoader.load( - ResourceLocator.ofPath(settingsDir.resolve("BIMM-groups-EUR.csv")), - ResourceLocator.ofPath(settingsDir.resolve("BIMM-settings-EUR.csv")), - ResourceLocator.ofPath(settingsDir.resolve("BIMM-nodes-EUR.csv"))); - return curveGroups.get(CurveGroupName.of("BIMM")); - } + /** + * Loads the curve group definition from data files. + *

+ * A curve group maps from curve name to index for forward curves and curve name to currency for discount curves. + */ + private static CurveGroupDefinition loadCurveGroup() { + Path settingsDir = Paths.get("src/test/resources/settings"); + Map curveGroups = RatesCalibrationCsvLoader.load( + ResourceLocator.ofPath(settingsDir.resolve("BIMM-groups-EUR.csv")), + ResourceLocator.ofPath(settingsDir.resolve("BIMM-settings-EUR.csv")), + ResourceLocator.ofPath(settingsDir.resolve("BIMM-nodes-EUR.csv"))); + return curveGroups.get(CurveGroupName.of("BIMM")); + } - //-------------------------------------------------------------------------------------------------- + //-------------------------------------------------------------------------------------------------- - private static SwapTrade createVanillaFixedVsLibor3mSwap() { - return FixedIborSwapConventions.EUR_FIXED_1Y_EURIBOR_3M.createTrade( - VALUATION_DATE, - Tenor.TENOR_4Y, - BuySell.BUY, - 200_000_000, - 0.015, - ReferenceData.standard()); - } + private static SwapTrade createVanillaFixedVsLibor3mSwap() { + return FixedIborSwapConventions.EUR_FIXED_1Y_EURIBOR_3M.createTrade( + VALUATION_DATE, + Tenor.TENOR_4Y, + BuySell.BUY, + 200_000_000, + 0.015, + ReferenceData.standard()); + } - private static SwapTrade createVanillaFixedVsLibor6mSwap() { - return FixedIborSwapConventions.EUR_FIXED_1Y_EURIBOR_6M.createTrade( - VALUATION_DATE, - Tenor.TENOR_10Y, - BuySell.SELL, - 100_000_000, - 0.013, - ReferenceData.standard()); - } + private static SwapTrade createVanillaFixedVsLibor6mSwap() { + return FixedIborSwapConventions.EUR_FIXED_1Y_EURIBOR_6M.createTrade( + VALUATION_DATE, + Tenor.TENOR_10Y, + BuySell.SELL, + 100_000_000, + 0.013, + ReferenceData.standard()); + } } diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt index e373de677d..101171880c 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt @@ -27,19 +27,21 @@ import java.util.* * Interface for communicating with nodes running the trader demo. */ class TraderDemoClientApi(val rpc: CordaRPCOps) { - val cashCount: Long get() { - val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } - val countCriteria = QueryCriteria.VaultCustomQueryCriteria(count) - return rpc.vaultQueryBy(countCriteria).otherResults.single() as Long - } + val cashCount: Long + get() { + val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } + val countCriteria = QueryCriteria.VaultCustomQueryCriteria(count) + return rpc.vaultQueryBy(countCriteria).otherResults.single() as Long + } val dollarCashBalance: Amount get() = rpc.getCashBalance(USD) - val commercialPaperCount: Long get() { - val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } - val countCriteria = QueryCriteria.VaultCustomQueryCriteria(count) - return rpc.vaultQueryBy(countCriteria).otherResults.single() as Long - } + val commercialPaperCount: Long + get() { + val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } + val countCriteria = QueryCriteria.VaultCustomQueryCriteria(count) + return rpc.vaultQueryBy(countCriteria).otherResults.single() as Long + } fun runIssuer(amount: Amount, buyerName: CordaX500Name, sellerName: CordaX500Name) { val ref = OpaqueBytes.of(1) diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt index 297f033ca1..cd1e7fed4b 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt @@ -31,7 +31,9 @@ class CommercialPaperIssueFlow(private val amount: Amount, companion object { val PROSPECTUS_HASH = SecureHash.parse("decd098666b9657314870e192ced0c3519c2c9d395507a238338f8d003929de9") + object ISSUING : ProgressTracker.Step("Issuing and timestamping some commercial paper") + fun tracker() = ProgressTracker(ISSUING) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/DriverConstants.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/DriverConstants.kt index 1a17d02f3a..73e36aba5e 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/DriverConstants.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/DriverConstants.kt @@ -39,11 +39,13 @@ class PredefinedTestNode internal constructor(party: Party, driver: DriverDSLExp * for it: you won't have [ALICE_KEY]. */ fun DriverDSLExposedInterface.alice(): PredefinedTestNode = PredefinedTestNode(ALICE, this, null) + /** * Returns a plain, entirely stock node pre-configured with the [BOB] identity. Note that a random key will be generated * for it: you won't have [BOB_KEY]. */ fun DriverDSLExposedInterface.bob(): PredefinedTestNode = PredefinedTestNode(BOB, this, null) + /** * Returns a plain single node notary pre-configured with the [DUMMY_NOTARY] identity. Note that a random key will be generated * for it: you won't have [DUMMY_NOTARY_KEY]. diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt index 7f8069e919..7fe3ee5c09 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt @@ -20,7 +20,8 @@ import java.nio.file.Path * Creates and tests a ledger built by the passed in dsl. The provided services can be customised, otherwise a default * of a freshly built [MockServices] is used. */ -@JvmOverloads fun ledger( +@JvmOverloads +fun ledger( services: ServiceHub = MockServices(), initialiseSerialization: Boolean = true, dsl: LedgerDSL.() -> Unit @@ -40,7 +41,8 @@ import java.nio.file.Path * * @see LedgerDSLInterpreter._transaction */ -@JvmOverloads fun transaction( +@JvmOverloads +fun transaction( transactionLabel: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(notary = DUMMY_NOTARY), initialiseSerialization: Boolean = true, @@ -54,6 +56,7 @@ fun testNodeConfiguration( myLegalName: CordaX500Name, notaryConfig: NotaryConfig? = null): NodeConfiguration { abstract class MockableNodeConfiguration : NodeConfiguration // Otherwise Mockito is defeated by val getters. + val nc = spy() whenever(nc.baseDirectory).thenReturn(baseDirectory) whenever(nc.myLegalName).thenReturn(myLegalName) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/RPCDriver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/RPCDriver.kt index 4aff94842f..c5156c16e2 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/RPCDriver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/RPCDriver.kt @@ -64,7 +64,7 @@ interface RPCDriverExposedDSLInterface : DriverDSLExposedInterface { maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE, maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE, configuration: RPCServerConfiguration = RPCServerConfiguration.default, - ops : I + ops: I ): CordaFuture /** @@ -110,8 +110,8 @@ interface RPCDriverExposedDSLInterface : DriverDSLExposedInterface { maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE, configuration: RPCServerConfiguration = RPCServerConfiguration.default, customPort: NetworkHostAndPort? = null, - ops : I - ) : CordaFuture + ops: I + ): CordaFuture /** * Starts a Netty RPC client. @@ -180,16 +180,19 @@ interface RPCDriverExposedDSLInterface : DriverDSLExposedInterface { brokerHandle: RpcBrokerHandle ): RpcServerHandle } + inline fun RPCDriverExposedDSLInterface.startInVmRpcClient( username: String = rpcTestUser.username, password: String = rpcTestUser.password, configuration: RPCClientConfiguration = RPCClientConfiguration.default ) = startInVmRpcClient(I::class.java, username, password, configuration) + inline fun RPCDriverExposedDSLInterface.startRandomRpcClient( hostAndPort: NetworkHostAndPort, username: String = rpcTestUser.username, password: String = rpcTestUser.password ) = startRandomRpcClient(I::class.java, hostAndPort, username, password) + inline fun RPCDriverExposedDSLInterface.startRpcClient( rpcAddress: NetworkHostAndPort, username: String = rpcTestUser.username, @@ -200,7 +203,8 @@ inline fun RPCDriverExposedDSLInterface.startRpcClient( interface RPCDriverInternalDSLInterface : DriverDSLInternalInterface, RPCDriverExposedDSLInterface data class RpcBrokerHandle( - val hostAndPort: NetworkHostAndPort?,/** null if this is an InVM broker */ + val hostAndPort: NetworkHostAndPort?, + /** null if this is an InVM broker */ val clientTransportConfiguration: TransportConfiguration, val serverControl: ActiveMQServerControl ) @@ -253,6 +257,7 @@ private class SingleUserSecurityManager(val rpcUser: User) : ActiveMQSecurityMan override fun validateUser(user: String?, password: String?, certificates: Array?): String? { return validate(user, password) } + override fun validateUserAndRole(user: String?, password: String?, roles: MutableSet?, checkType: CheckType?, address: String?, connection: RemotingConnection?): String? { return validate(user, password) } @@ -260,6 +265,7 @@ private class SingleUserSecurityManager(val rpcUser: User) : ActiveMQSecurityMan private fun isValid(user: String?, password: String?): Boolean { return rpcUser.username == user && rpcUser.password == password } + private fun validate(user: String?, password: String?): String? { return if (isValid(user, password)) user else null } @@ -303,6 +309,7 @@ data class RPCDriverDSL( } ) } + fun createInVmRpcServerArtemisConfig(maxFileSize: Int, maxBufferedBytesPerClient: Long): Configuration { return ConfigurationImpl().apply { acceptorConfigurations = setOf(TransportConfiguration(InVMAcceptorFactory::class.java.name)) @@ -310,6 +317,7 @@ data class RPCDriverDSL( configureCommonSettings(maxFileSize, maxBufferedBytesPerClient) } } + fun createRpcServerArtemisConfig(maxFileSize: Int, maxBufferedBytesPerClient: Long, baseDirectory: Path, hostAndPort: NetworkHostAndPort): Configuration { val connectionDirection = ConnectionDirection.Inbound(acceptorFactoryClassName = NettyAcceptorFactory::class.java.name) return ConfigurationImpl().apply { @@ -321,6 +329,7 @@ data class RPCDriverDSL( configureCommonSettings(maxFileSize, maxBufferedBytesPerClient) } } + val inVmClientTransportConfiguration = TransportConfiguration(InVMConnectorFactory::class.java.name) fun createNettyClientTransportConfiguration(hostAndPort: NetworkHostAndPort): TransportConfiguration { return ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), hostAndPort, null) @@ -503,6 +512,7 @@ class RandomRpcUser { add(Generator.string()) add(Generator.int()) } + data class Call(val method: Method, val call: () -> Any?) @JvmStatic diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt index bf109863eb..4be46a59f5 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt @@ -143,7 +143,8 @@ class InMemoryMessagingNetwork( } /** This can be set to an object which can inject artificial latency between sender/recipient pairs. */ - @Volatile var latencyCalculator: LatencyCalculator? = null + @Volatile + var latencyCalculator: LatencyCalculator? = null private val timer = Timer() @Synchronized @@ -327,7 +328,7 @@ class InMemoryMessagingNetwork( while (!Thread.currentThread().isInterrupted) { try { pumpReceiveInternal(true) - } catch(e: InterruptedException) { + } catch (e: InterruptedException) { break } } @@ -452,7 +453,7 @@ class InMemoryMessagingNetwork( for (handler in deliverTo) { try { handler.callback(transfer.toReceivedMessage(), handler) - } catch(e: Exception) { + } catch (e: Exception) { log.error("Caught exception in handler for $this/${handler.topicSession}", e) } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt index 1c357a90a6..1b82ca04c1 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt @@ -53,6 +53,6 @@ class MockNetworkMapCache(serviceHub: ServiceHubInternal) : PersistentNetworkMap */ @VisibleForTesting fun deleteRegistration(legalIdentity: Party): Boolean { - return partyNodes.removeIf { legalIdentity.owningKey in it.legalIdentitiesAndCerts.map { it.owningKey }} + return partyNodes.removeIf { legalIdentity.owningKey in it.legalIdentitiesAndCerts.map { it.owningKey } } } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index 52989475b3..43471696df 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -247,7 +247,9 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, // This does not indirect through the NodeInfo object so it can be called before the node is started. // It is used from the network visualiser tool. - @Suppress("unused") val place: WorldMapLocation get() = findMyLocation()!! + @Suppress("unused") + val place: WorldMapLocation + get() = findMyLocation()!! private var dbCloser: (() -> Any?)? = null override fun initialiseDatabasePersistence(schemaService: SchemaService, insideTransaction: () -> T) = super.initialiseDatabasePersistence(schemaService) { @@ -302,7 +304,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, legalName = MOCK_NET_MAP.name, notaryIdentity = null, advertisedServices = arrayOf(), - entropyRoot = BigInteger.valueOf(random63BitValue()), + entropyRoot = BigInteger.valueOf(random63BitValue()), configOverrides = {}, start = true ).started!!.apply { 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 6263dfbc65..3d49484fef 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 @@ -82,7 +82,9 @@ open class MockServices( fun makeTestDatabaseProperties(key: String? = null, value: String? = null): Properties { val props = Properties() props.setProperty("transactionIsolationLevel", "repeatableRead") //for other possible values see net.corda.node.utilities.CordaPeristence.parserTransactionIsolationLevel(String) - if (key != null) { props.setProperty(key, value) } + if (key != null) { + props.setProperty(key, value) + } return props } @@ -154,10 +156,11 @@ open class MockServices( override val contractUpgradeService: ContractUpgradeService get() = throw UnsupportedOperationException() override val networkMapCache: NetworkMapCache get() = throw UnsupportedOperationException() override val clock: Clock get() = Clock.systemUTC() - override val myInfo: NodeInfo get() { - val identity = getTestPartyAndCertificate(MEGA_CORP.name, key.public) - return NodeInfo(emptyList(), listOf(identity), 1, serial = 1L) - } + override val myInfo: NodeInfo + get() { + val identity = getTestPartyAndCertificate(MEGA_CORP.name, key.public) + return NodeInfo(emptyList(), listOf(identity), 1, serial = 1L) + } override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2) val mockCordappProvider: MockCordappProvider = MockCordappProvider(CordappLoader.createWithTestPackages(cordappPackages + CordappLoader.testPackages)).start(attachments) as MockCordappProvider override val cordappProvider: CordappProvider = mockCordappProvider diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestClock.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestClock.kt index 690f765c3d..e1149b1fda 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestClock.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestClock.kt @@ -25,7 +25,8 @@ class TestClock(private var delegateClock: Clock = Clock.systemUTC()) : MutableC /** * Advance this [Clock] by the specified [Duration] for testing purposes. */ - @Synchronized fun advanceBy(duration: Duration) { + @Synchronized + fun advanceBy(duration: Duration) { delegateClock = offset(delegateClock, duration) notifyMutationObservers() } @@ -35,7 +36,8 @@ class TestClock(private var delegateClock: Clock = Clock.systemUTC()) : MutableC * * This will only be approximate due to the time ticking away, but will be some time shortly after the requested [Instant]. */ - @Synchronized fun setTo(newInstant: Instant) = advanceBy(instant() until newInstant) + @Synchronized + fun setTo(newInstant: Instant) = advanceBy(instant() until newInstant) @Synchronized override fun instant(): Instant { return delegateClock.instant() diff --git a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt index 6ab3de90d7..ae43e144db 100644 --- a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt +++ b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt @@ -46,6 +46,7 @@ class NodeProcess( class Factory(val buildDirectory: Path = Paths.get("build"), val cordaJar: Path = Paths.get(this::class.java.getResource("/corda.jar").toURI())) { val nodesDirectory = buildDirectory / formatter.format(Instant.now()) + init { nodesDirectory.createDirectories() } @@ -95,11 +96,11 @@ class NodeProcess( private fun startNode(nodeDir: Path): Process { val builder = ProcessBuilder() - .command(javaPath.toString(), "-jar", cordaJar.toString()) - .directory(nodeDir.toFile()) + .command(javaPath.toString(), "-jar", cordaJar.toString()) + .directory(nodeDir.toFile()) builder.environment().putAll(mapOf( - "CAPSULE_CACHE_DIR" to (buildDirectory / "capsule").toString() + "CAPSULE_CACHE_DIR" to (buildDirectory / "capsule").toString() )) return builder.start() diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index 5bec1a4aeb..4e20194249 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -117,8 +117,8 @@ fun freePort(): Int = freePortCounter.getAndAccumulate(0) { prev, _ -> 30000 + ( * to the Node, some other process else could allocate the returned ports. */ fun getFreeLocalPorts(hostName: String, numberToAlloc: Int): List { - val freePort = freePortCounter.getAndAccumulate(0) { prev, _ -> 30000 + (prev - 30000 + numberToAlloc) % 10000 } - return (freePort .. freePort + numberToAlloc - 1).map { NetworkHostAndPort(hostName, it) } + val freePort = freePortCounter.getAndAccumulate(0) { prev, _ -> 30000 + (prev - 30000 + numberToAlloc) % 10000 } + return (freePort..freePort + numberToAlloc - 1).map { NetworkHostAndPort(hostName, it) } } @JvmOverloads @@ -147,17 +147,17 @@ fun getTestPartyAndCertificate(name: CordaX500Name, publicKey: PublicKey, trustR } @Suppress("unused") -inline fun T.kryoSpecific(reason: String, function: () -> Unit) = if(!AMQP_ENABLED) { +inline fun T.kryoSpecific(reason: String, function: () -> Unit) = if (!AMQP_ENABLED) { function() } else { - loggerFor().info("Ignoring Kryo specific test, reason: $reason" ) + loggerFor().info("Ignoring Kryo specific test, reason: $reason") } @Suppress("unused") -inline fun T.amqpSpecific(reason: String, function: () -> Unit) = if(AMQP_ENABLED) { +inline fun T.amqpSpecific(reason: String, function: () -> Unit) = if (AMQP_ENABLED) { function() } else { - loggerFor().info("Ignoring AMQP specific test, reason: $reason" ) + loggerFor().info("Ignoring AMQP specific test, reason: $reason") } /** @@ -165,6 +165,7 @@ inline fun T.amqpSpecific(reason: String, function: () -> Unit * TODO: Should be removed after multiple identities are introduced. */ fun NodeInfo.chooseIdentityAndCert(): PartyAndCertificate = legalIdentitiesAndCerts.first() + fun NodeInfo.chooseIdentity(): Party = chooseIdentityAndCert().party /** Returns the identity of the first notary found on the network */ fun ServiceHub.getDefaultNotary(): Party = networkMapCache.notaryIdentities.first() diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/Eventually.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/Eventually.kt index b3a4588609..693fffeca9 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/Eventually.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/Eventually.kt @@ -6,15 +6,16 @@ import java.time.Duration * Ideas borrowed from "io.kotlintest" with some improvements made * This is meant for use from Kotlin code use only mainly due to it's inline/reified nature */ -inline fun eventually(duration: Duration, f: () -> R): R { +inline fun eventually(duration: Duration, f: () -> R): R { val end = System.nanoTime() + duration.toNanos() var times = 0 while (System.nanoTime() < end) { try { return f() } catch (e: Throwable) { - when(e) { - is E -> {}// ignore and continue + when (e) { + is E -> { + }// ignore and continue else -> throw e // unexpected exception type - rethrow } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt index d79568d693..74f622bc37 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt @@ -78,14 +78,15 @@ class FlowStackSnapshotFactoryImpl : FlowStackSnapshotFactory { return FlowStackSnapshot(Instant.now(), flowClass.name, frames) } - private val StackTraceElement.instrumentedAnnotation: Instrumented? get() { - Class.forName(className).methods.forEach { - if (it.name == methodName && it.isAnnotationPresent(Instrumented::class.java)) { - return it.getAnnotation(Instrumented::class.java) + private val StackTraceElement.instrumentedAnnotation: Instrumented? + get() { + Class.forName(className).methods.forEach { + if (it.name == methodName && it.isAnnotationPresent(Instrumented::class.java)) { + return it.getAnnotation(Instrumented::class.java) + } } + return null } - return null - } private fun removeConstructorStackTraceElements(stackTrace: List): List { val newStackTrace = ArrayList() diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/Measure.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/Measure.kt index 102cc0f413..91eaecf0a0 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/Measure.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/Measure.kt @@ -12,10 +12,13 @@ import kotlin.reflect.jvm.reflect fun measure(a: Iterable, f: (A) -> R) = measure(listOf(a), f.reflect()!!) { f(uncheckedCast(it[0])) } + fun measure(a: Iterable, b: Iterable, f: (A, B) -> R) = measure(listOf(a, b), f.reflect()!!) { f(uncheckedCast(it[0]), uncheckedCast(it[1])) } + fun measure(a: Iterable, b: Iterable, c: Iterable, f: (A, B, C) -> R) = measure(listOf(a, b, c), f.reflect()!!) { f(uncheckedCast(it[0]), uncheckedCast(it[1]), uncheckedCast(it[2])) } + fun measure(a: Iterable, b: Iterable, c: Iterable, d: Iterable, f: (A, B, C, D) -> R) = measure(listOf(a, b, c, d), f.reflect()!!) { f(uncheckedCast(it[0]), uncheckedCast(it[1]), uncheckedCast(it[2]), uncheckedCast(it[3])) } @@ -30,8 +33,8 @@ private fun measure(paramIterables: List>, kCallable: KCallab } data class MeasureResult( - val parameters: List>, - val result: R + val parameters: List>, + val result: R ) fun iterateLexical(iterables: List>): Iterable> { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt index c3f5872cd7..891808e4b5 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt @@ -22,32 +22,32 @@ fun initialiseTestSerialization() { // Check that everything is configured for testing with mutable delegating instances. try { check(SerializationDefaults.SERIALIZATION_FACTORY is TestSerializationFactory) - } catch(e: IllegalStateException) { + } catch (e: IllegalStateException) { SerializationDefaults.SERIALIZATION_FACTORY = TestSerializationFactory() } try { check(SerializationDefaults.P2P_CONTEXT is TestSerializationContext) - } catch(e: IllegalStateException) { + } catch (e: IllegalStateException) { SerializationDefaults.P2P_CONTEXT = TestSerializationContext() } try { check(SerializationDefaults.RPC_SERVER_CONTEXT is TestSerializationContext) - } catch(e: IllegalStateException) { + } catch (e: IllegalStateException) { SerializationDefaults.RPC_SERVER_CONTEXT = TestSerializationContext() } try { check(SerializationDefaults.RPC_CLIENT_CONTEXT is TestSerializationContext) - } catch(e: IllegalStateException) { + } catch (e: IllegalStateException) { SerializationDefaults.RPC_CLIENT_CONTEXT = TestSerializationContext() } try { check(SerializationDefaults.STORAGE_CONTEXT is TestSerializationContext) - } catch(e: IllegalStateException) { + } catch (e: IllegalStateException) { SerializationDefaults.STORAGE_CONTEXT = TestSerializationContext() } try { check(SerializationDefaults.CHECKPOINT_CONTEXT is TestSerializationContext) - } catch(e: IllegalStateException) { + } catch (e: IllegalStateException) { SerializationDefaults.CHECKPOINT_CONTEXT = TestSerializationContext() } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt index 9b4e99b752..48658ea021 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt @@ -274,7 +274,7 @@ data class TestLedgerDSLInterpreter private constructor( transactionLabel: String?, transactionBuilder: TransactionBuilder, dsl: TransactionDSL.() -> Unit - ) = recordTransactionWithTransactionMap(transactionLabel, transactionBuilder, dsl, nonVerifiedTransactionWithLocations, fillTransaction = true) + ) = recordTransactionWithTransactionMap(transactionLabel, transactionBuilder, dsl, nonVerifiedTransactionWithLocations, fillTransaction = true) override fun tweak( dsl: LedgerDSL(val interpreter: T) : Tr */ @JvmOverloads fun output(contractClassName: ContractClassName, contractState: ContractState, attachmentConstraint: AttachmentConstraint = AutomaticHashConstraint) = - _output(contractClassName,null, DUMMY_NOTARY, null, attachmentConstraint, contractState) + _output(contractClassName, null, DUMMY_NOTARY, null, attachmentConstraint, contractState) /** * Adds a command to the transaction. @@ -146,5 +146,5 @@ class TransactionDSL(val interpreter: T) : Tr */ fun attachment(contractClassName: ContractClassName) = _attachment(contractClassName) - fun attachments(vararg contractClassNames: ContractClassName) = contractClassNames.forEach { attachment(it)} + fun attachments(vararg contractClassNames: ContractClassName) = contractClassNames.forEach { attachment(it) } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyDealContract.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyDealContract.kt index 2fb6817d0d..b19bfac076 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyDealContract.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyDealContract.kt @@ -19,8 +19,7 @@ class DummyDealContract : Contract { data class State( override val participants: List, - override val linearId: UniqueIdentifier) : DealState, QueryableState - { + override val linearId: UniqueIdentifier) : DealState, QueryableState { constructor(participants: List = listOf(), ref: String) : this(participants, UniqueIdentifier(ref)) diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt index 8abe70d329..a921cd00c4 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt @@ -32,7 +32,7 @@ import java.util.* @JvmOverloads fun ServiceHub.fillWithSomeTestDeals(dealIds: List, participants: List = emptyList(), - notary: Party = DUMMY_NOTARY) : Vault { + notary: Party = DUMMY_NOTARY): Vault { val myKey: PublicKey = myInfo.chooseIdentity().owningKey val me = AnonymousParty(myKey) @@ -63,7 +63,7 @@ fun ServiceHub.fillWithSomeTestLinearStates(numberToCreate: Int, linearString: String = "", linearNumber: Long = 0L, linearBoolean: Boolean = false, - linearTimestamp: Instant = now()) : Vault { + linearTimestamp: Instant = now()): Vault { val myKey: PublicKey = myInfo.chooseIdentity().owningKey val me = AnonymousParty(myKey) val issuerKey = DUMMY_NOTARY_KEY @@ -196,7 +196,7 @@ fun calculateRandomlySizedAmounts(howMuch: Amount, min: Int, max: Int, fun ServiceHub.consume(states: List>, notary: Party) { // Create a txn consuming different contract types states.forEach { - val builder = TransactionBuilder(notary = notary).apply { + val builder = TransactionBuilder(notary = notary).apply { addInputState(it) addCommand(dummyCommand(notary.owningKey)) } @@ -238,7 +238,7 @@ fun ServiceHub.consumeAndProduce(states: List>, fun ServiceHub.consumeDeals(dealStates: List>, notary: Party) = consume(dealStates, notary) fun ServiceHub.consumeLinearStates(linearStates: List>, notary: Party) = consume(linearStates, notary) fun ServiceHub.evolveLinearStates(linearStates: List>, notary: Party) = consumeAndProduce(linearStates, notary) -fun ServiceHub.evolveLinearState(linearState: StateAndRef, notary: Party) : StateAndRef = consumeAndProduce(linearState, notary) +fun ServiceHub.evolveLinearState(linearState: StateAndRef, notary: Party): StateAndRef = consumeAndProduce(linearState, notary) /** * Consume cash, sending any change to the default identity for this node. Only suitable for use in test scenarios, @@ -254,7 +254,7 @@ fun ServiceHub.consumeCash(amount: Amount, to: Party = CHARLIE, notary */ @JvmOverloads fun ServiceHub.consumeCash(amount: Amount, ourIdentity: PartyAndCertificate, to: Party = CHARLIE, notary: Party): Vault.Update { - val update = vaultService.rawUpdates.toFuture() + val update = vaultService.rawUpdates.toFuture() val services = this // A tx that spends our money. diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt index 743934e921..0c283fd844 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt @@ -29,6 +29,7 @@ class HttpApi(val root: URL, val mapper: ObjectMapper = defaultMapper) { companion object { fun fromHostAndPort(hostAndPort: NetworkHostAndPort, base: String, protocol: String = "http", mapper: ObjectMapper = defaultMapper): HttpApi = HttpApi(URL("$protocol://$hostAndPort/$base/"), mapper) + private val defaultMapper: ObjectMapper by lazy { net.corda.client.jackson.JacksonSupport.createNonRpcMapper() } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt index e25018bd9c..1a610c9372 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt @@ -20,8 +20,8 @@ object DummyLinearStateSchema object DummyLinearStateSchemaV1 : MappedSchema(schemaFamily = DummyLinearStateSchema.javaClass, version = 1, mappedTypes = listOf(PersistentDummyLinearState::class.java)) { @Entity @Table(name = "dummy_linear_states", - indexes = arrayOf(Index(name = "external_id_idx", columnList = "external_id"), - Index(name = "uuid_idx", columnList = "uuid"))) + indexes = arrayOf(Index(name = "external_id_idx", columnList = "external_id"), + Index(name = "uuid_idx", columnList = "uuid"))) class PersistentDummyLinearState( /** [ContractState] attributes */ From b053449b7428ad887fb7425d4afdfa6ff6c0a951 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Fri, 6 Oct 2017 15:36:56 +0100 Subject: [PATCH 128/185] Reformat files in `tools` --- .../kotlin/net/corda/demobench/explorer/Explorer.kt | 2 +- .../kotlin/net/corda/demobench/model/NodeConfig.kt | 2 +- .../net/corda/demobench/model/NodeController.kt | 7 ++++--- .../net/corda/demobench/model/ServiceController.kt | 3 +-- .../net/corda/demobench/views/NodeTerminalView.kt | 11 +++++++---- .../main/kotlin/net/corda/demobench/web/WebServer.kt | 2 +- .../kotlin/net/corda/demobench/LoggingTestSuite.kt | 3 ++- .../src/main/kotlin/net/corda/explorer/AmountDiff.kt | 9 +++++---- .../kotlin/net/corda/loadtest/ConnectionManager.kt | 1 + .../src/main/kotlin/net/corda/loadtest/Disruption.kt | 1 + .../src/main/kotlin/net/corda/loadtest/LoadTest.kt | 3 ++- 11 files changed, 26 insertions(+), 18 deletions(-) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt index 3bf445b827..a03e83d88c 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt @@ -88,7 +88,7 @@ class Explorer internal constructor(private val explorerController: ExplorerCont try { // Try making a symlink to make things faster and use less disk space. Files.createSymbolicLink(destPath, path) - } catch(e: UnsupportedOperationException) { + } catch (e: UnsupportedOperationException) { // OS doesn't support symbolic links? Files.copy(path, destPath, REPLACE_EXISTING) } catch (e: java.nio.file.FileAlreadyExistsException) { diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt index 03dda19564..8cde8f3bcf 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt @@ -62,7 +62,7 @@ class NodeConfig constructor( fun toText(): String = toFileConfig().root().render(renderOptions) fun moveTo(baseDir: Path) = NodeConfig( - baseDir, legalName, p2pPort, rpcPort, webPort, h2Port, extraServices, users, networkMap + baseDir, legalName, p2pPort, rpcPort, webPort, h2Port, extraServices, users, networkMap ) fun install(plugins: Collection) { 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 367f04b847..5bd33b700e 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 @@ -33,9 +33,10 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { private var networkMapConfig: NetworkMapConfig? = null - val activeNodes: List get() = nodes.values.filter { - (it.state == NodeState.RUNNING) || (it.state == NodeState.STARTING) - } + val activeNodes: List + get() = nodes.values.filter { + (it.state == NodeState.RUNNING) || (it.state == NodeState.STARTING) + } init { log.info("Base directory: $baseDir") diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/ServiceController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/ServiceController.kt index 2f63b8e1c0..729f305805 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/ServiceController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/ServiceController.kt @@ -28,8 +28,7 @@ class ServiceController(resourceName: String = "/services.conf") : Controller() val service = line.split(":").map { it.trim() } if (service.size != 2) { log.warning("Encountered corrupted line '$line' while reading services from config: $url") - } - else { + } else { map[service[1]] = service[0] log.info("Supports: $service") } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt index a19fcbc484..6046aa0e11 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt @@ -179,9 +179,9 @@ class NodeTerminalView : Fragment() { } private fun launchRPC(config: NodeConfig) = NodeRPC( - config = config, - start = this::initialise, - invoke = this::pollCashBalances + config = config, + start = this::initialise, + invoke = this::pollCashBalances ) private fun initialise(config: NodeConfig, ops: CordaRPCOps) { @@ -238,7 +238,10 @@ class NodeTerminalView : Fragment() { header.isDisable = true subscriptions.forEach { // Don't allow any exceptions here to halt tab destruction. - try { it.unsubscribe() } catch (e: Exception) {} + try { + it.unsubscribe() + } catch (e: Exception) { + } } webServer.close() explorer.close() diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/web/WebServer.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/web/WebServer.kt index 4ff4e23e01..4aec34a73b 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/web/WebServer.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/web/WebServer.kt @@ -98,7 +98,7 @@ class WebServer internal constructor(private val webServerController: WebServerC conn.connect() conn.disconnect() return url - } catch(e: IOException) { + } catch (e: IOException) { } } throw TimeoutException("Web server did not start within ${timeout.seconds} seconds") diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/LoggingTestSuite.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/LoggingTestSuite.kt index cd662ace6a..946718c42a 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/LoggingTestSuite.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/LoggingTestSuite.kt @@ -25,7 +25,8 @@ class LoggingTestSuite { */ companion object { @BeforeClass - @JvmStatic fun `setup logging`() { + @JvmStatic + fun `setup logging`() { LoggingConfig() } } diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/AmountDiff.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/AmountDiff.kt index a77205adb8..24a95ec46b 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/AmountDiff.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/AmountDiff.kt @@ -7,10 +7,11 @@ enum class Positivity { Negative } -val Positivity.sign: String get() = when (this) { - Positivity.Positive -> "" - Positivity.Negative -> "-" -} +val Positivity.sign: String + get() = when (this) { + Positivity.Positive -> "" + Positivity.Negative -> "-" + } data class AmountDiff( val positivity: Positivity, diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/ConnectionManager.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/ConnectionManager.kt index 5b4f0f57ba..fe3e4fc621 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/ConnectionManager.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/ConnectionManager.kt @@ -47,6 +47,7 @@ fun setupJSchWithSshAgent(): JSch { override fun getSignature(data: ByteArray?) = agentProxy.sign(identity.blob, data) @Suppress("OverridingDeprecatedMember") override fun decrypt() = true + override fun getPublicKeyBlob() = identity.blob override fun setPassphrase(passphrase: ByteArray?) = true } diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/Disruption.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/Disruption.kt index 87aa80de90..b704f50acd 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/Disruption.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/Disruption.kt @@ -45,6 +45,7 @@ val isNotary = { node: NodeConnection -> val notaries = node.proxy.notaryIdentities() node.info.legalIdentities.any { it in notaries } } + fun ((A) -> Boolean).or(other: (A) -> Boolean): (A) -> Boolean = { this(it) || other(it) } fun hang(hangIntervalRange: LongRange) = Disruption("Hang randomly") { node, random -> diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTest.kt index 645b31ad67..a7751ca708 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTest.kt @@ -182,7 +182,8 @@ fun runLoadTests(configuration: LoadTestConfiguration, tests: List acc + "\n" + elem.name + ": " + elem.owningKey.toBase58String() } }.joinToString("\n") From 3f3ffd50e1939beae42fc61a2588c75450fcbe87 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Fri, 6 Oct 2017 15:37:29 +0100 Subject: [PATCH 129/185] Reformat files in `verifier` --- .../kotlin/net/corda/verifier/VerifierTests.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt index 6648fdb83d..d12dbc7a41 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt @@ -111,8 +111,8 @@ class VerifierTests { @Test fun `single verifier works with a node`() { verifierDriver( - networkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = true), - extraCordappPackagesToScan = listOf("net.corda.finance.contracts") + networkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = true), + extraCordappPackagesToScan = listOf("net.corda.finance.contracts") ) { val aliceFuture = startNode(providedName = ALICE.name) val notaryFuture = startNotaryNode(DUMMY_NOTARY.name, verifierType = VerifierType.OutOfProcess) From f9dc331551aac47b454bf10091333d52005d0df4 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Fri, 6 Oct 2017 15:38:19 +0100 Subject: [PATCH 130/185] Reformat files in `webserver` --- .../net/corda/webserver/internal/NodeWebServer.kt | 10 +++++----- .../webserver/servlets/AttachmentDownloadServlet.kt | 2 +- .../net/corda/webserver/servlets/CorDappInfoServlet.kt | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) 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 462f792c52..8d5bf05a03 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt @@ -120,7 +120,7 @@ class NodeWebServer(val config: WebServerConfig) { } @Throws(IOException::class) - override fun writeErrorPageMessage(request: HttpServletRequest, writer: Writer, code: Int, message: String , uri: String) { + override fun writeErrorPageMessage(request: HttpServletRequest, writer: Writer, code: Int, message: String, uri: String) { writer.write("

Corda $safeLegalName

\n") super.writeErrorPageMessage(request, writer, code, message, uri) } @@ -135,10 +135,10 @@ class NodeWebServer(val config: WebServerConfig) { } val resourceConfig = ResourceConfig() - .register(ObjectMapperConfig(rpcObjectMapper)) - .register(ResponseFilter()) - .register(CordaConverterProvider) - .register(APIServerImpl(localRpc)) + .register(ObjectMapperConfig(rpcObjectMapper)) + .register(ResponseFilter()) + .register(CordaConverterProvider) + .register(APIServerImpl(localRpc)) val webAPIsOnClasspath = pluginRegistries.flatMap { x -> x.webApis } for (webapi in webAPIsOnClasspath) { diff --git a/webserver/src/main/kotlin/net/corda/webserver/servlets/AttachmentDownloadServlet.kt b/webserver/src/main/kotlin/net/corda/webserver/servlets/AttachmentDownloadServlet.kt index 9750f595ac..e23d420059 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/servlets/AttachmentDownloadServlet.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/servlets/AttachmentDownloadServlet.kt @@ -55,7 +55,7 @@ class AttachmentDownloadServlet : HttpServlet() { // Closing the output stream commits our response. We cannot change the status code after this. resp.outputStream.close() - } catch(e: FileNotFoundException) { + } catch (e: FileNotFoundException) { log.warn("404 Not Found whilst trying to handle attachment download request for ${servletContext.contextPath}/$reqPath") resp.sendError(HttpServletResponse.SC_NOT_FOUND) return diff --git a/webserver/src/main/kotlin/net/corda/webserver/servlets/CorDappInfoServlet.kt b/webserver/src/main/kotlin/net/corda/webserver/servlets/CorDappInfoServlet.kt index eba6597428..8daa446af9 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/servlets/CorDappInfoServlet.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/servlets/CorDappInfoServlet.kt @@ -15,7 +15,7 @@ import javax.servlet.http.HttpServletResponse * Dumps some data about the installed CorDapps. * TODO: Add registered flow initiators. */ -class CorDappInfoServlet(val plugins: List, val rpc: CordaRPCOps): HttpServlet() { +class CorDappInfoServlet(val plugins: List, val rpc: CordaRPCOps) : HttpServlet() { @Throws(IOException::class) override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { @@ -73,7 +73,7 @@ class CorDappInfoServlet(val plugins: List, val rpc: Co for (method in resource.allMethods) { if (method.type == ResourceMethod.JaxrsType.SUB_RESOURCE_LOCATOR) { - resources.add( Resource.from(resource.resourceLocator.invocable.definitionMethod.returnType)) + resources.add(Resource.from(resource.resourceLocator.invocable.definitionMethod.returnType)) } else { endpoints.add(Endpoint(method.httpMethod, "api$path", resource.path)) } From 14f959b4afdd445b6092a821ff8ccea532409abf Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Fri, 6 Oct 2017 16:22:38 +0100 Subject: [PATCH 131/185] Code clean-up run --- .../net/corda/client/jfx/model/NetworkIdentityModel.kt | 2 +- .../src/main/kotlin/net/corda/core/flows/FinalityFlow.kt | 2 +- .../net/corda/finance/contracts/JavaCommercialPaper.java | 4 +--- .../net/corda/finance/contracts/asset/Obligation.kt | 2 +- .../net/corda/finance/contracts/CommercialPaperTests.kt | 3 ++- .../net/corda/finance/contracts/asset/CashTests.kt | 7 ++++--- .../internal/serialization/SerializationScheme.kt | 2 +- .../internal/serialization/amqp/PropertySerializer.kt | 6 +++--- .../corda/node/services/network/NodeInfoWatcherTest.kt | 2 +- .../node/services/statemachine/FlowStateMachineImpl.kt | 2 +- .../net/corda/node/utilities/NonInvalidatingCache.kt | 3 --- .../corda/node/utilities/NonInvalidatingUnboundCache.kt | 3 --- .../resources/net/corda/node/shell/base/login.groovy | 2 +- .../corda/node/services/vault/NodeVaultServiceTest.kt | 5 +++-- .../net/corda/node/services/vault/VaultWithCashTest.kt | 9 +++++---- .../src/main/kotlin/net/corda/irs/contract/IRSUtils.kt | 1 - .../src/main/kotlin/net/corda/demobench/pty/R3Pty.kt | 2 +- .../src/main/kotlin/net/corda/explorer/views/Network.kt | 6 +++--- .../main/kotlin/net/corda/explorer/views/SearchField.kt | 2 +- .../kotlin/net/corda/explorer/views/TransactionViewer.kt | 4 ++-- .../net/corda/explorer/views/cordapps/cash/CashViewer.kt | 2 +- 21 files changed, 33 insertions(+), 38 deletions(-) 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 9ff83660aa..2213d40bbc 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 @@ -38,7 +38,7 @@ class NetworkIdentityModel { }) val notaries: ObservableList = networkIdentities.map { - it.legalIdentitiesAndCerts.find { it.name.commonName?.let { ServiceType.parse(it).isNotary() } ?: false } + it.legalIdentitiesAndCerts.find { it.name.commonName?.let { ServiceType.parse(it).isNotary() } == true } }.map { it?.party }.filterNotNull() val notaryNodes: ObservableList = notaries.map { rpcProxy.value?.nodeInfoFromParty(it) }.filterNotNull() diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt index e2aafc165c..a08654acb9 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt @@ -88,7 +88,7 @@ class FinalityFlow(val transaction: SignedTransaction, private fun hasNoNotarySignature(stx: SignedTransaction): Boolean { val notaryKey = stx.tx.notary?.owningKey val signers = stx.sigs.map { it.by }.toSet() - return !(notaryKey?.isFulfilledBy(signers) ?: false) + return notaryKey?.isFulfilledBy(signers) != true } private fun getPartiesToSend(ltx: LedgerTransaction): Set { diff --git a/finance/src/main/java/net/corda/finance/contracts/JavaCommercialPaper.java b/finance/src/main/java/net/corda/finance/contracts/JavaCommercialPaper.java index 02bf2bbd45..221e02427c 100644 --- a/finance/src/main/java/net/corda/finance/contracts/JavaCommercialPaper.java +++ b/finance/src/main/java/net/corda/finance/contracts/JavaCommercialPaper.java @@ -101,9 +101,7 @@ public class JavaCommercialPaper implements Contract { if (issuance != null ? !issuance.equals(state.issuance) : state.issuance != null) return false; if (owner != null ? !owner.equals(state.owner) : state.owner != null) return false; if (faceValue != null ? !faceValue.equals(state.faceValue) : state.faceValue != null) return false; - if (maturityDate != null ? !maturityDate.equals(state.maturityDate) : state.maturityDate != null) - return false; - return true; + return maturityDate != null ? maturityDate.equals(state.maturityDate) : state.maturityDate == null; } @Override diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt index e5d45f6d51..c30ddc0875 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt @@ -454,7 +454,7 @@ class Obligation

: Contract { requireThat { "there is a time-window from the authority" using (timeWindow != null) - "the due date has passed" using (timeWindow!!.fromTime?.isAfter(deadline) ?: false) + "the due date has passed" using (timeWindow!!.fromTime?.isAfter(deadline) == true) "input state lifecycle is correct" using (input.lifecycle == expectedInputLifecycle) "output state corresponds exactly to input state, with lifecycle changed" using (expectedOutput == actualOutput) } 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 a1a296eb05..9a8150ef0f 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt @@ -12,6 +12,7 @@ import net.corda.core.utilities.seconds import net.corda.finance.DOLLARS import net.corda.finance.`issued by` import net.corda.finance.contracts.asset.* +import net.corda.finance.contracts.asset.Cash.Companion.generateSpend import net.corda.testing.* import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.testing.node.MockServices @@ -269,7 +270,7 @@ class CommercialPaperTestsGeneric { // Alice pays $9000 to BigCorp to own some of their debt. moveTX = run { val builder = TransactionBuilder(DUMMY_NOTARY) - Cash.generateSpend(aliceServices, builder, 9000.DOLLARS, AnonymousParty(bigCorpServices.key.public)) + generateSpend(aliceServices, builder, 9000.DOLLARS, AnonymousParty(bigCorpServices.key.public), ourIdentity, emptySet()) CommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(aliceServices.key.public)) val ptx = aliceServices.signInitialTransaction(builder) val ptx2 = bigCorpServices.addSignature(ptx) diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt index 952aa3d9df..e888d0705b 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt @@ -12,6 +12,7 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.OpaqueBytes import net.corda.finance.* +import net.corda.finance.contracts.asset.Cash.Companion.generateSpend import net.corda.finance.utils.sumCash import net.corda.finance.utils.sumCashBy import net.corda.finance.utils.sumCashOrNull @@ -505,7 +506,7 @@ class CashTests : TestDependencyInjectionBase() { private fun makeSpend(amount: Amount, dest: AbstractParty): WireTransaction { val tx = TransactionBuilder(DUMMY_NOTARY) database.transaction { - Cash.generateSpend(miniCorpServices, tx, amount, dest) + generateSpend(miniCorpServices, tx, amount, dest, ourIdentity, emptySet()) } return tx.toWireTransaction(miniCorpServices) } @@ -607,7 +608,7 @@ class CashTests : TestDependencyInjectionBase() { database.transaction { val tx = TransactionBuilder(DUMMY_NOTARY) - Cash.generateSpend(miniCorpServices, tx, 80.DOLLARS, ALICE, setOf(MINI_CORP)) + generateSpend(miniCorpServices, tx, 80.DOLLARS, ALICE, ourIdentity, setOf(MINI_CORP)) assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0]) } @@ -826,7 +827,7 @@ class CashTests : TestDependencyInjectionBase() { PartyAndAmount(THEIR_IDENTITY_1, 400.DOLLARS), PartyAndAmount(THEIR_IDENTITY_2, 150.DOLLARS) ) - Cash.generateSpend(miniCorpServices, tx, payments) + generateSpend(miniCorpServices, tx, amount, to, ourIdentity, emptySet()) } val wtx = tx.toWireTransaction(miniCorpServices) fun out(i: Int) = wtx.getOutput(i) as Cash.State diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt index e4deaf2c55..1e7c257a1d 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt @@ -52,7 +52,7 @@ data class SerializationContextImpl(override val preferredSerializationVersion: * 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): SerializationContext { - properties[attachmentsClassLoaderEnabledPropertyName] as? Boolean ?: false || return this + properties[attachmentsClassLoaderEnabledPropertyName] as? Boolean == true || return this val serializationContext = properties[serializationContextKey] as? SerializeAsTokenContextImpl ?: return this // Some tests don't set one. try { return withClassLoader(cache.get(attachmentHashes) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt index feb06e123e..3d9fa8b0a5 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt @@ -21,8 +21,8 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r val default: String? = generateDefault() val mandatory: Boolean = generateMandatory() - private val isInterface: Boolean get() = resolvedType.asClass()?.isInterface ?: false - private val isJVMPrimitive: Boolean get() = resolvedType.asClass()?.isPrimitive ?: false + private val isInterface: Boolean get() = resolvedType.asClass()?.isInterface == true + private val isJVMPrimitive: Boolean get() = resolvedType.asClass()?.isPrimitive == true private fun generateType(): String { return if (isInterface || resolvedType == Any::class.java) "*" else SerializerFactory.nameForType(resolvedType) @@ -45,7 +45,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r } private fun generateMandatory(): Boolean { - return isJVMPrimitive || !(readMethod?.returnsNullable() ?: true) + return isJVMPrimitive || readMethod?.returnsNullable() == false } private fun Method.returnsNullable(): Boolean { diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt index 556e47cb50..268d23d394 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt @@ -35,7 +35,7 @@ class NodeInfoWatcherTest : NodeBasedTest() { lateinit var keyManagementService: KeyManagementService lateinit var nodeInfoPath: Path - val scheduler = TestScheduler(); + val scheduler = TestScheduler() val testSubscriber = TestSubscriber() // Object under test 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 0b478243f6..dea328709a 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 @@ -265,7 +265,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, // This is a hack to allow cash app access list of permitted issuer currency. // TODO: replace this with cordapp configuration. val config = serviceHub.configuration as? FullNodeConfiguration - val permissionGranted = config?.extraAdvertisedServiceIds?.contains(permissionName) ?: true + val permissionGranted = config?.extraAdvertisedServiceIds?.contains(permissionName) != false val checkPermissionEvent = FlowPermissionAuditEvent( serviceHub.clock.instant(), flowInitiator, 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 a81ce259e5..07dd16b936 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt @@ -27,8 +27,5 @@ class NonInvalidatingCache private constructor( } override fun load(key: K) = loadFunction(key) - override fun loadAll(keys: Iterable): MutableMap { - return super.loadAll(keys) - } } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingUnboundCache.kt b/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingUnboundCache.kt index 8719a4e448..ea16bfd064 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingUnboundCache.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingUnboundCache.kt @@ -29,8 +29,5 @@ class NonInvalidatingUnboundCache private constructor( } override fun load(key: K) = loadFunction(key) - override fun loadAll(keys: Iterable): MutableMap { - return super.loadAll(keys) - } } } \ No newline at end of file diff --git a/node/src/main/resources/net/corda/node/shell/base/login.groovy b/node/src/main/resources/net/corda/node/shell/base/login.groovy index 500704ae69..f6df32b386 100644 --- a/node/src/main/resources/net/corda/node/shell/base/login.groovy +++ b/node/src/main/resources/net/corda/node/shell/base/login.groovy @@ -11,5 +11,5 @@ Useful commands include 'help' to see what is available, and 'bye' to shut down """ prompt = { -> - return "${new Date()}>>> "; + return "${new Date()}>>> " } 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 5250e7f500..9d385d9663 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 @@ -20,6 +20,7 @@ import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toNonEmptySet import net.corda.finance.* import net.corda.finance.contracts.asset.Cash +import net.corda.finance.contracts.asset.Cash.Companion.generateSpend import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_KEY import net.corda.finance.contracts.getCashBalance @@ -515,7 +516,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { database.transaction { val moveTx = TransactionBuilder(services.myInfo.chooseIdentity()).apply { - Cash.generateSpend(services, this, Amount(1000, GBP), thirdPartyIdentity) + generateSpend(services, this, Amount(1000, GBP), thirdPartyIdentity, ourIdentity, emptySet()) }.toWireTransaction(services) service.notify(moveTx) } @@ -560,7 +561,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { // Move cash val moveTx = database.transaction { TransactionBuilder(newNotary).apply { - Cash.generateSpend(services, this, Amount(1000, GBP), thirdPartyIdentity) + generateSpend(services, this, Amount(1000, GBP), thirdPartyIdentity, ourIdentity, emptySet()) }.toWireTransaction(services) } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index 60e853d023..aacc46ce2b 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -12,6 +12,7 @@ import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria import net.corda.core.transactions.TransactionBuilder import net.corda.finance.* import net.corda.finance.contracts.asset.Cash +import net.corda.finance.contracts.asset.Cash.Companion.generateSpend import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_KEY import net.corda.finance.contracts.getCashBalance @@ -99,7 +100,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { database.transaction { // A tx that spends our money. val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY) - Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB) + generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB, ourIdentity, emptySet()) val spendPTX = services.signInitialTransaction(spendTXBuilder, freshKey) notaryServices.addSignature(spendPTX) } @@ -151,7 +152,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { database.transaction { try { val txn1Builder = TransactionBuilder(DUMMY_NOTARY) - Cash.generateSpend(services, txn1Builder, 60.DOLLARS, BOB) + generateSpend(services, txn1Builder, 60.DOLLARS, BOB, ourIdentity, emptySet()) val ptxn1 = notaryServices.signInitialTransaction(txn1Builder) val txn1 = services.addSignature(ptxn1, freshKey) println("txn1: ${txn1.id} spent ${((txn1.tx.outputs[0].data) as Cash.State).amount}") @@ -187,7 +188,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { database.transaction { try { val txn2Builder = TransactionBuilder(DUMMY_NOTARY) - Cash.generateSpend(services, txn2Builder, 80.DOLLARS, BOB) + generateSpend(services, txn2Builder, 80.DOLLARS, BOB, ourIdentity, emptySet()) val ptxn2 = notaryServices.signInitialTransaction(txn2Builder) val txn2 = services.addSignature(ptxn2, freshKey) println("txn2: ${txn2.id} spent ${((txn2.tx.outputs[0].data) as Cash.State).amount}") @@ -311,7 +312,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { database.transaction { // A tx that spends our money. val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY) - Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB) + generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB, ourIdentity, emptySet()) val spendPTX = notaryServices.signInitialTransaction(spendTXBuilder) val spendTX = services.addSignature(spendPTX, freshKey) services.recordTransactions(spendTX) diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt index fb7e367a3c..c362e602a5 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt @@ -71,7 +71,6 @@ class FixedRate(ratioUnit: RatioUnit) : Rate(ratioUnit) { fun isPositive(): Boolean = ratioUnit!!.value > BigDecimal("0.0") override fun equals(other: Any?) = other?.javaClass == javaClass && super.equals(other) - override fun hashCode() = super.hashCode() } /** diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt index afc91bb6f4..cd82181367 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt @@ -21,7 +21,7 @@ class R3Pty(val name: CordaX500Name, settings: SettingsProvider, dimension: Dime val terminal = JediTermWidget(dimension, settings) - val isConnected: Boolean get() = terminal.ttyConnector?.isConnected ?: false + val isConnected: Boolean get() = terminal.ttyConnector?.isConnected == true override fun close() { log.info("Closing terminal '{}'", name) diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt index cb7f69df1f..de6a41aded 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt @@ -103,7 +103,7 @@ class Network : CordaView() { hgap = 5.0 vgap = 5.0 for (identity in identities) { - val isNotary = identity.name.commonName?.let { ServiceType.parse(it).isNotary() } ?: false + val isNotary = identity.name.commonName?.let { ServiceType.parse(it).isNotary() } == true row("${if (isNotary) "Notary " else ""}Public Key :") { copyableLabel(SimpleObjectProperty(identity.owningKey.toBase58String())) } @@ -150,7 +150,7 @@ class Network : CordaView() { } override fun onDock() { - centralLabel = mapLabels.firstOrDefault(SimpleObjectProperty(myLabel), { centralPeer?.contains(it.text, true) ?: false }) + centralLabel = mapLabels.firstOrDefault(SimpleObjectProperty(myLabel), { centralPeer?.contains(it.text, true) == true }) centralLabel.value?.let { mapScrollPane.centerLabel(it) } } @@ -160,7 +160,7 @@ class Network : CordaView() { } init { - centralLabel = mapLabels.firstOrDefault(SimpleObjectProperty(myLabel), { centralPeer?.contains(it.text, true) ?: false }) + centralLabel = mapLabels.firstOrDefault(SimpleObjectProperty(myLabel), { centralPeer?.contains(it.text, true) == true }) Bindings.bindContent(notaryList.children, notaryButtons) Bindings.bindContent(peerList.children, peerButtons) // Run once when the screen is ready. diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/SearchField.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/SearchField.kt index 2feeb8a938..ce06f65318 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/SearchField.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/SearchField.kt @@ -37,7 +37,7 @@ class SearchField(private val data: ObservableList, vararg filterCriteria: text.isNullOrBlank() || if (category == ALL) { filterCriteria.any { it.second(data, text) } } else { - filterCriteria.toMap()[category]?.invoke(data, text) ?: false + filterCriteria.toMap()[category]?.invoke(data, text) == true } } }, arrayOf(textField.textProperty(), searchCategory.valueProperty()))) diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt index f528e0ec66..128c2f221b 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt @@ -135,8 +135,8 @@ class TransactionViewer : CordaView("Transactions") { "Transaction ID" to { tx, s -> "${tx.id}".contains(s, true) }, "Input" to { tx, s -> tx.inputs.resolved.any { it.state.contract.contains(s, true) } }, "Output" to { tx, s -> tx.outputs.any { it.state.contract.contains(s, true) } }, - "Input Party" to { tx, s -> tx.inputParties.any { it.any { it.value?.name?.organisation?.contains(s, true) ?: false } } }, - "Output Party" to { tx, s -> tx.outputParties.any { it.any { it.value?.name?.organisation?.contains(s, true) ?: false } } }, + "Input Party" to { tx, s -> tx.inputParties.any { it.any { it.value?.name?.organisation?.contains(s, true) == true } } }, + "Output Party" to { tx, s -> tx.outputParties.any { it.any { it.value?.name?.organisation?.contains(s, true) == true } } }, "Command Type" to { tx, s -> tx.commandTypes.any { it.simpleName.contains(s, true) } } ) root.top = searchField.root diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt index 480ae1116e..6093a235b1 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt @@ -147,7 +147,7 @@ class CashViewer : CordaView("Cash") { */ val searchField = SearchField(cashStates, "Currency" to { state, text -> state.state.data.amount.token.product.toString().contains(text, true) }, - "Issuer" to { state, text -> state.resolveIssuer().value?.name?.organisation?.contains(text, true) ?: false } + "Issuer" to { state, text -> state.resolveIssuer().value?.name?.organisation?.contains(text, true) == true } ) root.top = hbox(5.0) { button("New Transaction", FontAwesomeIconView(FontAwesomeIcon.PLUS)) { From e1458a40cb760a233a75431c6269198e13c8e9eb Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Mon, 9 Oct 2017 10:20:20 +0100 Subject: [PATCH 132/185] Revert code transformation; use of deprecated method --- .../net/corda/finance/contracts/CommercialPaperTests.kt | 3 +-- .../net/corda/finance/contracts/asset/CashTests.kt | 7 +++---- .../corda/node/services/vault/NodeVaultServiceTest.kt | 5 ++--- .../net/corda/node/services/vault/VaultWithCashTest.kt | 9 ++++----- 4 files changed, 10 insertions(+), 14 deletions(-) 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 9a8150ef0f..a1a296eb05 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt @@ -12,7 +12,6 @@ import net.corda.core.utilities.seconds import net.corda.finance.DOLLARS import net.corda.finance.`issued by` import net.corda.finance.contracts.asset.* -import net.corda.finance.contracts.asset.Cash.Companion.generateSpend import net.corda.testing.* import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.testing.node.MockServices @@ -270,7 +269,7 @@ class CommercialPaperTestsGeneric { // Alice pays $9000 to BigCorp to own some of their debt. moveTX = run { val builder = TransactionBuilder(DUMMY_NOTARY) - generateSpend(aliceServices, builder, 9000.DOLLARS, AnonymousParty(bigCorpServices.key.public), ourIdentity, emptySet()) + Cash.generateSpend(aliceServices, builder, 9000.DOLLARS, AnonymousParty(bigCorpServices.key.public)) CommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(aliceServices.key.public)) val ptx = aliceServices.signInitialTransaction(builder) val ptx2 = bigCorpServices.addSignature(ptx) diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt index e888d0705b..952aa3d9df 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt @@ -12,7 +12,6 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.OpaqueBytes import net.corda.finance.* -import net.corda.finance.contracts.asset.Cash.Companion.generateSpend import net.corda.finance.utils.sumCash import net.corda.finance.utils.sumCashBy import net.corda.finance.utils.sumCashOrNull @@ -506,7 +505,7 @@ class CashTests : TestDependencyInjectionBase() { private fun makeSpend(amount: Amount, dest: AbstractParty): WireTransaction { val tx = TransactionBuilder(DUMMY_NOTARY) database.transaction { - generateSpend(miniCorpServices, tx, amount, dest, ourIdentity, emptySet()) + Cash.generateSpend(miniCorpServices, tx, amount, dest) } return tx.toWireTransaction(miniCorpServices) } @@ -608,7 +607,7 @@ class CashTests : TestDependencyInjectionBase() { database.transaction { val tx = TransactionBuilder(DUMMY_NOTARY) - generateSpend(miniCorpServices, tx, 80.DOLLARS, ALICE, ourIdentity, setOf(MINI_CORP)) + Cash.generateSpend(miniCorpServices, tx, 80.DOLLARS, ALICE, setOf(MINI_CORP)) assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0]) } @@ -827,7 +826,7 @@ class CashTests : TestDependencyInjectionBase() { PartyAndAmount(THEIR_IDENTITY_1, 400.DOLLARS), PartyAndAmount(THEIR_IDENTITY_2, 150.DOLLARS) ) - generateSpend(miniCorpServices, tx, amount, to, ourIdentity, emptySet()) + Cash.generateSpend(miniCorpServices, tx, payments) } val wtx = tx.toWireTransaction(miniCorpServices) fun out(i: Int) = wtx.getOutput(i) as Cash.State 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 9d385d9663..5250e7f500 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 @@ -20,7 +20,6 @@ import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toNonEmptySet import net.corda.finance.* import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.Cash.Companion.generateSpend import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_KEY import net.corda.finance.contracts.getCashBalance @@ -516,7 +515,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { database.transaction { val moveTx = TransactionBuilder(services.myInfo.chooseIdentity()).apply { - generateSpend(services, this, Amount(1000, GBP), thirdPartyIdentity, ourIdentity, emptySet()) + Cash.generateSpend(services, this, Amount(1000, GBP), thirdPartyIdentity) }.toWireTransaction(services) service.notify(moveTx) } @@ -561,7 +560,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { // Move cash val moveTx = database.transaction { TransactionBuilder(newNotary).apply { - generateSpend(services, this, Amount(1000, GBP), thirdPartyIdentity, ourIdentity, emptySet()) + Cash.generateSpend(services, this, Amount(1000, GBP), thirdPartyIdentity) }.toWireTransaction(services) } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index aacc46ce2b..60e853d023 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -12,7 +12,6 @@ import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria import net.corda.core.transactions.TransactionBuilder import net.corda.finance.* import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.Cash.Companion.generateSpend import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_KEY import net.corda.finance.contracts.getCashBalance @@ -100,7 +99,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { database.transaction { // A tx that spends our money. val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY) - generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB, ourIdentity, emptySet()) + Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB) val spendPTX = services.signInitialTransaction(spendTXBuilder, freshKey) notaryServices.addSignature(spendPTX) } @@ -152,7 +151,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { database.transaction { try { val txn1Builder = TransactionBuilder(DUMMY_NOTARY) - generateSpend(services, txn1Builder, 60.DOLLARS, BOB, ourIdentity, emptySet()) + Cash.generateSpend(services, txn1Builder, 60.DOLLARS, BOB) val ptxn1 = notaryServices.signInitialTransaction(txn1Builder) val txn1 = services.addSignature(ptxn1, freshKey) println("txn1: ${txn1.id} spent ${((txn1.tx.outputs[0].data) as Cash.State).amount}") @@ -188,7 +187,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { database.transaction { try { val txn2Builder = TransactionBuilder(DUMMY_NOTARY) - generateSpend(services, txn2Builder, 80.DOLLARS, BOB, ourIdentity, emptySet()) + Cash.generateSpend(services, txn2Builder, 80.DOLLARS, BOB) val ptxn2 = notaryServices.signInitialTransaction(txn2Builder) val txn2 = services.addSignature(ptxn2, freshKey) println("txn2: ${txn2.id} spent ${((txn2.tx.outputs[0].data) as Cash.State).amount}") @@ -312,7 +311,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { database.transaction { // A tx that spends our money. val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY) - generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB, ourIdentity, emptySet()) + Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB) val spendPTX = notaryServices.signInitialTransaction(spendTXBuilder) val spendTX = services.addSignature(spendPTX, freshKey) services.recordTransactions(spendTX) From 7340a2e32f4348a92753a6384374e46af1f71a1d Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Mon, 9 Oct 2017 16:15:27 +0100 Subject: [PATCH 133/185] CORDA-654: Handle non-standard file systems in NodeInfoWatcher (#1818) Handle non-standard file systems such as JimFs, in NodeInfoWatcher. Instead of using `toFile()` to convert a Path to a File, open the Path for writing to directly. --- .../services/network/NodeInfoWatcherTest.kt | 58 ++++++++++++------- .../node/services/network/NodeInfoWatcher.kt | 15 ++--- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt index 268d23d394..d4436236ef 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt @@ -1,16 +1,16 @@ package net.corda.node.services.network +import com.google.common.jimfs.Configuration +import com.google.common.jimfs.Jimfs import net.corda.cordform.CordformNode import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.node.NodeInfo import net.corda.core.node.services.KeyManagementService -import net.corda.core.utilities.seconds import net.corda.node.services.identity.InMemoryIdentityService import net.corda.testing.ALICE import net.corda.testing.ALICE_KEY import net.corda.testing.DEV_TRUST_ROOT -import net.corda.testing.eventually import net.corda.testing.getTestPartyAndCertificate import net.corda.testing.node.MockKeyManagementService import net.corda.testing.node.NodeBasedTest @@ -56,6 +56,7 @@ class NodeInfoWatcherTest : NodeBasedTest() { @Test fun `save a NodeInfo`() { + assertEquals(0, folder.root.list().size) NodeInfoWatcher.saveToFile(folder.root.toPath(), nodeInfo, keyManagementService) assertEquals(1, folder.root.list().size) @@ -66,30 +67,45 @@ class NodeInfoWatcherTest : NodeBasedTest() { assertThat(contentOf(file)).isNotEmpty() } + @Test + fun `save a NodeInfo to JimFs`() { + val jimFs = Jimfs.newFileSystem(Configuration.unix()) + val jimFolder = jimFs.getPath("/nodeInfo") + NodeInfoWatcher.saveToFile(jimFolder, nodeInfo, keyManagementService) + } + @Test fun `load an empty Directory`() { nodeInfoPath.createDirectories() - nodeInfoWatcher.nodeInfoUpdates() + val subscription = nodeInfoWatcher.nodeInfoUpdates() .subscribe(testSubscriber) - advanceTime() + try { + advanceTime() - val readNodes = testSubscriber.onNextEvents.distinct() - assertEquals(0, readNodes.size) + val readNodes = testSubscriber.onNextEvents.distinct() + assertEquals(0, readNodes.size) + } finally { + subscription.unsubscribe() + } } @Test fun `load a non empty Directory`() { createNodeInfoFileInPath(nodeInfo) - nodeInfoWatcher.nodeInfoUpdates() + val subscription = nodeInfoWatcher.nodeInfoUpdates() .subscribe(testSubscriber) advanceTime() - val readNodes = testSubscriber.onNextEvents.distinct() + try { + val readNodes = testSubscriber.onNextEvents.distinct() - assertEquals(1, readNodes.size) - assertEquals(nodeInfo, readNodes.first()) + assertEquals(1, readNodes.size) + assertEquals(nodeInfo, readNodes.first()) + } finally { + subscription.unsubscribe() + } } @Test @@ -97,22 +113,24 @@ class NodeInfoWatcherTest : NodeBasedTest() { nodeInfoPath.createDirectories() // Start polling with an empty folder. - nodeInfoWatcher.nodeInfoUpdates() + val subscription = nodeInfoWatcher.nodeInfoUpdates() .subscribe(testSubscriber) - // Ensure the watch service is started. - advanceTime() - // Check no nodeInfos are read. - assertEquals(0, testSubscriber.valueCount) - createNodeInfoFileInPath(nodeInfo) + try { + // Ensure the watch service is started. + advanceTime() + // Check no nodeInfos are read. + assertEquals(0, testSubscriber.valueCount) + createNodeInfoFileInPath(nodeInfo) - advanceTime() + advanceTime() - // We need the WatchService to report a change and that might not happen immediately. - eventually(5.seconds) { + // 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() - assertEquals(1, readNodes.size) assertEquals(nodeInfo, readNodes.first()) + } finally { + subscription.unsubscribe() } } 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 83121ba969..7d8542d314 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 @@ -1,14 +1,8 @@ package net.corda.node.services.network import net.corda.cordform.CordformNode -import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignedData -import net.corda.core.internal.createDirectories -import net.corda.core.internal.div -import net.corda.core.internal.isDirectory -import net.corda.core.internal.isRegularFile -import net.corda.core.internal.list -import net.corda.core.internal.readAll +import net.corda.core.internal.* import net.corda.core.node.NodeInfo import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.deserialize @@ -17,6 +11,7 @@ import net.corda.core.utilities.loggerFor import rx.Observable import rx.Scheduler import rx.schedulers.Schedulers +import java.nio.file.Files import java.nio.file.Path import java.util.concurrent.TimeUnit import kotlin.streams.toList @@ -52,11 +47,9 @@ class NodeInfoWatcher(private val nodePath: Path, try { path.createDirectories() val serializedBytes = nodeInfo.serialize() - val regSig = keyManager.sign(serializedBytes.bytes, - nodeInfo.legalIdentities.first().owningKey) + val regSig = keyManager.sign(serializedBytes.bytes, nodeInfo.legalIdentities.first().owningKey) val signedData = SignedData(serializedBytes, regSig) - val file = (path / ("nodeInfo-" + SecureHash.sha256(serializedBytes.bytes).toString())).toFile() - file.writeBytes(signedData.serialize().bytes) + signedData.serialize().open().copyTo(path / "nodeInfo-${serializedBytes.hash}") } catch (e: Exception) { logger.warn("Couldn't write node info to file", e) } From 7ad754fe782d5f34ac3ad01323c9d5029c540a2f Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Mon, 9 Oct 2017 17:03:04 +0100 Subject: [PATCH 134/185] Add signature exchange to transaction key flow (#1417) Require a signature on a deterministic data blob (which includes X.500 name and public key) when exchanging new confidential identities, in order to ensure that the owner of the key pair wants it to represent the specified name, not just that the certificate owner states the key represents the given identity. --- .../corda/confidential/SwapIdentitiesFlow.kt | 74 ++++++++++++++++-- .../confidential/SwapIdentitiesHandler.kt | 16 +++- .../confidential/SwapIdentitiesFlowTests.kt | 75 +++++++++++++++++-- .../net/corda/core/crypto/DigitalSignature.kt | 1 + .../core/serialization/SerializationAPI.kt | 1 - 5 files changed, 148 insertions(+), 19 deletions(-) diff --git a/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesFlow.kt b/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesFlow.kt index 7257620d19..575c1f8d52 100644 --- a/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesFlow.kt +++ b/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesFlow.kt @@ -1,15 +1,32 @@ package net.corda.confidential import co.paralleluniverse.fibers.Suspendable +import net.corda.core.crypto.DigitalSignature +import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StartableByRPC import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate +import net.corda.core.internal.toX509CertHolder import net.corda.core.node.services.IdentityService +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap +import org.bouncycastle.asn1.DERSet +import org.bouncycastle.asn1.pkcs.CertificationRequestInfo +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import java.io.ByteArrayOutputStream +import java.nio.charset.Charset +import java.security.PublicKey +import java.security.SignatureException +import java.security.cert.CertPath +import java.util.* /** * Very basic flow which generates new confidential identities for parties in a transaction and exchanges the transaction @@ -27,8 +44,32 @@ class SwapIdentitiesFlow(private val otherParty: Party, object AWAITING_KEY : ProgressTracker.Step("Awaiting key") fun tracker() = ProgressTracker(AWAITING_KEY) - fun validateAndRegisterIdentity(identityService: IdentityService, otherSide: Party, anonymousOtherSide: PartyAndCertificate): PartyAndCertificate { - require(anonymousOtherSide.name == otherSide.name) + /** + * Generate the determinstic data blob the confidential identity's key holder signs to indicate they want to + * represent the subject named in the X.509 certificate. Note that this is never actually sent between nodes, + * but only the signature is sent. The blob is built independently on each node and the received signature + * verified against the expected blob, rather than exchanging the blob. + */ + fun buildDataToSign(confidentialIdentity: PartyAndCertificate): ByteArray { + val certOwnerAssert = CertificateOwnershipAssertion(confidentialIdentity.name, confidentialIdentity.owningKey) + return certOwnerAssert.serialize().bytes + } + + @Throws(SwapIdentitiesException::class) + fun validateAndRegisterIdentity(identityService: IdentityService, + otherSide: Party, + anonymousOtherSideBytes: PartyAndCertificate, + sigBytes: DigitalSignature): PartyAndCertificate { + val anonymousOtherSide: PartyAndCertificate = anonymousOtherSideBytes + if (anonymousOtherSide.name != otherSide.name) { + throw SwapIdentitiesException("Certificate subject must match counterparty's well known identity.") + } + val signature = DigitalSignature.WithKey(anonymousOtherSide.owningKey, sigBytes.bytes) + try { + signature.verify(buildDataToSign(anonymousOtherSideBytes)) + } catch(ex: SignatureException) { + throw SwapIdentitiesException("Signature does not match the expected identity ownership assertion.", ex) + } // Validate then store their identity so that we can prove the key in the transaction is owned by the // counterparty. identityService.verifyAndRegisterIdentity(anonymousOtherSide) @@ -40,6 +81,7 @@ class SwapIdentitiesFlow(private val otherParty: Party, override fun call(): LinkedHashMap { progressTracker.currentStep = AWAITING_KEY val legalIdentityAnonymous = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, revocationEnabled) + val serializedIdentity = SerializedBytes(legalIdentityAnonymous.serialize().bytes) // Special case that if we're both parties, a single identity is generated val identities = LinkedHashMap() @@ -47,13 +89,33 @@ class SwapIdentitiesFlow(private val otherParty: Party, identities.put(otherParty, legalIdentityAnonymous.party.anonymise()) } else { val otherSession = initiateFlow(otherParty) - val anonymousOtherSide = otherSession.sendAndReceive(legalIdentityAnonymous).unwrap { confidentialIdentity -> - validateAndRegisterIdentity(serviceHub.identityService, otherSession.counterparty, confidentialIdentity) - } + val data = buildDataToSign(legalIdentityAnonymous) + val ourSig: DigitalSignature.WithKey = serviceHub.keyManagementService.sign(data, legalIdentityAnonymous.owningKey) + val ourIdentWithSig = IdentityWithSignature(serializedIdentity, ourSig.withoutKey()) + val anonymousOtherSide = otherSession.sendAndReceive(ourIdentWithSig) + .unwrap { (confidentialIdentityBytes, theirSigBytes) -> + val confidentialIdentity: PartyAndCertificate = confidentialIdentityBytes.bytes.deserialize() + validateAndRegisterIdentity(serviceHub.identityService, otherParty, confidentialIdentity, theirSigBytes) + } identities.put(ourIdentity, legalIdentityAnonymous.party.anonymise()) - identities.put(otherSession.counterparty, anonymousOtherSide.party.anonymise()) + identities.put(otherParty, anonymousOtherSide.party.anonymise()) } return identities } + @CordaSerializable + data class IdentityWithSignature(val identity: SerializedBytes, val signature: DigitalSignature) } + +/** + * Data class used only in the context of asserting the owner of the private key for the listed key wants to use it + * to represent the named entity. This is pairs with an X.509 certificate (which asserts the signing identity says + * the key represents the named entity), but protects against a certificate authority incorrectly claiming others' + * keys. + */ +@CordaSerializable +data class CertificateOwnershipAssertion(val x500Name: CordaX500Name, + val publicKey: PublicKey) + +open class SwapIdentitiesException @JvmOverloads constructor(message: String, cause: Throwable? = null) + : FlowException(message, cause) \ No newline at end of file diff --git a/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesHandler.kt b/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesHandler.kt index 753d9a3927..a5a17ccdf8 100644 --- a/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesHandler.kt +++ b/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesHandler.kt @@ -4,6 +4,9 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession import net.corda.core.identity.PartyAndCertificate +import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap @@ -20,9 +23,14 @@ class SwapIdentitiesHandler(val otherSideSession: FlowSession, val revocationEna override fun call() { val revocationEnabled = false progressTracker.currentStep = SENDING_KEY - val legalIdentityAnonymous = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, revocationEnabled) - otherSideSession.sendAndReceive(legalIdentityAnonymous).unwrap { confidentialIdentity -> - SwapIdentitiesFlow.validateAndRegisterIdentity(serviceHub.identityService, otherSideSession.counterparty, confidentialIdentity) - } + val ourConfidentialIdentity = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, revocationEnabled) + val serializedIdentity = SerializedBytes(ourConfidentialIdentity.serialize().bytes) + val data = SwapIdentitiesFlow.buildDataToSign(ourConfidentialIdentity) + val ourSig = serviceHub.keyManagementService.sign(data, ourConfidentialIdentity.owningKey) + otherSideSession.sendAndReceive(SwapIdentitiesFlow.IdentityWithSignature(serializedIdentity, ourSig.withoutKey())) + .unwrap { (theirConfidentialIdentityBytes, theirSigBytes) -> + val theirConfidentialIdentity = theirConfidentialIdentityBytes.deserialize() + SwapIdentitiesFlow.validateAndRegisterIdentity(serviceHub.identityService, otherSideSession.counterparty, theirConfidentialIdentity, theirSigBytes) + } } } \ No newline at end of file diff --git a/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt b/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt index 5554b8ea34..55e9ef9d26 100644 --- a/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt +++ b/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt @@ -4,16 +4,10 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.utilities.getOrThrow -import net.corda.testing.ALICE -import net.corda.testing.BOB -import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.chooseIdentity +import net.corda.testing.* import net.corda.testing.node.MockNetwork import org.junit.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotEquals -import kotlin.test.assertTrue +import kotlin.test.* class SwapIdentitiesFlowTests { @Test @@ -52,4 +46,69 @@ class SwapIdentitiesFlowTests { mockNet.stopNodes() } + + /** + * Check that flow is actually validating the name on the certificate presented by the counterparty. + */ + @Test + fun `verifies identity name`() { + // We run this in parallel threads to help catch any race conditions that may exist. + val mockNet = MockNetwork(false, true) + + // Set up values we'll need + val notaryNode = mockNet.createNotaryNode(DUMMY_NOTARY.name) + val aliceNode = mockNet.createPartyNode(ALICE.name) + val bobNode = mockNet.createPartyNode(BOB.name) + val bob: Party = bobNode.services.myInfo.chooseIdentity() + val notBob = notaryNode.database.transaction { + notaryNode.services.keyManagementService.freshKeyAndCert(notaryNode.services.myInfo.chooseIdentityAndCert(), false) + } + val sigData = SwapIdentitiesFlow.buildDataToSign(notBob) + val signature = notaryNode.services.keyManagementService.sign(sigData, notBob.owningKey) + assertFailsWith("Certificate subject must match counterparty's well known identity.") { + SwapIdentitiesFlow.validateAndRegisterIdentity(aliceNode.services.identityService, bob, notBob, signature.withoutKey()) + } + + mockNet.stopNodes() + } + + /** + * Check that flow is actually validating its the signature presented by the counterparty. + */ + @Test + fun `verifies signature`() { + // We run this in parallel threads to help catch any race conditions that may exist. + val mockNet = MockNetwork(false, true) + + // Set up values we'll need + val notaryNode = mockNet.createNotaryNode(DUMMY_NOTARY.name) + val aliceNode = mockNet.createPartyNode(ALICE.name) + val bobNode = mockNet.createPartyNode(BOB.name) + val bob: Party = bobNode.services.myInfo.chooseIdentity() + // Check that the wrong signature is rejected + notaryNode.database.transaction { + notaryNode.services.keyManagementService.freshKeyAndCert(notaryNode.services.myInfo.chooseIdentityAndCert(), false) + }.let { anonymousNotary -> + val sigData = SwapIdentitiesFlow.buildDataToSign(anonymousNotary) + val signature = notaryNode.services.keyManagementService.sign(sigData, anonymousNotary.owningKey) + assertFailsWith("Signature does not match the given identity and nonce") { + SwapIdentitiesFlow.validateAndRegisterIdentity(aliceNode.services.identityService, bob, anonymousNotary, signature.withoutKey()) + } + } + // Check that the right signing key, but wrong identity is rejected + val anonymousAlice = aliceNode.database.transaction { + aliceNode.services.keyManagementService.freshKeyAndCert(aliceNode.services.myInfo.chooseIdentityAndCert(), false) + } + bobNode.database.transaction { + bobNode.services.keyManagementService.freshKeyAndCert(bobNode.services.myInfo.chooseIdentityAndCert(), false) + }.let { anonymousBob -> + val sigData = SwapIdentitiesFlow.buildDataToSign(anonymousAlice) + val signature = bobNode.services.keyManagementService.sign(sigData, anonymousBob.owningKey) + assertFailsWith("Signature does not match the given identity and nonce.") { + SwapIdentitiesFlow.validateAndRegisterIdentity(aliceNode.services.identityService, bob, anonymousBob, signature.withoutKey()) + } + } + + mockNet.stopNodes() + } } diff --git a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt index d343913712..0db44ba841 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt @@ -46,5 +46,6 @@ open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) { */ @Throws(InvalidKeyException::class, SignatureException::class) fun isValid(content: ByteArray) = by.isValid(content, this) + fun withoutKey() : DigitalSignature = DigitalSignature(this.bytes) } } 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 b0499938ef..f77e3ae122 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt @@ -196,7 +196,6 @@ fun T.serialize(serializationFactory: SerializationFactory = Serializa * A type safe wrapper around a byte array that contains a serialised object. You can call [SerializedBytes.deserialize] * to get the original object back. */ -@Suppress("unused") // Type parameter is just for documentation purposes. class SerializedBytes(bytes: ByteArray) : OpaqueBytes(bytes) { // It's OK to use lazy here because SerializedBytes is configured to use the ImmutableClassSerializer. val hash: SecureHash by lazy { bytes.sha256() } From 70f3d02ce49a4084edadc16d5140b7bbe8d298d2 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Mon, 9 Oct 2017 17:14:55 +0100 Subject: [PATCH 135/185] Update docs on how IdentitySyncFlow works (#1816) --- docs/source/api-identity.rst | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/docs/source/api-identity.rst b/docs/source/api-identity.rst index c87b50a45f..fb3a18342e 100644 --- a/docs/source/api-identity.rst +++ b/docs/source/api-identity.rst @@ -93,7 +93,8 @@ Identity synchronization flow When constructing a transaction whose input states reference confidential identities, it is common for other signing entities (counterparties) to require to know which well known identities those confidential identities map to. The -``IdentitySyncFlow`` handles this process, and you can see an example of its use in ``TwoPartyTradeFlow.kt``: +``IdentitySyncFlow`` handles distribution of a node's confidential identities, and you can see an example of its +use in ``TwoPartyTradeFlow.kt``: .. container:: codeset @@ -105,27 +106,33 @@ entities (counterparties) to require to know which well known identities those c The identity synchronization flow goes through the following key steps: -1. Extract participant identities from all input and output states and remove any well known identities. Required signers - on commands are currently ignored as they are presumed to be included in the participants on states, or to be well - known identities of services (such as an oracle service). +1. Extract participant identities from all input and output states. Filter this set down to confidential identities + of the flow's well known identity. Required signers on commands are currently ignored as they are presumed to be + included in the participants on states, or to be well known identities of services (such as an oracle service). 2. For each counterparty node, send a list of the public keys of the confidential identities, and receive back a list of those the counterparty needs the certificate path for. 3. Verify the requested list of identities contains only confidential identities in the offered list, and abort otherwise. 4. Send the requested confidential identities as ``PartyAndCertificate`` instances to the counterparty. -.. note:: ``IdentitySyncFlow`` works on a push basis. The initiating node can only send confidential identities it has - the X.509 certificates for, and the remote nodes can only request confidential identities being offered (are - referenced in the transaction passed to the initiating flow). There is no standard flow for nodes to collect +.. note:: ``IdentitySyncFlow`` works on a push basis. Receiving nodes can only request confidential identities being + offered by the initiating node. There is no standard flow for nodes to collect confidential identities before assembling a transaction, and this is left for individual flows to manage if required. -``IdentitySyncFlow`` will serve all confidential identities in the provided transaction, irrespective of well known -identity. This is important for more complex transaction cases with 3+ parties, for example: +``IdentitySyncFlow`` will serve only confidential identities in the provided transaction, limited to those that are +signed by the well known identity the flow is initiated by. This is done to avoid a risk of a node including +states it doesn't have the well known identity of participants in, to try convincing one of its counterparties to +reveal the identity. In case of a more complex transaction where multiple well known identities need confidential +identities distributed this flow should be run by each node in turn. For example: * Alice is building the transaction, and provides some input state *x* owned by a confidential identity of Alice * Bob provides some input state *y* owned by a confidential identity of Bob * Charlie provides some input state *z* owned by a confidential identity of Charlie -Alice may know all of the confidential identities ahead of time, but Bob not know about Charlie's and vice-versa. -The assembled transaction therefore has three input states *x*, *y* and *z*, for which only Alice possesses certificates -for all confidential identities. ``IdentitySyncFlow`` must send not just Alice's confidential identity but also any other -identities in the transaction to the Bob and Charlie. \ No newline at end of file +Alice, Bob and Charlie must all run ``IdentitySyncFlow`` to send their involved confidential identities to the other +parties. For an illustration of the security implications of not requiring this, consider: + +1. Alice is building the transaction, and provides some input state *x* owned by a confidential identity of Alice +2. Bob provides some input state *y* owned by a confidential identity it doesn't know the well known identity of, but + Alice does. +3. Alice runs ``IdentitySyncFlow`` and sends not just their confidential identity, but also the confidential identity + in state *y*, violating the privacy model. \ No newline at end of file From 747830ff90e959ab81778b15efacb7cc7381349c Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Mon, 9 Oct 2017 17:38:45 +0100 Subject: [PATCH 136/185] Scan API for addition of new abstract methods. (#1854) * Scan API for addition of new abstract methods. * Make sure we ignore blank lines when counting API changes. * Add 6 new abstract APIs to our API definition. --- .ci/api-current.txt | 8 ++++++++ .ci/check-api-changes.sh | 38 +++++++++++++++++++++++++++++++++----- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index cd28b2719a..6892aeae53 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -465,6 +465,7 @@ public interface net.corda.core.cordapp.Cordapp @org.jetbrains.annotations.NotNull public abstract List getRpcFlows() @org.jetbrains.annotations.NotNull public abstract List getSchedulableFlows() @org.jetbrains.annotations.NotNull public abstract List getSerializationWhitelists() + @org.jetbrains.annotations.NotNull public abstract List getServiceFlows() @org.jetbrains.annotations.NotNull public abstract List getServices() ## public final class net.corda.core.cordapp.CordappContext extends java.lang.Object @@ -1353,6 +1354,7 @@ public interface net.corda.core.messaging.CordaRPCOps extends net.corda.core.mes @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.NodeInfo nodeInfo() @org.jetbrains.annotations.Nullable public abstract net.corda.core.node.NodeInfo nodeInfoFromParty(net.corda.core.identity.AbstractParty) @org.jetbrains.annotations.NotNull public abstract List notaryIdentities() + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party notaryPartyFromX500Name(net.corda.core.identity.CordaX500Name) @org.jetbrains.annotations.NotNull public abstract java.io.InputStream openAttachment(net.corda.core.crypto.SecureHash) @org.jetbrains.annotations.NotNull public abstract Set partiesFromName(String, boolean) @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party partyFromKey(java.security.PublicKey) @@ -1485,6 +1487,10 @@ public static final class net.corda.core.messaging.StateMachineUpdate$Removed ex public int hashCode() public String toString() ## +public interface net.corda.core.node.AppServiceHub extends net.corda.core.node.ServiceHub + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.FlowHandle startFlow(net.corda.core.flows.FlowLogic) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.FlowProgressHandle startTrackedFlow(net.corda.core.flows.FlowLogic) +## public final class net.corda.core.node.NodeInfo extends java.lang.Object public (List, List, int, long) @org.jetbrains.annotations.NotNull public final List component1() @@ -1579,10 +1585,12 @@ public interface net.corda.core.node.services.NetworkMapCache @org.jetbrains.annotations.Nullable public abstract net.corda.core.node.NodeInfo getNodeByLegalName(net.corda.core.identity.CordaX500Name) @org.jetbrains.annotations.NotNull public abstract net.corda.core.concurrent.CordaFuture getNodeReady() @org.jetbrains.annotations.NotNull public abstract List getNodesByLegalIdentityKey(java.security.PublicKey) + @org.jetbrains.annotations.NotNull public abstract List getNodesByLegalName(net.corda.core.identity.CordaX500Name) @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party getNotary(net.corda.core.identity.CordaX500Name) @org.jetbrains.annotations.NotNull public abstract List getNotaryIdentities() @org.jetbrains.annotations.Nullable public abstract net.corda.core.node.services.PartyInfo getPartyInfo(net.corda.core.identity.Party) @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party getPeerByLegalName(net.corda.core.identity.CordaX500Name) + @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.PartyAndCertificate getPeerCertificateByLegalName(net.corda.core.identity.CordaX500Name) public abstract boolean isNotary(net.corda.core.identity.Party) public abstract boolean isValidatingNotary(net.corda.core.identity.Party) @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed track() diff --git a/.ci/check-api-changes.sh b/.ci/check-api-changes.sh index e2bd6afddc..7694d995dd 100755 --- a/.ci/check-api-changes.sh +++ b/.ci/check-api-changes.sh @@ -11,9 +11,37 @@ if [ ! -f $apiCurrent ]; then fi diffContents=`diff -u $apiCurrent $APIHOME/../build/api/api-corda-*.txt` -echo "Diff contents: " +echo "Diff contents:" echo "$diffContents" -removals=`echo "$diffContents" | grep "^-\s" | wc -l` -echo "Number of API removals/changes: "$removals -echo "Exiting with exit code" $removals -exit $removals +echo + +# A removed line means that an API was either deleted or modified. +removals=$(echo "$diffContents" | grep "^-\s") +removalCount=`grep -v "^$" < Date: Mon, 9 Oct 2017 20:08:08 +0100 Subject: [PATCH 137/185] CORDA-686 - Split Cordapp gradle plugin from cordformation (#1817) Added CorDapp gradle plugin written in Kotlin and bumped the version of gradle plugins to 2.0.0 to reflect that this backwards incompatible change is a part of the on going stabilisation of the Corda gradle plugin suite. --- .idea/compiler.xml | 4 + build.gradle | 21 ++--- confidential-identities/build.gradle | 5 +- constants.properties | 2 +- docs/source/changelog.rst | 4 + docs/source/cordapp-build-systems.rst | 6 +- docs/source/example-code/build.gradle | 13 ++-- finance/build.gradle | 2 +- gradle-plugins/build.gradle | 4 +- gradle-plugins/cordapp/README.md | 10 +++ gradle-plugins/cordapp/build.gradle | 18 +++++ .../kotlin/net/corda/plugins/CordappPlugin.kt | 76 +++++++++++++++++++ .../main/kotlin/net/corda/plugins/Utils.kt | 17 +++++ .../net.corda.plugins.cordapp.properties | 1 + gradle-plugins/cordformation/build.gradle | 7 +- .../net/corda/plugins/Cordformation.groovy | 62 +-------------- gradle-plugins/settings.gradle | 1 + samples/attachment-demo/build.gradle | 1 + samples/bank-of-corda-demo/build.gradle | 1 + samples/irs-demo/build.gradle | 1 + samples/notary-demo/build.gradle | 1 + samples/simm-valuation-demo/build.gradle | 1 + samples/trader-demo/build.gradle | 1 + 23 files changed, 167 insertions(+), 92 deletions(-) create mode 100644 gradle-plugins/cordapp/README.md create mode 100644 gradle-plugins/cordapp/build.gradle create mode 100644 gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/CordappPlugin.kt create mode 100644 gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/Utils.kt create mode 100644 gradle-plugins/cordapp/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.cordapp.properties diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 2d6467076b..3516efcb93 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -2,6 +2,8 @@ + + @@ -19,6 +21,8 @@ + + diff --git a/build.gradle b/build.gradle index bf12b2ff1f..9b19ced795 100644 --- a/build.gradle +++ b/build.gradle @@ -59,6 +59,7 @@ buildscript { classpath "net.corda.plugins:publish-utils:$gradle_plugins_version" classpath "net.corda.plugins:quasar-utils:$gradle_plugins_version" classpath "net.corda.plugins:cordformation:$gradle_plugins_version" + classpath "net.corda.plugins:cordapp:$gradle_plugins_version" classpath "net.corda.plugins:api-scanner:$gradle_plugins_version" classpath 'com.github.ben-manes:gradle-versions-plugin:0.15.0' classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version" @@ -180,21 +181,21 @@ repositories { // Required for building out the fat JAR. dependencies { - cordaCompile project(':node') + compile project(':node') compile "com.google.guava:guava:$guava_version" // Set to corda compile to ensure it exists now deploy nodes no longer relies on build - cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts') - cordaCompile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') + compile project(path: ":node:capsule", configuration: 'runtimeArtifacts') + compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') // For the buildCordappDependenciesJar task - cordaRuntime project(':client:jfx') - cordaRuntime project(':client:mock') - cordaRuntime project(':client:rpc') - cordaRuntime project(':core') - cordaRuntime project(':confidential-identities') - cordaRuntime project(':finance') - cordaRuntime project(':webserver') + runtime project(':client:jfx') + runtime project(':client:mock') + runtime project(':client:rpc') + runtime project(':core') + runtime project(':confidential-identities') + runtime project(':finance') + runtime project(':webserver') testCompile project(':test-utils') } diff --git a/confidential-identities/build.gradle b/confidential-identities/build.gradle index 04212b3df1..c6a29c0d2e 100644 --- a/confidential-identities/build.gradle +++ b/confidential-identities/build.gradle @@ -6,15 +6,12 @@ apply plugin: 'kotlin' apply plugin: CanonicalizerPlugin apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.quasar-utils' -apply plugin: 'net.corda.plugins.cordformation' apply plugin: 'com.jfrog.artifactory' description 'Corda Experimental Confidential Identities' dependencies { - // Note the :confidential-identities module is a CorDapp in its own right - // and CorDapps using :confidential-identities features should use 'cordapp' not 'compile' linkage. - cordaCompile project(':core') + compile project(':core') // Quasar, for suspendable fibres. compileOnly "co.paralleluniverse:quasar-core:$quasar_version:jdk8" diff --git a/constants.properties b/constants.properties index dd42901d00..165810dcd2 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=1.1.2 +gradlePluginsVersion=2.0.0 kotlinVersion=1.1.50 guavaVersion=21.0 bouncycastleVersion=1.57 diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 0955876373..c6bffa9c85 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -8,6 +8,10 @@ UNRELEASED ---------- * ``FlowLogic`` now exposes a series of function called ``receiveAll(...)`` allowing to join ``receive(...)`` instructions. +* The ``Cordformation`` gradle plugin has been split into ``cordformation`` and ``cordapp``. The former builds and + deploys nodes for development and testing, the latter turns a project into a cordapp project that generates JARs in + the standard CorDapp format. + * ``Cordform`` and node identity generation * Cordform may not specify a value for ``NetworkMap``, when that happens, during the task execution the following happens: 1. Each node is started and its signed serialized NodeInfo is written to disk in the node base directory. diff --git a/docs/source/cordapp-build-systems.rst b/docs/source/cordapp-build-systems.rst index 398bfa0dc1..016d741bf2 100644 --- a/docs/source/cordapp-build-systems.rst +++ b/docs/source/cordapp-build-systems.rst @@ -16,7 +16,7 @@ For example if your Cordapp depends on ``corda-core``, ``your-other-cordapp`` an JAR will contain all classes and resources from the ``apache-commons`` JAR and its dependencies and *nothing* from the other two JARs. -.. note:: The rest of this tutorial assumes you are using ``gradle``, the ``cordformation`` plugin and have forked from +.. note:: The rest of this tutorial assumes you are using ``gradle``, the ``cordapp`` plugin and have forked from one of our cordapp templates. The ``jar`` task included by default in the cordapp templates will automatically build your JAR in this format as long @@ -40,7 +40,7 @@ To make use of the Corda test facilities you must; .. warning:: Never include ``corda-test-utils`` as a ``compile`` or ``cordaCompile`` dependency. -These configurations work by the ``cordformation`` plugin adding ``cordaCompile`` as a new configuration that ``compile`` +These configurations work by the ``cordapp`` plugin adding ``cordaCompile`` as a new configuration that ``compile`` extends from, and ``cordaRuntime`` which ``runtime`` extends from. Choosing your Corda version @@ -57,7 +57,7 @@ can find the latest published version of both here: https://bintray.com/r3/corda ``corda_gradle_plugins_versions`` are given in the form ``major.minor.patch``. You should use the same ``major`` and ``minor`` versions as the Corda version you are using, and the latest ``patch`` version. A list of all the available -versions can be found here: https://bintray.com/r3/corda/cordformation. +versions can be found here: https://bintray.com/r3/corda/cordapp. In certain cases, you may also wish to build against the unstable Master branch. See :doc:`building-against-master`. diff --git a/docs/source/example-code/build.gradle b/docs/source/example-code/build.gradle index 6709d9fcbe..ae0b57fc34 100644 --- a/docs/source/example-code/build.gradle +++ b/docs/source/example-code/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'kotlin' apply plugin: 'application' apply plugin: 'net.corda.plugins.cordformation' +apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.quasar-utils' repositories { @@ -30,9 +31,9 @@ sourceSets { compileTestJava.dependsOn tasks.getByPath(':node:capsule:buildCordaJAR') dependencies { - cordaCompile project(':core') - cordaCompile project(':client:jfx') - cordaCompile project(':node-driver') + compile project(':core') + compile project(':client:jfx') + compile project(':node-driver') testCompile project(':verifier') testCompile project(':test-utils') @@ -42,11 +43,11 @@ dependencies { exclude group: "junit" } - cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts') - cordaCompile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') + compile project(path: ":node:capsule", configuration: 'runtimeArtifacts') + compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') // Corda Plugins: dependent flows and services - cordapp project(':finance') + compile project(':finance') } mainClassName = "net.corda.docs.ClientRpcTutorialKt" diff --git a/finance/build.gradle b/finance/build.gradle index 7ded74b45d..3386d94b40 100644 --- a/finance/build.gradle +++ b/finance/build.gradle @@ -5,7 +5,7 @@ apply plugin: 'kotlin-jpa' apply plugin: CanonicalizerPlugin apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.quasar-utils' -apply plugin: 'net.corda.plugins.cordformation' +apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'com.jfrog.artifactory' description 'Corda finance modules' diff --git a/gradle-plugins/build.gradle b/gradle-plugins/build.gradle index acfdbb2b05..88169c215c 100644 --- a/gradle-plugins/build.gradle +++ b/gradle-plugins/build.gradle @@ -10,6 +10,7 @@ buildscript { ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion") ext.bouncycastle_version = constants.getProperty("bouncycastleVersion") ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion") + ext.kotlin_version = constants.getProperty("kotlinVersion") repositories { mavenLocal() @@ -19,6 +20,7 @@ buildscript { dependencies { classpath "net.corda.plugins:publish-utils:$gradle_plugins_version" classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -39,7 +41,7 @@ bintrayConfig { projectUrl = 'https://github.com/corda/corda' gpgSign = true gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE') - publications = ['cordformation', 'quasar-utils', 'cordform-common', 'api-scanner'] + publications = ['cordformation', 'quasar-utils', 'cordform-common', 'api-scanner', 'cordapp'] license { name = 'Apache-2.0' url = 'https://www.apache.org/licenses/LICENSE-2.0' diff --git a/gradle-plugins/cordapp/README.md b/gradle-plugins/cordapp/README.md new file mode 100644 index 0000000000..6b0cebe690 --- /dev/null +++ b/gradle-plugins/cordapp/README.md @@ -0,0 +1,10 @@ +# Cordapp Gradle Plugin + +## Purpose + +To transform any project this plugin is applied to into a cordapp project that generates a cordapp JAR. + +## Effects + +Will modify the default JAR task to create a CorDapp format JAR instead [see here](https://docs.corda.net/cordapp-build-systems.html) +for more information. \ No newline at end of file diff --git a/gradle-plugins/cordapp/build.gradle b/gradle-plugins/cordapp/build.gradle new file mode 100644 index 0000000000..dc284faca1 --- /dev/null +++ b/gradle-plugins/cordapp/build.gradle @@ -0,0 +1,18 @@ +apply plugin: 'kotlin' +apply plugin: 'net.corda.plugins.publish-utils' + +description 'Turns a project into a cordapp project that produces cordapp fat JARs' + +repositories { + mavenCentral() + jcenter() +} + +dependencies { + compile gradleApi() + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" +} + +publish { + name project.name +} \ No newline at end of file diff --git a/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/CordappPlugin.kt b/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/CordappPlugin.kt new file mode 100644 index 0000000000..ce65e18e5f --- /dev/null +++ b/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/CordappPlugin.kt @@ -0,0 +1,76 @@ +package net.corda.plugins + +import org.gradle.api.* +import org.gradle.api.artifacts.* +import org.gradle.jvm.tasks.Jar +import java.io.File + +/** + * The Cordapp plugin will turn a project into a cordapp project which builds cordapp JARs with the correct format + * and with the information needed to run on Corda. + */ +class CordappPlugin : Plugin { + override fun apply(project: Project) { + project.logger.info("Configuring ${project.name} as a cordapp") + + Utils.createCompileConfiguration("cordapp", project) + Utils.createCompileConfiguration("cordaCompile", project) + + val configuration: Configuration = project.configurations.create("cordaRuntime") + configuration.isTransitive = false + project.configurations.single { it.name == "runtime" }.extendsFrom(configuration) + + configureCordappJar(project) + } + + /** + * Configures this project's JAR as a Cordapp JAR + */ + private fun configureCordappJar(project: Project) { + // Note: project.afterEvaluate did not have full dependency resolution completed, hence a task is used instead + val task = project.task("configureCordappFatJar") + val jarTask = project.tasks.single { it.name == "jar" } as Jar + task.doLast { + jarTask.from(getDirectNonCordaDependencies(project).map { project.zipTree(it)}).apply { + exclude("META-INF/*.SF") + exclude("META-INF/*.DSA") + exclude("META-INF/*.RSA") + } + } + jarTask.dependsOn(task) + } + + private fun getDirectNonCordaDependencies(project: Project): Set { + project.logger.info("Finding direct non-corda dependencies for inclusion in CorDapp JAR") + val excludes = listOf( + mapOf("group" to "org.jetbrains.kotlin", "name" to "kotlin-stdlib"), + mapOf("group" to "org.jetbrains.kotlin", "name" to "kotlin-stdlib-jre8"), + mapOf("group" to "org.jetbrains.kotlin", "name" to "kotlin-reflect"), + mapOf("group" to "co.paralleluniverse", "name" to "quasar-core") + ) + + val runtimeConfiguration = project.configuration("runtime") + // The direct dependencies of this project + val excludeDeps = project.configuration("cordapp").allDependencies + + project.configuration("cordaCompile").allDependencies + + project.configuration("cordaRuntime").allDependencies + val directDeps = runtimeConfiguration.allDependencies - excludeDeps + // We want to filter out anything Corda related or provided by Corda, like kotlin-stdlib and quasar + val filteredDeps = directDeps.filter { dep -> + excludes.none { exclude -> (exclude["group"] == dep.group) && (exclude["name"] == dep.name) } + } + filteredDeps.forEach { + // net.corda or com.r3.corda.enterprise may be a core dependency which shouldn't be included in this cordapp so give a warning + if ((it.group.startsWith("net.corda.") || it.group.startsWith("com.r3.corda.enterprise."))) { + project.logger.warn("You appear to have included a Corda platform component ($it) using a 'compile' or 'runtime' dependency." + + "This can cause node stability problems. Please use 'corda' instead." + + "See http://docs.corda.net/cordapp-build-systems.html") + } else { + project.logger.info("Including dependency in CorDapp JAR: $it") + } + } + return filteredDeps.map { runtimeConfiguration.files(it) }.flatten().toSet() + } + + private fun Project.configuration(name: String): Configuration = configurations.single { it.name == name } +} diff --git a/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/Utils.kt b/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/Utils.kt new file mode 100644 index 0000000000..33e552ca7d --- /dev/null +++ b/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/Utils.kt @@ -0,0 +1,17 @@ +package net.corda.plugins + +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration + +class Utils { + companion object { + @JvmStatic + fun createCompileConfiguration(name: String, project: Project) { + if(!project.configurations.any { it.name == name }) { + val configuration = project.configurations.create(name) + configuration.isTransitive = false + project.configurations.single { it.name == "compile" }.extendsFrom(configuration) + } + } + } +} \ No newline at end of file diff --git a/gradle-plugins/cordapp/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.cordapp.properties b/gradle-plugins/cordapp/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.cordapp.properties new file mode 100644 index 0000000000..90871e27c8 --- /dev/null +++ b/gradle-plugins/cordapp/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.cordapp.properties @@ -0,0 +1 @@ +implementation-class=net.corda.plugins.CordappPlugin diff --git a/gradle-plugins/cordformation/build.gradle b/gradle-plugins/cordformation/build.gradle index 414aa34b30..828647d434 100644 --- a/gradle-plugins/cordformation/build.gradle +++ b/gradle-plugins/cordformation/build.gradle @@ -1,10 +1,4 @@ buildscript { - // For sharing constants between builds - Properties constants = new Properties() - file("$projectDir/../../constants.properties").withInputStream { constants.load(it) } - - ext.kotlin_version = constants.getProperty("kotlinVersion") - repositories { mavenCentral() } @@ -41,6 +35,7 @@ sourceSets { dependencies { compile gradleApi() compile localGroovy() + compile project(":cordapp") noderunner "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy index 2fe24a7933..eeb4443801 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy @@ -9,40 +9,6 @@ import org.gradle.api.artifacts.Configuration * testing, and debugging. It will prepopulate several fields in the configuration and create a simple node runner. */ class Cordformation implements Plugin { - void apply(Project project) { - createCompileConfiguration("cordapp", project) - createCompileConfiguration("cordaCompile", project) - - Configuration configuration = project.configurations.create("cordaRuntime") - configuration.transitive = false - project.configurations.runtime.extendsFrom configuration - - configureCordappJar(project) - } - - private void createCompileConfiguration(String name, Project project) { - Configuration configuration = project.configurations.create(name) - configuration.transitive = false - project.configurations.compile.extendsFrom configuration - } - - /** - * Configures this project's JAR as a Cordapp JAR - */ - private void configureCordappJar(Project project) { - // Note: project.afterEvaluate did not have full dependency resolution completed, hence a task is used instead - def task = project.task('configureCordappFatJar') { - doLast { - project.tasks.jar.from(getDirectNonCordaDependencies(project).collect { project.zipTree(it)}) { - exclude "META-INF/*.SF" - exclude "META-INF/*.DSA" - exclude "META-INF/*.RSA" - } - } - } - project.tasks.jar.dependsOn task - } - /** * Gets a resource file from this plugin's JAR file. * @@ -56,31 +22,7 @@ class Cordformation implements Plugin { }, filePathInJar).asFile() } - private static Set getDirectNonCordaDependencies(Project project) { - def excludes = [ - [group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib'], - [group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jre8'], - [group: 'org.jetbrains.kotlin', name: 'kotlin-reflect'], - [group: 'co.paralleluniverse', name: 'quasar-core'] - ] - - project.with { - // The direct dependencies of this project - def excludeDeps = configurations.cordapp.allDependencies + configurations.cordaCompile.allDependencies + configurations.cordaRuntime.allDependencies - def directDeps = configurations.runtime.allDependencies - excludeDeps - // We want to filter out anything Corda related or provided by Corda, like kotlin-stdlib and quasar - def filteredDeps = directDeps.findAll { excludes.collect { exclude -> (exclude.group == it.group) && (exclude.name == it.name) }.findAll { it }.isEmpty() } - filteredDeps.each { - // net.corda or com.r3.corda.enterprise may be a core dependency which shouldn't be included in this cordapp so give a warning - if (it.group && (it.group.startsWith('net.corda.') || it.group.startsWith('com.r3.corda.enterprise.'))) { - logger.warn("You appear to have included a Corda platform component ($it) using a 'compile' or 'runtime' dependency." + - "This can cause node stability problems. Please use 'corda' instead." + - "See http://docs.corda.net/cordapp-build-systems.html") - } else { - logger.info("Including dependency in CorDapp JAR: $it") - } - } - return filteredDeps.collect { configurations.runtime.files it }.flatten().toSet() - } + void apply(Project project) { + Utils.createCompileConfiguration("cordapp", project) } } diff --git a/gradle-plugins/settings.gradle b/gradle-plugins/settings.gradle index 64349cda39..995cd8c899 100644 --- a/gradle-plugins/settings.gradle +++ b/gradle-plugins/settings.gradle @@ -4,3 +4,4 @@ include 'quasar-utils' include 'cordformation' include 'cordform-common' include 'api-scanner' +include 'cordapp' \ No newline at end of file diff --git a/samples/attachment-demo/build.gradle b/samples/attachment-demo/build.gradle index 379fcf6af8..5fa296e26d 100644 --- a/samples/attachment-demo/build.gradle +++ b/samples/attachment-demo/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'kotlin' apply plugin: 'idea' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.cordformation' apply plugin: 'maven-publish' diff --git a/samples/bank-of-corda-demo/build.gradle b/samples/bank-of-corda-demo/build.gradle index c673a10da1..043ead8c0b 100644 --- a/samples/bank-of-corda-demo/build.gradle +++ b/samples/bank-of-corda-demo/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'kotlin' apply plugin: 'idea' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.cordformation' apply plugin: 'maven-publish' diff --git a/samples/irs-demo/build.gradle b/samples/irs-demo/build.gradle index bc482b26d7..7d1dccfb8d 100644 --- a/samples/irs-demo/build.gradle +++ b/samples/irs-demo/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'kotlin' apply plugin: 'idea' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.cordformation' apply plugin: 'maven-publish' apply plugin: 'application' diff --git a/samples/notary-demo/build.gradle b/samples/notary-demo/build.gradle index fbbaa6a9a7..a13ab70ad8 100644 --- a/samples/notary-demo/build.gradle +++ b/samples/notary-demo/build.gradle @@ -5,6 +5,7 @@ apply plugin: 'kotlin' apply plugin: 'idea' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.cordformation' apply plugin: 'maven-publish' diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index ffce26eb4f..ec982a20a7 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -7,6 +7,7 @@ apply plugin: 'kotlin' apply plugin: 'idea' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.cordformation' apply plugin: 'maven-publish' diff --git a/samples/trader-demo/build.gradle b/samples/trader-demo/build.gradle index 5d6f861e8a..5d0a57663e 100644 --- a/samples/trader-demo/build.gradle +++ b/samples/trader-demo/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'kotlin' apply plugin: 'idea' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.cordformation' apply plugin: 'maven-publish' From d1db35c344d193b8352601ee879cfc2999cb7ffb Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Tue, 10 Oct 2017 09:25:45 +0100 Subject: [PATCH 138/185] Removes unnecessary @JVMOverloads annotation. --- .../kotlin/net/corda/core/transactions/TransactionBuilder.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 847bdfc31b..b0149dce8d 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -125,7 +125,6 @@ open class TransactionBuilder( return this } - @JvmOverloads fun addOutputState(state: TransactionState<*>): TransactionBuilder { outputs.add(state) return this From 22b1dead325f10df495e54d777e84c51951174ed Mon Sep 17 00:00:00 2001 From: Alberto Arri <30873160+al-r3@users.noreply.github.com> Date: Tue, 10 Oct 2017 09:32:43 +0100 Subject: [PATCH 139/185] Remove functions with spaces in their name (#1850) Remove all the Kotlin functions with spaces in them since the Android doesn't support them. See https://github.com/corda/corda/issues/1730 for a more in-depth discussion. --- .../docs/tutorial/testdsl/TutorialTestDSL.kt | 15 ++++++------ .../net/corda/finance/contracts/asset/Cash.kt | 22 ++++------------- .../contracts/asset/CommodityContract.kt | 2 +- .../finance/contracts/CommercialPaperTests.kt | 10 ++++---- .../finance/contracts/asset/CashTests.kt | 24 +++++++++---------- .../contracts/asset/ObligationTests.kt | 14 +++++------ .../node/messaging/TwoPartyTradeFlowTests.kt | 13 +++++----- .../corda/irs/api/NodeInterestRatesTest.kt | 5 ++-- 8 files changed, 44 insertions(+), 61 deletions(-) diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt index a52961b0e0..dd3fd47ff1 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt @@ -8,8 +8,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.finance.contracts.asset.`issued by` -import net.corda.finance.contracts.asset.`owned by` +import net.corda.finance.contracts.asset.ownedBy import net.corda.testing.* import org.junit.Test @@ -132,7 +131,7 @@ class CommercialPaperTest { ledger { unverifiedTransaction { attachments(Cash.PROGRAM_ID) - output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE) + output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy ALICE) } // Some CP is issued onto the ledger by MegaCorp. @@ -148,7 +147,7 @@ class CommercialPaperTest { transaction("Trade") { input("paper") input("alice's $900") - output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP } + output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP } output(CP_PROGRAM_ID, "alice's paper") { "paper".output().withOwner(ALICE) } command(ALICE_PUBKEY) { Cash.Commands.Move() } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } @@ -165,7 +164,7 @@ class CommercialPaperTest { ledger { unverifiedTransaction { attachments(Cash.PROGRAM_ID) - output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE) + output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy ALICE) } // Some CP is issued onto the ledger by MegaCorp. @@ -180,7 +179,7 @@ class CommercialPaperTest { transaction("Trade") { input("paper") input("alice's $900") - output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP } + output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP } output(CP_PROGRAM_ID, "alice's paper") { "paper".output().withOwner(ALICE) } command(ALICE_PUBKEY) { Cash.Commands.Move() } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } @@ -207,7 +206,7 @@ class CommercialPaperTest { ledger { unverifiedTransaction { attachments(Cash.PROGRAM_ID) - output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE) + output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy ALICE) } // Some CP is issued onto the ledger by MegaCorp. @@ -222,7 +221,7 @@ class CommercialPaperTest { transaction("Trade") { input("paper") input("alice's $900") - output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP } + output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP } output(CP_PROGRAM_ID, "alice's paper") { "paper".output().withOwner(ALICE) } command(ALICE_PUBKEY) { Cash.Commands.Move() } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() } diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt index 13e1725c09..55dffe1d36 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt @@ -129,10 +129,10 @@ class Cash : OnLedgerAsset() { override fun toString() = "${Emoji.bagOfCash}Cash($amount at ${amount.token.issuer} owned by $owner)" override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Commands.Move(), copy(owner = newOwner)) - fun ownedBy(owner: AbstractParty) = copy(owner = owner) - fun issuedBy(party: AbstractParty) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = amount.token.issuer.copy(party = party)))) - fun issuedBy(deposit: PartyAndReference) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = deposit))) - fun withDeposit(deposit: PartyAndReference): Cash.State = copy(amount = amount.copy(token = amount.token.copy(issuer = deposit))) + infix fun ownedBy(owner: AbstractParty) = copy(owner = owner) + infix fun issuedBy(party: AbstractParty) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = amount.token.issuer.copy(party = party)))) + infix fun issuedBy(deposit: PartyAndReference) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = deposit))) + infix fun withDeposit(deposit: PartyAndReference): Cash.State = copy(amount = amount.copy(token = amount.token.copy(issuer = deposit))) /** Object Relational Mapping support. */ override fun generateMappedObject(schema: MappedSchema): PersistentState { @@ -399,20 +399,6 @@ class Cash : OnLedgerAsset() { } } -// Small DSL extensions. - -/** @suppress */ -infix fun Cash.State.`owned by`(owner: AbstractParty) = ownedBy(owner) - -/** @suppress */ -infix fun Cash.State.`issued by`(party: AbstractParty) = issuedBy(party) - -/** @suppress */ -infix fun Cash.State.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) - -/** @suppress */ -infix fun Cash.State.`with deposit`(deposit: PartyAndReference): Cash.State = withDeposit(deposit) - // Unit testing helpers. These could go in a separate file but it's hardly worth it for just a few functions. /** A randomly generated key. */ diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/CommodityContract.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/CommodityContract.kt index 052223b7e4..677728874b 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/CommodityContract.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/CommodityContract.kt @@ -139,7 +139,7 @@ class CommodityContract : OnLedgerAsset() requireThat { - "output deposits are owned by a command signer" using (issuer.party in issueCommand.signingParties) + "output deposits are ownedBy a command signer" using (issuer.party in issueCommand.signingParties) "output values sum to more than the inputs" using (outputAmount > inputAmount) "there is only a single issue command" using (commodityCommands.count() == 1) } 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 a1a296eb05..6175bca1b7 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt @@ -96,8 +96,8 @@ class CommercialPaperTestsGeneric { ledger { unverifiedTransaction { attachment(Cash.PROGRAM_ID) - output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE) - output(Cash.PROGRAM_ID, "some profits", someProfits.STATE `owned by` MEGA_CORP) + output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy ALICE) + output(Cash.PROGRAM_ID, "some profits", someProfits.STATE ownedBy MEGA_CORP) } // Some CP is issued onto the ledger by MegaCorp. @@ -115,7 +115,7 @@ class CommercialPaperTestsGeneric { attachments(Cash.PROGRAM_ID, JavaCommercialPaper.JCP_PROGRAM_ID) input("paper") input("alice's $900") - output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP } + output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP } output(thisTest.getContract(), "alice's paper") { "paper".output().withOwner(ALICE) } command(ALICE_PUBKEY) { Cash.Commands.Move() } command(MEGA_CORP_PUBKEY) { thisTest.getMoveCommand() } @@ -130,8 +130,8 @@ class CommercialPaperTestsGeneric { input("some profits") fun TransactionDSL.outputs(aliceGetsBack: Amount>) { - output(Cash.PROGRAM_ID, "Alice's profit") { aliceGetsBack.STATE `owned by` ALICE } - output(Cash.PROGRAM_ID, "Change") { (someProfits - aliceGetsBack).STATE `owned by` MEGA_CORP } + output(Cash.PROGRAM_ID, "Alice's profit") { aliceGetsBack.STATE ownedBy ALICE } + output(Cash.PROGRAM_ID, "Change") { (someProfits - aliceGetsBack).STATE ownedBy MEGA_CORP } } command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt index 952aa3d9df..fa7eb3c051 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt @@ -105,7 +105,7 @@ class CashTests : TestDependencyInjectionBase() { } tweak { output(Cash.PROGRAM_ID) { outState } - output(Cash.PROGRAM_ID) { outState `issued by` MINI_CORP } + output(Cash.PROGRAM_ID) { outState issuedBy MINI_CORP } command(ALICE_PUBKEY) { Cash.Commands.Move() } this `fails with` "at least one cash input" } @@ -309,7 +309,7 @@ class CashTests : TestDependencyInjectionBase() { transaction { attachment(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID) { inState } - output(Cash.PROGRAM_ID) { outState `issued by` MINI_CORP } + output(Cash.PROGRAM_ID) { outState issuedBy MINI_CORP } command(ALICE_PUBKEY) { Cash.Commands.Move() } this `fails with` "the amounts balance" } @@ -348,7 +348,7 @@ class CashTests : TestDependencyInjectionBase() { transaction { attachment(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID) { inState } - input(Cash.PROGRAM_ID) { inState `issued by` MINI_CORP } + input(Cash.PROGRAM_ID) { inState issuedBy MINI_CORP } output(Cash.PROGRAM_ID) { outState } command(ALICE_PUBKEY) { Cash.Commands.Move() } this `fails with` "the amounts balance" @@ -396,9 +396,9 @@ class CashTests : TestDependencyInjectionBase() { transaction { attachment(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID) { issuerInState } - input(Cash.PROGRAM_ID) { issuerInState.copy(owner = MINI_CORP) `issued by` MINI_CORP } + input(Cash.PROGRAM_ID) { issuerInState.copy(owner = MINI_CORP) issuedBy MINI_CORP } - output(Cash.PROGRAM_ID) { issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) `issued by` MINI_CORP } + output(Cash.PROGRAM_ID) { issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) issuedBy MINI_CORP } output(Cash.PROGRAM_ID) { issuerInState.copy(owner = MINI_CORP, amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) } command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { Cash.Commands.Move() } @@ -432,7 +432,7 @@ class CashTests : TestDependencyInjectionBase() { attachment(Cash.PROGRAM_ID) // Gather 2000 dollars from two different issuers. input(Cash.PROGRAM_ID) { inState } - input(Cash.PROGRAM_ID) { inState `issued by` MINI_CORP } + input(Cash.PROGRAM_ID) { inState issuedBy MINI_CORP } command(ALICE_PUBKEY) { Cash.Commands.Move() } // Can't merge them together. @@ -449,7 +449,7 @@ class CashTests : TestDependencyInjectionBase() { // This works. output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) } - output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) `issued by` MINI_CORP } + output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) issuedBy MINI_CORP } this.verifies() } } @@ -460,10 +460,10 @@ class CashTests : TestDependencyInjectionBase() { transaction { attachment(Cash.PROGRAM_ID) val pounds = Cash.State(658.POUNDS `issued by` MINI_CORP.ref(3, 4, 5), AnonymousParty(BOB_PUBKEY)) - input(Cash.PROGRAM_ID) { inState `owned by` AnonymousParty(ALICE_PUBKEY) } + input(Cash.PROGRAM_ID) { inState ownedBy AnonymousParty(ALICE_PUBKEY) } input(Cash.PROGRAM_ID) { pounds } - output(Cash.PROGRAM_ID) { inState `owned by` AnonymousParty(BOB_PUBKEY) } - output(Cash.PROGRAM_ID) { pounds `owned by` AnonymousParty(ALICE_PUBKEY) } + output(Cash.PROGRAM_ID) { inState ownedBy AnonymousParty(BOB_PUBKEY) } + output(Cash.PROGRAM_ID) { pounds ownedBy AnonymousParty(ALICE_PUBKEY) } command(ALICE_PUBKEY, BOB_PUBKEY) { Cash.Commands.Move() } this.verifies() @@ -718,8 +718,8 @@ class CashTests : TestDependencyInjectionBase() { Cash.State(1000.POUNDS `issued by` MINI_CORP.ref(3), MEGA_CORP).amount.token) // States cannot be aggregated if the reference differs - assertNotEquals(fiveThousandDollarsFromMega.amount.token, (fiveThousandDollarsFromMega `with deposit` defaultIssuer).amount.token) - assertNotEquals((fiveThousandDollarsFromMega `with deposit` defaultIssuer).amount.token, fiveThousandDollarsFromMega.amount.token) + assertNotEquals(fiveThousandDollarsFromMega.amount.token, (fiveThousandDollarsFromMega withDeposit defaultIssuer).amount.token) + assertNotEquals((fiveThousandDollarsFromMega withDeposit defaultIssuer).amount.token, fiveThousandDollarsFromMega.amount.token) } @Test diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt index 8a293c4009..d5aa6d8cdc 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt @@ -60,7 +60,7 @@ class ObligationTests { output(Obligation.PROGRAM_ID, "Alice's $1,000,000 obligation to Bob", oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)) output(Obligation.PROGRAM_ID, "Bob's $1,000,000 obligation to Alice", oneMillionDollars.OBLIGATION between Pair(BOB, ALICE)) output(Obligation.PROGRAM_ID, "MegaCorp's $1,000,000 obligation to Bob", oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, BOB)) - output(Obligation.PROGRAM_ID, "Alice's $1,000,000", 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE) + output(Obligation.PROGRAM_ID, "Alice's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy ALICE) } } @@ -484,7 +484,7 @@ class ObligationTests { attachments(Obligation.PROGRAM_ID) input("Alice's $1,000,000 obligation to Bob") input("Alice's $1,000,000") - output(Obligation.PROGRAM_ID, "Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB } + output(Obligation.PROGRAM_ID, "Bob's $1,000,000") { 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB } command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)) } command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) } attachment(attachment(cashContractBytes.inputStream())) @@ -498,9 +498,9 @@ class ObligationTests { transaction("Settlement") { attachments(Obligation.PROGRAM_ID, Cash.PROGRAM_ID) input(Obligation.PROGRAM_ID, oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)) - input(Cash.PROGRAM_ID, 500000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE) + input(Cash.PROGRAM_ID, 500000.DOLLARS.CASH issuedBy defaultIssuer ownedBy ALICE) output(Obligation.PROGRAM_ID, "Alice's $500,000 obligation to Bob") { halfAMillionDollars.OBLIGATION between Pair(ALICE, BOB) } - output(Obligation.PROGRAM_ID, "Bob's $500,000") { 500000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB } + output(Obligation.PROGRAM_ID, "Bob's $500,000") { 500000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB } command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)) } command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) } attachment(attachment(cashContractBytes.inputStream())) @@ -514,8 +514,8 @@ class ObligationTests { transaction("Settlement") { attachments(Obligation.PROGRAM_ID, Cash.PROGRAM_ID) input(Obligation.PROGRAM_ID, defaultedObligation) // Alice's defaulted $1,000,000 obligation to Bob - input(Cash.PROGRAM_ID, 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE) - output(Obligation.PROGRAM_ID, "Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB } + input(Cash.PROGRAM_ID, 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy ALICE) + output(Obligation.PROGRAM_ID, "Bob's $1,000,000") { 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB } command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)) } command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) } this `fails with` "all inputs are in the normal state" @@ -529,7 +529,7 @@ class ObligationTests { attachments(Obligation.PROGRAM_ID) input("Alice's $1,000,000 obligation to Bob") input("Alice's $1,000,000") - output(Obligation.PROGRAM_ID, "Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB } + output(Obligation.PROGRAM_ID, "Bob's $1,000,000") { 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB } command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)) } command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) } attachment(attachment(cashContractBytes.inputStream())) diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index ffaafa6246..79e224fe76 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -31,8 +31,7 @@ import net.corda.finance.`issued by` import net.corda.finance.contracts.CommercialPaper import net.corda.finance.contracts.asset.CASH import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.`issued by` -import net.corda.finance.contracts.asset.`owned by` +import net.corda.finance.contracts.asset.ownedBy import net.corda.finance.flows.TwoPartyTradeFlow.Buyer import net.corda.finance.flows.TwoPartyTradeFlow.Seller import net.corda.node.internal.StartedNode @@ -679,8 +678,8 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { // wants to sell to Bob. val eb1 = transaction(transactionBuilder = TransactionBuilder(notary = notary)) { // Issued money to itself. - output(Cash.PROGRAM_ID, "elbonian money 1", notary = notary) { 800.DOLLARS.CASH `issued by` issuer `owned by` interimOwner } - output(Cash.PROGRAM_ID, "elbonian money 2", notary = notary) { 1000.DOLLARS.CASH `issued by` issuer `owned by` interimOwner } + output(Cash.PROGRAM_ID, "elbonian money 1", notary = notary) { 800.DOLLARS.CASH issuedBy issuer ownedBy interimOwner } + output(Cash.PROGRAM_ID, "elbonian money 2", notary = notary) { 1000.DOLLARS.CASH issuedBy issuer ownedBy interimOwner } if (!withError) { command(issuer.party.owningKey) { Cash.Commands.Issue() } } else { @@ -698,15 +697,15 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { // Bob gets some cash onto the ledger from BoE val bc1 = transaction(transactionBuilder = TransactionBuilder(notary = notary)) { input("elbonian money 1") - output(Cash.PROGRAM_ID, "bob cash 1", notary = notary) { 800.DOLLARS.CASH `issued by` issuer `owned by` owner } + output(Cash.PROGRAM_ID, "bob cash 1", notary = notary) { 800.DOLLARS.CASH issuedBy issuer ownedBy owner } command(interimOwner.owningKey) { Cash.Commands.Move() } this.verifies() } val bc2 = transaction(transactionBuilder = TransactionBuilder(notary = notary)) { input("elbonian money 2") - output(Cash.PROGRAM_ID, "bob cash 2", notary = notary) { 300.DOLLARS.CASH `issued by` issuer `owned by` owner } - output(Cash.PROGRAM_ID, notary = notary) { 700.DOLLARS.CASH `issued by` issuer `owned by` interimOwner } // Change output. + output(Cash.PROGRAM_ID, "bob cash 2", notary = notary) { 300.DOLLARS.CASH issuedBy issuer ownedBy owner } + output(Cash.PROGRAM_ID, notary = notary) { 700.DOLLARS.CASH issuedBy issuer ownedBy interimOwner } // Change output. command(interimOwner.owningKey) { Cash.Commands.Move() } this.verifies() } diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index ef07a9a5a3..38fc4e0b56 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -14,8 +14,7 @@ import net.corda.finance.contracts.Fix import net.corda.finance.contracts.FixOf import net.corda.finance.contracts.asset.CASH import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.`issued by` -import net.corda.finance.contracts.asset.`owned by` +import net.corda.finance.contracts.asset.ownedBy import net.corda.irs.flows.RatesFixFlow import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase @@ -242,7 +241,7 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { } private fun makePartialTX() = TransactionBuilder(DUMMY_NOTARY).withItems( - TransactionState(1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE, Cash.PROGRAM_ID, DUMMY_NOTARY)) + TransactionState(1000.DOLLARS.CASH issuedBy DUMMY_CASH_ISSUER ownedBy ALICE, Cash.PROGRAM_ID, DUMMY_NOTARY)) private fun makeFullTx() = makePartialTX().withItems(dummyCommand()) } From 6166fa8358583adcbe2a62f3e9046e06660f3c8e Mon Sep 17 00:00:00 2001 From: cburlinchon <31621751+cburlinchon@users.noreply.github.com> Date: Tue, 10 Oct 2017 09:51:01 +0100 Subject: [PATCH 140/185] Don't generate Kt classes (#1798) --- core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt | 2 ++ .../kotlin/net/corda/core/serialization/SerializationAPI.kt | 2 ++ core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt | 2 ++ 3 files changed, 6 insertions(+) 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 3996dee1de..5f26ef2e48 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -1,3 +1,5 @@ +@file:JvmName("InternalUtils") + package net.corda.core.internal import net.corda.core.crypto.SecureHash 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 f77e3ae122..0eccaf6ffb 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt @@ -1,3 +1,5 @@ +@file:JvmName("SerializationAPI") + package net.corda.core.serialization import net.corda.core.crypto.SecureHash diff --git a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt index b12ea8353d..897ae4d1d8 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt @@ -1,3 +1,5 @@ +@file:JvmName("KotlinUtils") + package net.corda.core.utilities import net.corda.core.internal.concurrent.get From 7af1f02a2d885f6648fb3ed3942a7bef6780a68e Mon Sep 17 00:00:00 2001 From: Alberto Arri <30873160+al-r3@users.noreply.github.com> Date: Tue, 10 Oct 2017 09:55:20 +0100 Subject: [PATCH 141/185] Add a configuration option to set how often should a node check for new nodeinfos on disk (#1851) * Add a configuration option to set how often should a node check for new NodeInfo files in additional-node-infos --- .../node/services/network/NodeInfoWatcherTest.kt | 2 +- .../corda/node/services/config/NodeConfiguration.kt | 5 ++++- .../corda/node/services/network/NodeInfoWatcher.kt | 12 ++++++++++-- .../services/network/PersistentNetworkMapCache.kt | 4 +++- .../services/config/FullNodeConfigurationTest.kt | 4 +++- 5 files changed, 21 insertions(+), 6 deletions(-) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt index d4436236ef..0f102a5e79 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt @@ -50,7 +50,7 @@ class NodeInfoWatcherTest : NodeBasedTest() { fun start() { val identityService = InMemoryIdentityService(trustRoot = DEV_TRUST_ROOT) keyManagementService = MockKeyManagementService(identityService, ALICE_KEY) - nodeInfoWatcher = NodeInfoWatcher(folder.root.toPath(), scheduler) + nodeInfoWatcher = NodeInfoWatcher(folder.root.toPath(), scheduler = scheduler) nodeInfoPath = folder.root.toPath() / CordformNode.NODE_INFO_DIRECTORY } 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 95467b63bc..64b460b2ba 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 @@ -2,6 +2,7 @@ package net.corda.node.services.config import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.seconds import net.corda.node.internal.NetworkMapInfo import net.corda.node.services.messaging.CertificateChainCheckPolicy import net.corda.nodeapi.User @@ -33,6 +34,7 @@ interface NodeConfiguration : NodeSSLConfiguration { val messageRedeliveryDelaySeconds: Int val notary: NotaryConfig? val activeMQServer: ActiveMqServerConfiguration + val additionalNodeInfoPollingFrequencyMsec: Long } data class NotaryConfig(val validating: Boolean, val raft: RaftConfig? = null, val bftSMaRt: BFTSMaRtConfiguration? = null) { @@ -86,7 +88,8 @@ data class FullNodeConfiguration( override val devMode: Boolean = false, val useTestClock: Boolean = false, val detectPublicIp: Boolean = true, - override val activeMQServer: ActiveMqServerConfiguration + override val activeMQServer: ActiveMqServerConfiguration, + override val additionalNodeInfoPollingFrequencyMsec: Long = 5.seconds.toMillis() ) : NodeConfiguration { override val exportJMXto: String get() = "http" 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 7d8542d314..aefd79c2d1 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 @@ -8,10 +8,10 @@ import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.seconds import rx.Observable import rx.Scheduler import rx.schedulers.Schedulers -import java.nio.file.Files import java.nio.file.Path import java.util.concurrent.TimeUnit import kotlin.streams.toList @@ -22,13 +22,17 @@ import kotlin.streams.toList * - Poll a directory for new serialized [NodeInfo] * * @param path the base path of a node. + * @param pollFrequencyMsec how often to poll the filesystem in milliseconds. Any value smaller than 5 seconds will + * be treated as 5 seconds. * @param scheduler a [Scheduler] for the rx [Observable] returned by [nodeInfoUpdates], this is mainly useful for * testing. It defaults to the io scheduler which is the appropriate value for production uses. */ class NodeInfoWatcher(private val nodePath: Path, + pollFrequencyMsec: Long = 5.seconds.toMillis(), private val scheduler: Scheduler = Schedulers.io()) { private val nodeInfoDirectory = nodePath / CordformNode.NODE_INFO_DIRECTORY + private val pollFrequencyMsec: Long companion object { private val logger = loggerFor() @@ -56,6 +60,10 @@ class NodeInfoWatcher(private val nodePath: Path, } } + init { + this.pollFrequencyMsec = maxOf(pollFrequencyMsec, 5.seconds.toMillis()) + } + /** * Read all the files contained in [nodePath] / [CordformNode.NODE_INFO_DIRECTORY] and keep watching * the folder for further updates. @@ -67,7 +75,7 @@ class NodeInfoWatcher(private val nodePath: Path, * than once. */ fun nodeInfoUpdates(): Observable { - return Observable.interval(5, TimeUnit.SECONDS, scheduler) + return Observable.interval(pollFrequencyMsec, TimeUnit.MILLISECONDS, scheduler) .flatMapIterable { loadFromDirectory() } } 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 09b3c8997f..1f24e27526 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 @@ -39,6 +39,7 @@ import rx.Observable import rx.subjects.PublishSubject import java.security.PublicKey import java.security.SignatureException +import java.time.Duration import java.util.* import javax.annotation.concurrent.ThreadSafe import kotlin.collections.HashMap @@ -87,7 +88,8 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) .sortedBy { it.name.toString() } } - private val nodeInfoSerializer = NodeInfoWatcher(serviceHub.configuration.baseDirectory) + private val nodeInfoSerializer = NodeInfoWatcher(serviceHub.configuration.baseDirectory, + serviceHub.configuration.additionalNodeInfoPollingFrequencyMsec) init { loadFromFiles() diff --git a/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt index 2935053699..d46c32c5a0 100644 --- a/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt @@ -1,6 +1,7 @@ package net.corda.node.services.config import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.seconds import net.corda.nodeapi.User import net.corda.testing.ALICE import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties @@ -33,7 +34,8 @@ class FullNodeConfigurationTest { notary = null, certificateChainCheckPolicies = emptyList(), devMode = true, - activeMQServer = ActiveMqServerConfiguration(BridgeConfiguration(0, 0, 0.0))) + activeMQServer = ActiveMqServerConfiguration(BridgeConfiguration(0, 0, 0.0)), + additionalNodeInfoPollingFrequencyMsec = 5.seconds.toMillis()) fun configWithRPCUsername(username: String) { testConfiguration.copy(rpcUsers = listOf(User(username, "pass", emptySet()))) From 0e47e53b609a46a0f360b87a6d667db3bc7d6156 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Tue, 10 Oct 2017 12:28:19 +0100 Subject: [PATCH 142/185] Make AttachmentLoadingTests more stable (#1810) --- .../node/services/AttachmentLoadingTests.kt | 64 +++++++++++-------- 1 file changed, 39 insertions(+), 25 deletions(-) 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 e5a79d7d5c..3c15d0aec5 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 @@ -16,6 +16,7 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.seconds import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.nodeapi.User @@ -25,6 +26,7 @@ import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.driver.DriverDSLExposedInterface import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver +import net.corda.testing.eventually import net.corda.testing.node.MockServices import org.junit.Assert.assertEquals import org.junit.Before @@ -53,6 +55,37 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() { val flowInitiatorClass = Class.forName("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator", true, URLClassLoader(arrayOf(isolatedJAR))) .asSubclass(FlowLogic::class.java) + + private fun DriverDSLExposedInterface.createTwoNodesAndNotary(): List { + val adminUser = User("admin", "admin", permissions = setOf("ALL")) + val nodes = listOf( + startNode(providedName = bankAName, rpcUsers = listOf(adminUser)), + startNode(providedName = bankBName, rpcUsers = listOf(adminUser)), + startNotaryNode(providedName = notaryName, rpcUsers = listOf(adminUser), validating = false) + ).transpose().getOrThrow() // Wait for all nodes to start up. + nodes.forEach { it.rpc.waitUntilNetworkReady().getOrThrow() } + return nodes + } + + private fun DriverDSLExposedInterface.installIsolatedCordappTo(nodeName: CordaX500Name) { + // Copy the app jar to the first node. The second won't have it. + val path = (baseDirectory(nodeName.toString()) / "plugins").createDirectories() / "isolated.jar" + logger.info("Installing isolated jar to $path") + isolatedJAR.openStream().buffered().use { input -> + Files.newOutputStream(path).buffered().use { output -> + input.copyTo(output) + } + } + } + + // Due to cluster instability after nodes been started it may take some time to all the nodes to become available + // *and* discover each other to reliably communicate. Hence, eventual nature of the test. + // TODO: Remove this method and usages of it once NetworkMap service been re-worked + private fun eventuallyPassingTest(block: () -> Unit) { + eventually(30.seconds) { + block() + } + } } private lateinit var services: Services @@ -85,9 +118,10 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() { driver(initialiseSerialization = false) { installIsolatedCordappTo(bankAName) val (bankA, bankB, _) = createTwoNodesAndNotary() - - assertFailsWith("Party C=CH,L=Zurich,O=BankB rejected session request: Don't know net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator") { - bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow() + eventuallyPassingTest { + assertFailsWith("Party C=CH,L=Zurich,O=BankB rejected session request: Don't know net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator") { + bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow() + } } } } @@ -98,29 +132,9 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() { installIsolatedCordappTo(bankAName) installIsolatedCordappTo(bankBName) val (bankA, bankB, _) = createTwoNodesAndNotary() - bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow() - } - } - - private fun DriverDSLExposedInterface.installIsolatedCordappTo(nodeName: CordaX500Name) { - // Copy the app jar to the first node. The second won't have it. - val path = (baseDirectory(nodeName.toString()) / "plugins").createDirectories() / "isolated.jar" - logger.info("Installing isolated jar to $path") - isolatedJAR.openStream().buffered().use { input -> - Files.newOutputStream(path).buffered().use { output -> - input.copyTo(output) + eventuallyPassingTest { + bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow() } } } - - private fun DriverDSLExposedInterface.createTwoNodesAndNotary(): List { - val adminUser = User("admin", "admin", permissions = setOf("ALL")) - val nodes = listOf( - startNode(providedName = bankAName, rpcUsers = listOf(adminUser)), - startNode(providedName = bankBName, rpcUsers = listOf(adminUser)), - startNotaryNode(providedName = notaryName, rpcUsers = listOf(adminUser), validating = false) - ).transpose().getOrThrow() // Wait for all nodes to start up. - nodes.forEach { it.rpc.waitUntilNetworkReady().getOrThrow() } - return nodes - } } From 242b019dc2ce31f000a3e67193ef7a46cb9aa2fc Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Tue, 10 Oct 2017 13:23:31 +0100 Subject: [PATCH 143/185] CORDA-641: Remove special case handling of notary transactions (#1675) Move special case handling of notary transactions into `SignedTransaction` --- .../flows/AbstractStateReplacementFlow.kt | 22 +++------------- .../kotlin/net/corda/core/flows/NotaryFlow.kt | 6 +---- .../kotlin/net/corda/core/node/ServiceHub.kt | 6 +---- .../core/transactions/SignedTransaction.kt | 26 +++++++++++++++++++ .../transactions/TransactionWithSignatures.kt | 2 ++ .../net/corda/docs/CustomNotaryTutorial.kt | 12 ++++----- .../net/corda/node/internal/StartedNode.kt | 4 +-- .../transactions/ValidatingNotaryFlow.kt | 12 ++++----- 8 files changed, 44 insertions(+), 46 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt b/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt index 82a960cfc7..af0c406410 100644 --- a/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt @@ -70,15 +70,7 @@ abstract class AbstractStateReplacementFlow { val finalTx = stx + signatures serviceHub.recordTransactions(finalTx) - val newOutput = run { - if (stx.isNotaryChangeTransaction()) { - stx.resolveNotaryChangeTransaction(serviceHub).outRef(0) - } else { - stx.tx.outRef(0) - } - } - - return newOutput + return stx.resolveBaseTransaction(serviceHub).outRef(0) } /** @@ -174,11 +166,7 @@ abstract class AbstractStateReplacementFlow { } val finalTx = stx + allSignatures - if (finalTx.isNotaryChangeTransaction()) { - finalTx.resolveNotaryChangeTransaction(serviceHub).verifyRequiredSignatures() - } else { - finalTx.verifyRequiredSignatures() - } + finalTx.resolveTransactionWithSignatures(serviceHub).verifyRequiredSignatures() serviceHub.recordTransactions(finalTx) } @@ -195,11 +183,7 @@ abstract class AbstractStateReplacementFlow { // TODO Check the set of multiple identities? val myKey = ourIdentity.owningKey - val requiredKeys = if (stx.isNotaryChangeTransaction()) { - stx.resolveNotaryChangeTransaction(serviceHub).requiredSigningKeys - } else { - stx.tx.requiredSigningKeys - } + val requiredKeys = stx.resolveTransactionWithSignatures(serviceHub).requiredSigningKeys require(myKey in requiredKeys) { "Party is not a participant for any of the input states of transaction ${stx.id}" } } diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt index c59986c61b..c96070fba7 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt @@ -54,11 +54,7 @@ class NotaryFlow { } try { - if (stx.isNotaryChangeTransaction()) { - stx.resolveNotaryChangeTransaction(serviceHub).verifySignaturesExcept(notaryParty.owningKey) - } else { - stx.verifySignaturesExcept(notaryParty.owningKey) - } + stx.resolveTransactionWithSignatures(serviceHub).verifySignaturesExcept(notaryParty.owningKey) } catch (ex: SignatureException) { throw NotaryException(NotaryError.TransactionInvalid(ex)) } diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index c278ac16a3..7114e6c843 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -164,11 +164,7 @@ interface ServiceHub : ServicesForResolution { @Throws(TransactionResolutionException::class) fun toStateAndRef(stateRef: StateRef): StateAndRef { val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) - return if (stx.isNotaryChangeTransaction()) { - stx.resolveNotaryChangeTransaction(this).outRef(stateRef.index) - } else { - stx.tx.outRef(stateRef.index) - } + return stx.resolveBaseTransaction(this).outRef(stateRef.index) } private val legalIdentityKey: PublicKey get() = this.myInfo.legalIdentitiesAndCerts.first().owningKey diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt index d583a5f386..c34fd26065 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -179,6 +179,32 @@ data class SignedTransaction(val txBits: SerializedBytes, fun isNotaryChangeTransaction() = transaction is NotaryChangeWireTransaction + /** + * Resolves the underlying base transaction and then returns it, handling any special case transactions such as + * [NotaryChangeWireTransaction]. + */ + fun resolveBaseTransaction(services: StateLoader): BaseTransaction { + return when (transaction) { + is NotaryChangeWireTransaction -> resolveNotaryChangeTransaction(services) + is WireTransaction -> this.tx + is FilteredTransaction -> throw IllegalStateException("Persistence of filtered transactions is not supported.") + else -> throw IllegalStateException("Unknown transaction type ${transaction::class.qualifiedName}") + } + } + + /** + * Resolves the underlying transaction with signatures and then returns it, handling any special case transactions + * such as [NotaryChangeWireTransaction]. + */ + fun resolveTransactionWithSignatures(services: ServiceHub): TransactionWithSignatures { + return when (transaction) { + is NotaryChangeWireTransaction -> resolveNotaryChangeTransaction(services) + is WireTransaction -> this + is FilteredTransaction -> throw IllegalStateException("Persistence of filtered transactions is not supported.") + else -> throw IllegalStateException("Unknown transaction type ${transaction::class.qualifiedName}") + } + } + /** * If [transaction] is a [NotaryChangeWireTransaction], loads the input states and resolves it to a * [NotaryChangeLedgerTransaction] so the signatures can be verified. diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt index ab25d68064..1909168198 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt @@ -1,6 +1,8 @@ package net.corda.core.transactions +import net.corda.core.contracts.ContractState import net.corda.core.contracts.NamedByHash +import net.corda.core.contracts.TransactionState import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.isFulfilledBy import net.corda.core.transactions.SignedTransaction.SignaturesMissingException diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt index bf1e504bf3..8d1c0d0274 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt @@ -38,13 +38,11 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false)) val notary = stx.notary checkNotary(notary) - var timeWindow: TimeWindow? = null - val transactionWithSignatures = if (stx.isNotaryChangeTransaction()) { - stx.resolveNotaryChangeTransaction(serviceHub) - } else { - timeWindow = stx.tx.timeWindow - stx - } + val timeWindow: TimeWindow? = if (stx.isNotaryChangeTransaction()) + null + else + stx.tx.timeWindow + val transactionWithSignatures = stx.resolveTransactionWithSignatures(serviceHub) checkSignatures(transactionWithSignatures) return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!) } catch (e: Exception) { diff --git a/node/src/main/kotlin/net/corda/node/internal/StartedNode.kt b/node/src/main/kotlin/net/corda/node/internal/StartedNode.kt index 501b8f926a..e0320d9c62 100644 --- a/node/src/main/kotlin/net/corda/node/internal/StartedNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/StartedNode.kt @@ -35,8 +35,6 @@ class StateLoaderImpl(private val validatedTransactions: TransactionStorage) : S @Throws(TransactionResolutionException::class) override fun loadState(stateRef: StateRef): TransactionState<*> { val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) - return if (stx.isNotaryChangeTransaction()) { - stx.resolveNotaryChangeTransaction(this).outputs[stateRef.index] - } else stx.tx.outputs[stateRef.index] + return stx.resolveBaseTransaction(this).outputs[stateRef.index] } } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt index 2cee47aa89..8ce7ba6365 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt @@ -25,13 +25,11 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false)) val notary = stx.notary checkNotary(notary) - var timeWindow: TimeWindow? = null - val transactionWithSignatures = if (stx.isNotaryChangeTransaction()) { - stx.resolveNotaryChangeTransaction(serviceHub) - } else { - timeWindow = stx.tx.timeWindow - stx - } + val timeWindow: TimeWindow? = if (stx.isNotaryChangeTransaction()) + null + else + stx.tx.timeWindow + val transactionWithSignatures = stx.resolveTransactionWithSignatures(serviceHub) checkSignatures(transactionWithSignatures) return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!) } catch (e: Exception) { From 34dbbe626bbea8bb1e8500b8f3c120ea6c58a69c Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Tue, 10 Oct 2017 13:46:25 +0100 Subject: [PATCH 144/185] Add confidential identity support to cash exit logic (#1849) generateExit() previously required all states to have a single owner, which was used as the change output's owner. This deprecates that function but enables it to work as expected using `firstOrNull()` instead of `singleOrNull()`, and adds new functions which take in the change output's owner. --- .../finance/contracts/asset/OnLedgerAsset.kt | 65 +++++++++++++++++-- .../finance/contracts/asset/CashTests.kt | 36 ++++++---- 2 files changed, 80 insertions(+), 21 deletions(-) diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/OnLedgerAsset.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/OnLedgerAsset.kt index 9f93a80a8c..d64bb817b2 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/OnLedgerAsset.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/OnLedgerAsset.kt @@ -230,12 +230,36 @@ abstract class OnLedgerAsset> : C */ @Throws(InsufficientBalanceException::class) @JvmStatic - fun , T : Any> generateExit(tx: TransactionBuilder, amountIssued: Amount>, - assetStates: List>, - deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, - generateMoveCommand: () -> CommandData, - generateExitCommand: (Amount>) -> CommandData): Set { - val owner = assetStates.map { it.state.data.owner }.toSet().singleOrNull() ?: throw InsufficientBalanceException(amountIssued) + @Deprecated("Replaced with generateExit() which takes in a party to pay change to") + fun , T: Any> generateExit(tx: TransactionBuilder, amountIssued: Amount>, + assetStates: List>, + deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, + generateMoveCommand: () -> CommandData, + generateExitCommand: (Amount>) -> CommandData): Set { + val owner = assetStates.map { it.state.data.owner }.toSet().firstOrNull() ?: throw InsufficientBalanceException(amountIssued) + return generateExit(tx, amountIssued, assetStates, owner, deriveState, generateMoveCommand, generateExitCommand) + } + + /** + * Generate an transaction exiting fungible assets from the ledger. + * + * @param tx transaction builder to add states and commands to. + * @param amountIssued the amount to be exited, represented as a quantity of issued currency. + * @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is + * the responsibility of the caller to check that they do not attempt to exit funds held by others. + * @param payChangeTo party to pay any change to; this is normally a confidential identity of the calling + * party. + * @return the public keys which must sign the transaction for it to be valid. + */ + @Throws(InsufficientBalanceException::class) + @JvmStatic + fun , T: Any> generateExit(tx: TransactionBuilder, amountIssued: Amount>, + assetStates: List>, + payChangeTo: AbstractParty, + deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, + generateMoveCommand: () -> CommandData, + generateExitCommand: (Amount>) -> CommandData): Set { + require(assetStates.isNotEmpty()) { "List of states to exit cannot be empty." } val currency = amountIssued.token.product val amount = Amount(amountIssued.quantity, currency) var acceptableCoins = assetStates.filter { ref -> ref.state.data.amount.token == amountIssued.token } @@ -255,7 +279,7 @@ abstract class OnLedgerAsset> : C val outputs = if (change != null) { // Add a change output and adjust the last output downwards. - listOf(deriveState(gathered.last().state, change, owner)) + listOf(deriveState(gathered.last().state, change, payChangeTo)) } else emptyList() for (state in gathered) tx.addInputState(state) @@ -295,9 +319,12 @@ abstract class OnLedgerAsset> : C * @param amountIssued the amount to be exited, represented as a quantity of issued currency. * @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is * the responsibility of the caller to check that they do not exit funds held by others. + * @param payChangeTo party to pay any change to; this is normally a confidential identity of the calling + * party. * @return the public keys which must sign the transaction for it to be valid. */ @Throws(InsufficientBalanceException::class) + @Deprecated("Replaced with generateExit() which takes in a party to pay change to") fun generateExit(tx: TransactionBuilder, amountIssued: Amount>, assetStates: List>): Set { return generateExit( @@ -310,6 +337,30 @@ abstract class OnLedgerAsset> : C ) } + /** + * Generate an transaction exiting assets from the ledger. + * + * @param tx transaction builder to add states and commands to. + * @param amountIssued the amount to be exited, represented as a quantity of issued currency. + * @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is + * the responsibility of the caller to check that they do not exit funds held by others. + * @return the public keys which must sign the transaction for it to be valid. + */ + @Throws(InsufficientBalanceException::class) + fun generateExit(tx: TransactionBuilder, amountIssued: Amount>, + assetStates: List>, + payChangeTo: AbstractParty): Set { + return generateExit( + tx, + amountIssued, + assetStates, + payChangeTo, + deriveState = { state, amount, owner -> deriveState(state, amount, owner) }, + generateMoveCommand = { -> generateMoveCommand() }, + generateExitCommand = { amount -> generateExitCommand(amount) } + ) + } + abstract fun generateExitCommand(amount: Amount>): CommandData abstract fun generateMoveCommand(): MoveCommand diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt index fa7eb3c051..537f347bd6 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt @@ -6,6 +6,7 @@ import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub import net.corda.core.node.services.VaultService import net.corda.core.node.services.queryBy import net.corda.core.transactions.TransactionBuilder @@ -480,9 +481,9 @@ class CashTests : TestDependencyInjectionBase() { val THEIR_IDENTITY_1 = AnonymousParty(MINI_CORP_PUBKEY) val THEIR_IDENTITY_2 = AnonymousParty(CHARLIE_PUBKEY) - fun makeCash(amount: Amount, corp: Party, depositRef: Byte = 1) = + fun makeCash(amount: Amount, issuer: AbstractParty, depositRef: Byte = 1) = StateAndRef( - TransactionState(Cash.State(amount `issued by` corp.ref(depositRef), OUR_IDENTITY_1), Cash.PROGRAM_ID, DUMMY_NOTARY), + TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), OUR_IDENTITY_1), Cash.PROGRAM_ID, DUMMY_NOTARY), StateRef(SecureHash.randomSHA256(), Random().nextInt(32)) ) @@ -496,10 +497,11 @@ class CashTests : TestDependencyInjectionBase() { /** * Generate an exit transaction, removing some amount of cash from the ledger. */ - private fun makeExit(amount: Amount, corp: Party, depositRef: Byte = 1): WireTransaction { + private fun makeExit(serviceHub: ServiceHub, amount: Amount, issuer: Party, depositRef: Byte = 1): WireTransaction { val tx = TransactionBuilder(DUMMY_NOTARY) - Cash().generateExit(tx, Amount(amount.quantity, Issued(corp.ref(depositRef), amount.token)), WALLET) - return tx.toWireTransaction(miniCorpServices) + val payChangeTo = serviceHub.keyManagementService.freshKeyAndCert(MINI_CORP_IDENTITY, false).party + Cash().generateExit(tx, Amount(amount.quantity, Issued(issuer.ref(depositRef), amount.token)), WALLET, payChangeTo) + return tx.toWireTransaction(serviceHub) } private fun makeSpend(amount: Amount, dest: AbstractParty): WireTransaction { @@ -516,7 +518,7 @@ class CashTests : TestDependencyInjectionBase() { @Test fun generateSimpleExit() { initialiseTestSerialization() - val wtx = makeExit(100.DOLLARS, MEGA_CORP, 1) + val wtx = makeExit(miniCorpServices, 100.DOLLARS, MEGA_CORP, 1) assertEquals(WALLET[0].ref, wtx.inputs[0]) assertEquals(0, wtx.outputs.size) @@ -532,10 +534,16 @@ class CashTests : TestDependencyInjectionBase() { @Test fun generatePartialExit() { initialiseTestSerialization() - val wtx = makeExit(50.DOLLARS, MEGA_CORP, 1) - assertEquals(WALLET[0].ref, wtx.inputs[0]) - assertEquals(1, wtx.outputs.size) - assertEquals(WALLET[0].state.data.copy(amount = WALLET[0].state.data.amount.splitEvenly(2).first()), wtx.getOutput(0)) + val wtx = makeExit(miniCorpServices, 50.DOLLARS, MEGA_CORP, 1) + val actualInput = wtx.inputs.single() + // Filter the available inputs and confirm exactly one has been used + val expectedInputs = WALLET.filter { it.ref == actualInput } + assertEquals(1, expectedInputs.size) + val inputState = expectedInputs.single() + val actualChange = wtx.outputs.single().data as Cash.State + val expectedChangeAmount = (inputState.state.data as Cash.State).amount.quantity - 50.DOLLARS.quantity + val expectedChange = WALLET[0].state.data.copy(amount = WALLET[0].state.data.amount.copy(quantity = expectedChangeAmount), owner = actualChange.owner) + assertEquals(expectedChange, wtx.getOutput(0)) } /** @@ -544,7 +552,7 @@ class CashTests : TestDependencyInjectionBase() { @Test fun generateAbsentExit() { initialiseTestSerialization() - assertFailsWith { makeExit(100.POUNDS, MEGA_CORP, 1) } + assertFailsWith { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 1) } } /** @@ -553,7 +561,7 @@ class CashTests : TestDependencyInjectionBase() { @Test fun generateInvalidReferenceExit() { initialiseTestSerialization() - assertFailsWith { makeExit(100.POUNDS, MEGA_CORP, 2) } + assertFailsWith { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 2) } } /** @@ -562,7 +570,7 @@ class CashTests : TestDependencyInjectionBase() { @Test fun generateInsufficientExit() { initialiseTestSerialization() - assertFailsWith { makeExit(1000.DOLLARS, MEGA_CORP, 1) } + assertFailsWith { makeExit(miniCorpServices, 1000.DOLLARS, MEGA_CORP, 1) } } /** @@ -571,7 +579,7 @@ class CashTests : TestDependencyInjectionBase() { @Test fun generateOwnerWithNoStatesExit() { initialiseTestSerialization() - assertFailsWith { makeExit(100.POUNDS, CHARLIE, 1) } + assertFailsWith { makeExit(miniCorpServices, 100.POUNDS, CHARLIE, 1) } } /** From bd53a22efaf4d7a05d260e6fd859a2be28952198 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Mon, 9 Oct 2017 18:57:09 +0100 Subject: [PATCH 145/185] Removed extraAdvertisedServiceIds config The remaining use for it was the finance CorDapp for permissioning CCY issuers. Instead this is now taken from a custom config in node.conf. --- .idea/compiler.xml | 1 + build.gradle | 3 - config/dev/generalnodea.conf | 1 - config/dev/generalnodeb.conf | 1 - constants.properties | 2 +- .../net/corda/core/flows/AttachmentTests.kt | 5 +- .../AttachmentSerializationTest.kt | 5 +- docs/source/deploying-a-node.rst | 4 - docs/source/example-code/build.gradle | 2 - .../resources/example-network-map-node.conf | 1 - .../src/main/resources/example-node.conf | 1 - docs/source/hello-world-running.rst | 3 - docs/source/node-services.rst | 8 +- finance/build.gradle | 20 ++ .../finance/flows/CashConfigDataFlowTest.kt | 20 ++ .../corda/finance/flows/CashConfigDataFlow.kt | 53 +++-- .../AbstractStateReplacementFlowTest.java | 22 -- .../java/net/corda/cordform/CordformNode.java | 7 +- .../main/groovy/net/corda/plugins/Node.groovy | 4 +- .../corda/nodeapi/config/ConfigUtilities.kt | 3 +- .../corda/nodeapi/config/ConfigParsingTest.kt | 11 + .../kotlin/net/corda/node/BootTests.kt | 10 +- .../net/corda/node/internal/AbstractNode.kt | 2 - .../kotlin/net/corda/node/internal/Node.kt | 6 +- .../net/corda/node/internal/NodeStartup.kt | 8 +- .../node/services/config/NodeConfiguration.kt | 9 - .../statemachine/FlowStateMachineImpl.kt | 17 +- .../node/messaging/TwoPartyTradeFlowTests.kt | 26 +-- .../config/FullNodeConfigurationTest.kt | 1 - .../network/AbstractNetworkMapServiceTest.kt | 5 +- .../PersistentNetworkMapServiceTest.kt | 6 +- .../statemachine/FlowFrameworkTests.kt | 10 +- samples/attachment-demo/build.gradle | 3 - samples/bank-of-corda-demo/build.gradle | 4 +- samples/irs-demo/build.gradle | 3 - .../net/corda/netmap/simulation/Simulation.kt | 29 +-- samples/simm-valuation-demo/build.gradle | 4 - samples/trader-demo/build.gradle | 4 - .../kotlin/net/corda/testing/driver/Driver.kt | 10 +- .../testing/internal/demorun/CordformUtils.kt | 5 - .../kotlin/net/corda/testing/node/MockNode.kt | 29 +-- .../net/corda/testing/node/NodeBasedTest.kt | 12 +- tools/demobench/README.md | 3 - tools/demobench/build.gradle | 1 + .../net/corda/demobench/explorer/Explorer.kt | 32 +-- .../corda/demobench/model/InstallFactory.kt | 64 ++--- .../corda/demobench/model/NetworkMapConfig.kt | 10 - .../net/corda/demobench/model/NodeConfig.kt | 124 +++++----- .../corda/demobench/model/NodeController.kt | 113 +++++---- .../net/corda/demobench/model/NodeData.kt | 10 +- .../demobench/model/ServiceController.kt | 44 ---- .../kotlin/net/corda/demobench/model/User.kt | 15 -- .../demobench/plugin/PluginController.kt | 10 +- .../demobench/profile/ProfileController.kt | 6 +- .../kotlin/net/corda/demobench/rpc/NodeRPC.kt | 11 +- .../net/corda/demobench/views/NodeTabView.kt | 31 ++- .../corda/demobench/views/NodeTerminalView.kt | 29 +-- .../net/corda/demobench/web/WebServer.kt | 19 +- .../src/main/resources/services.conf | 6 - .../net/corda/demobench/LoggingTestSuite.kt | 2 - .../demobench/model/NetworkMapConfigTest.kt | 22 -- .../corda/demobench/model/NodeConfigTest.kt | 221 +++--------------- .../demobench/model/NodeControllerTest.kt | 57 +++-- .../demobench/model/ServiceControllerTest.kt | 44 ---- .../net/corda/demobench/model/UserTest.kt | 47 ---- .../test/resources/duplicate-services.conf | 3 - .../src/test/resources/empty-services.conf | 0 .../src/test/resources/notary-services.conf | 2 - .../net/corda/explorer/ExplorerSimulation.kt | 14 +- .../corda/explorer/model/IssuerModelTest.kt | 27 --- 70 files changed, 441 insertions(+), 906 deletions(-) create mode 100644 finance/src/integration-test/kotlin/net/corda/finance/flows/CashConfigDataFlowTest.kt delete mode 100644 finance/src/test/java/net/corda/finance/flows/AbstractStateReplacementFlowTest.java delete mode 100644 tools/demobench/src/main/kotlin/net/corda/demobench/model/NetworkMapConfig.kt delete mode 100644 tools/demobench/src/main/kotlin/net/corda/demobench/model/ServiceController.kt delete mode 100644 tools/demobench/src/main/kotlin/net/corda/demobench/model/User.kt delete mode 100644 tools/demobench/src/main/resources/services.conf delete mode 100644 tools/demobench/src/test/kotlin/net/corda/demobench/model/NetworkMapConfigTest.kt delete mode 100644 tools/demobench/src/test/kotlin/net/corda/demobench/model/ServiceControllerTest.kt delete mode 100644 tools/demobench/src/test/kotlin/net/corda/demobench/model/UserTest.kt delete mode 100644 tools/demobench/src/test/resources/duplicate-services.conf delete mode 100644 tools/demobench/src/test/resources/empty-services.conf delete mode 100644 tools/demobench/src/test/resources/notary-services.conf delete mode 100644 tools/explorer/src/test/kotlin/net/corda/explorer/model/IssuerModelTest.kt diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 3516efcb93..5df761b918 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -43,6 +43,7 @@ + diff --git a/build.gradle b/build.gradle index 9b19ced795..f8a21b7789 100644 --- a/build.gradle +++ b/build.gradle @@ -235,13 +235,11 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { node { name "O=Controller,OU=corda,L=London,C=GB" notary = [validating : true] - advertisedServices = [] p2pPort 10002 cordapps = [] } node { name "O=Bank A,OU=corda,L=London,C=GB" - advertisedServices = [] p2pPort 10012 rpcPort 10013 webPort 10014 @@ -249,7 +247,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "O=Bank B,OU=corda,L=London,C=GB" - advertisedServices = [] p2pPort 10007 rpcPort 10008 webPort 10009 diff --git a/config/dev/generalnodea.conf b/config/dev/generalnodea.conf index 16eb90f526..0089d1cb24 100644 --- a/config/dev/generalnodea.conf +++ b/config/dev/generalnodea.conf @@ -4,7 +4,6 @@ trustStorePassword : "trustpass" p2pAddress : "localhost:10002" rpcAddress : "localhost:10003" webAddress : "localhost:10004" -extraAdvertisedServiceIds : [ "corda.interest_rates" ] networkMapService : { address : "localhost:10000" legalName : "O=Network Map Service,OU=corda,L=London,C=GB" diff --git a/config/dev/generalnodeb.conf b/config/dev/generalnodeb.conf index 1eb839aece..af4e26cc27 100644 --- a/config/dev/generalnodeb.conf +++ b/config/dev/generalnodeb.conf @@ -4,7 +4,6 @@ trustStorePassword : "trustpass" p2pAddress : "localhost:10005" rpcAddress : "localhost:10006" webAddress : "localhost:10007" -extraAdvertisedServiceIds : [ "corda.interest_rates" ] networkMapService : { address : "localhost:10000" legalName : "O=Network Map Service,OU=corda,L=London,C=GB" diff --git a/constants.properties b/constants.properties index 165810dcd2..4591e03dea 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=2.0.0 +gradlePluginsVersion=2.0.1 kotlinVersion=1.1.50 guavaVersion=21.0 bouncycastleVersion=1.57 diff --git a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt index 034c28c834..6a2bf1ef8b 100644 --- a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt @@ -118,10 +118,9 @@ class AttachmentTests { // Make a node that doesn't do sanity checking at load time. val aliceNode = mockNet.createNotaryNode(legalName = ALICE.name, nodeFactory = object : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, - notaryIdentity: Pair?, + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): MockNetwork.MockNode { - return object : MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, id, notaryIdentity, entropyRoot) { + return object : MockNetwork.MockNode(config, network, networkMapAddr, id, notaryIdentity, entropyRoot) { override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false } } } diff --git a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt index 4b14705669..89bc81dad9 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt @@ -162,9 +162,8 @@ class AttachmentSerializationTest { client.dispose() client = mockNet.createNode(client.internals.id, object : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, notaryIdentity: Pair?, - entropyRoot: BigInteger): MockNetwork.MockNode { - return object : MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, id, notaryIdentity, entropyRoot) { + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): MockNetwork.MockNode { + return object : MockNetwork.MockNode(config, network, networkMapAddr, id, notaryIdentity, entropyRoot) { override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = checkAttachmentsOnLoad } } } diff --git a/docs/source/deploying-a-node.rst b/docs/source/deploying-a-node.rst index ec15b75d8b..4457b7d38c 100644 --- a/docs/source/deploying-a-node.rst +++ b/docs/source/deploying-a-node.rst @@ -21,7 +21,6 @@ notary/network map node: node { name "O=Controller,OU=corda,L=London,C=UK" notary = [validating : true] - advertisedServices = [] p2pPort 10002 rpcPort 10003 webPort 10004 @@ -29,7 +28,6 @@ notary/network map node: } node { name "CN=NodeA,O=NodeA,L=London,C=UK" - advertisedServices = [] p2pPort 10005 rpcPort 10006 webPort 10007 @@ -38,7 +36,6 @@ notary/network map node: } node { name "CN=NodeB,O=NodeB,L=New York,C=US" - advertisedServices = [] p2pPort 10008 rpcPort 10009 webPort 10010 @@ -47,7 +44,6 @@ notary/network map node: } node { name "CN=NodeC,O=NodeC,L=Paris,C=FR" - advertisedServices = [] p2pPort 10011 rpcPort 10012 webPort 10013 diff --git a/docs/source/example-code/build.gradle b/docs/source/example-code/build.gradle index ae0b57fc34..a3ea77f4d8 100644 --- a/docs/source/example-code/build.gradle +++ b/docs/source/example-code/build.gradle @@ -77,7 +77,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { node { name "O=Notary Service,OU=corda,L=London,C=GB" notary = [validating : true] - advertisedServices = [] p2pPort 10002 rpcPort 10003 webPort 10004 @@ -85,7 +84,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "O=Alice Corp,L=London,C=GB" - advertisedServices = [] p2pPort 10005 rpcPort 10006 webPort 10007 diff --git a/docs/source/example-code/src/main/resources/example-network-map-node.conf b/docs/source/example-code/src/main/resources/example-network-map-node.conf index fe00d9aa38..e75807bbc7 100644 --- a/docs/source/example-code/src/main/resources/example-network-map-node.conf +++ b/docs/source/example-code/src/main/resources/example-network-map-node.conf @@ -4,5 +4,4 @@ trustStorePassword : "trustpass" p2pAddress : "my-network-map:10000" webAddress : "localhost:10001" sshdAddress : "localhost:10002" -extraAdvertisedServiceIds : [] useHTTPS : false diff --git a/docs/source/example-code/src/main/resources/example-node.conf b/docs/source/example-code/src/main/resources/example-node.conf index d12273c18a..c9bad1eb5b 100644 --- a/docs/source/example-code/src/main/resources/example-node.conf +++ b/docs/source/example-code/src/main/resources/example-node.conf @@ -10,7 +10,6 @@ dataSourceProperties : { p2pAddress : "my-corda-node:10002" rpcAddress : "my-corda-node:10003" webAddress : "localhost:10004" -extraAdvertisedServiceIds : [ "corda.interest_rates" ] networkMapService : { address : "my-network-map:10000" legalName : "O=Network Map Service,OU=corda,L=London,C=GB" diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index ae91fe0907..ae1592fa82 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -29,14 +29,12 @@ Let's take a look at the nodes we're going to deploy. Open the project's ``build node { name "O=Controller,L=London,C=GB" notary = [validating : true] - advertisedServices = [] p2pPort 10002 rpcPort 10003 cordapps = ["net.corda:corda-finance:$corda_release_version"] } node { name "O=PartyA,L=London,C=GB" - advertisedServices = [] p2pPort 10005 rpcPort 10006 webPort 10007 @@ -45,7 +43,6 @@ Let's take a look at the nodes we're going to deploy. Open the project's ``build } node { name "O=PartyB,L=New York,C=US" - advertisedServices = [] p2pPort 10008 rpcPort 10009 webPort 10010 diff --git a/docs/source/node-services.rst b/docs/source/node-services.rst index 8e25eb6115..3ab92255b6 100644 --- a/docs/source/node-services.rst +++ b/docs/source/node-services.rst @@ -295,12 +295,8 @@ NotaryService (SimpleNotaryService, ValidatingNotaryService, RaftValidatingNotar The ``NotaryService`` is an abstract base class for the various concrete implementations of the Notary server flow. By default, a node does -not run any ``NotaryService`` server component. However, the appropriate -implementation service is automatically started if the relevant -``ServiceType`` id is included in the node's -``extraAdvertisedServiceIds`` configuration property. The node will then -advertise itself as a Notary via the ``NetworkMapService`` and may then -participate in controlling state uniqueness when contacted by nodes +not run any ``NotaryService`` server component. For that you need to specify the ``notary`` config. +The node may then participate in controlling state uniqueness when contacted by nodes using the ``NotaryFlow.Client`` ``subFlow``. The ``SimpleNotaryService`` only offers protection against double spend, but does no further verification. The ``ValidatingNotaryService`` checks diff --git a/finance/build.gradle b/finance/build.gradle index 3386d94b40..c642969785 100644 --- a/finance/build.gradle +++ b/finance/build.gradle @@ -10,12 +10,25 @@ apply plugin: 'com.jfrog.artifactory' description 'Corda finance modules' +sourceSets { + integrationTest { + kotlin { + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + srcDir file('src/integration-test/kotlin') + } + } +} + dependencies { // Note the :finance module is a CorDapp in its own right // and CorDapps using :finance features should use 'cordapp' not 'compile' linkage. cordaCompile project(':core') cordaCompile project(':confidential-identities') + // TODO Remove this once we have app configs + compile "com.typesafe:config:$typesafe_config_version" + testCompile project(':test-utils') testCompile project(path: ':core', configuration: 'testArtifacts') testCompile "junit:junit:$junit_version" @@ -23,6 +36,8 @@ dependencies { configurations { testArtifacts.extendsFrom testRuntime + integrationTestCompile.extendsFrom testCompile + integrationTestRuntime.extendsFrom testRuntime } task testJar(type: Jar) { @@ -30,6 +45,11 @@ task testJar(type: Jar) { from sourceSets.test.output } +task integrationTest(type: Test, dependsOn: []) { + testClassesDirs = sourceSets.integrationTest.output.classesDirs + classpath = sourceSets.integrationTest.runtimeClasspath +} + artifacts { testArtifacts testJar } diff --git a/finance/src/integration-test/kotlin/net/corda/finance/flows/CashConfigDataFlowTest.kt b/finance/src/integration-test/kotlin/net/corda/finance/flows/CashConfigDataFlowTest.kt new file mode 100644 index 0000000000..49351f1e13 --- /dev/null +++ b/finance/src/integration-test/kotlin/net/corda/finance/flows/CashConfigDataFlowTest.kt @@ -0,0 +1,20 @@ +package net.corda.finance.flows + +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.getOrThrow +import net.corda.finance.EUR +import net.corda.finance.USD +import net.corda.testing.driver.driver +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class CashConfigDataFlowTest { + @Test + fun `issuable currencies are read in from node config`() { + driver { + val node = startNode(customOverrides = mapOf("issuableCurrencies" to listOf("EUR", "USD"))).getOrThrow() + val config = node.rpc.startFlow(::CashConfigDataFlow).returnValue.getOrThrow() + assertThat(config.issuableCurrencies).containsExactly(EUR, USD) + } + } +} \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt index 59d50403ca..c202d5633c 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt @@ -1,38 +1,59 @@ package net.corda.finance.flows import co.paralleluniverse.fibers.Suspendable -import net.corda.core.flows.FlowException +import com.typesafe.config.ConfigFactory import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC +import net.corda.core.internal.declaredField +import net.corda.core.internal.div +import net.corda.core.internal.read +import net.corda.core.node.AppServiceHub +import net.corda.core.node.services.CordaService import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.finance.CHF import net.corda.finance.EUR import net.corda.finance.GBP import net.corda.finance.USD +import net.corda.finance.flows.ConfigHolder.Companion.supportedCurrencies +import java.nio.file.Path import java.util.* +// TODO Until apps have access to their own config, we'll hack things by first getting the baseDirectory, read the node.conf +// again to get our config and store it here for access by our flow +@CordaService +class ConfigHolder(services: AppServiceHub) : SingletonSerializeAsToken() { + companion object { + val supportedCurrencies = listOf(USD, GBP, CHF, EUR) + } + + val issuableCurrencies: List + + init { + // Warning!! You are about to see a major hack! + val baseDirectory = services.declaredField("serviceHub").value + .let { it.javaClass.getMethod("getConfiguration").apply { isAccessible = true }.invoke(it) } + .declaredField("baseDirectory").value + val config = (baseDirectory / "node.conf").read { ConfigFactory.parseReader(it.reader()) } + if (config.hasPath("issuableCurrencies")) { + issuableCurrencies = config.getStringList("issuableCurrencies").map { Currency.getInstance(it) } + require(supportedCurrencies.containsAll(issuableCurrencies)) + } else { + issuableCurrencies = emptyList() + } + } +} + + /** * Flow to obtain cash cordapp app configuration. */ @StartableByRPC class CashConfigDataFlow : FlowLogic() { - companion object { - private val supportedCurrencies = listOf(USD, GBP, CHF, EUR) - } - @Suspendable override fun call(): CashConfiguration { - val issuableCurrencies = supportedCurrencies.mapNotNull { - try { - // Currently it uses checkFlowPermission to determine the list of issuable currency as a temporary hack. - // TODO: get the config from proper configuration source. - checkFlowPermission("corda.issuer.$it", emptyMap()) - it - } catch (e: FlowException) { - null - } - } - return CashConfiguration(issuableCurrencies, supportedCurrencies) + val configHolder = serviceHub.cordaService(ConfigHolder::class.java) + return CashConfiguration(configHolder.issuableCurrencies, supportedCurrencies) } } diff --git a/finance/src/test/java/net/corda/finance/flows/AbstractStateReplacementFlowTest.java b/finance/src/test/java/net/corda/finance/flows/AbstractStateReplacementFlowTest.java deleted file mode 100644 index b753141ac5..0000000000 --- a/finance/src/test/java/net/corda/finance/flows/AbstractStateReplacementFlowTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package net.corda.finance.flows; - -import net.corda.core.flows.AbstractStateReplacementFlow; -import net.corda.core.flows.FlowSession; -import net.corda.core.transactions.SignedTransaction; -import net.corda.core.utilities.ProgressTracker; -import org.jetbrains.annotations.NotNull; - -@SuppressWarnings("unused") -public class AbstractStateReplacementFlowTest { - - // Acceptor used to have a type parameter of Unit which prevented Java code from subclassing it (https://youtrack.jetbrains.com/issue/KT-15964). - private static class TestAcceptorCanBeInheritedInJava extends AbstractStateReplacementFlow.Acceptor { - public TestAcceptorCanBeInheritedInJava(@NotNull FlowSession otherSideSession, @NotNull ProgressTracker progressTracker) { - super(otherSideSession, progressTracker); - } - - @Override - protected void verifyProposal(@NotNull SignedTransaction stx, @NotNull AbstractStateReplacementFlow.Proposal proposal) { - } - } -} diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java index 7ecaa9fce9..bca75347d8 100644 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java +++ b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java @@ -23,11 +23,6 @@ public class CordformNode implements NodeDefinition { return name; } - /** - * A list of advertised services ID strings. - */ - public List advertisedServices = emptyList(); - /** * Set the RPC users for this node. This configuration block allows arbitrary configuration. * The recommended current structure is: @@ -44,6 +39,8 @@ public class CordformNode implements NodeDefinition { */ public Map notary = null; + public Map extraConfig = null; + protected Config config = ConfigFactory.empty(); public Config getConfig() { diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy index 262f4df38c..f4169bf456 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy @@ -123,7 +123,9 @@ class Node extends CordformNode { if (notary) { config = config.withValue("notary", ConfigValueFactory.fromMap(notary)) } - config = config.withValue('extraAdvertisedServiceIds', ConfigValueFactory.fromIterable(advertisedServices*.toString())) + if (extraConfig) { + config = config.withFallback(ConfigFactory.parseMap(extraConfig)) + } } /** diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt index a67a55db37..cd1118928c 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt @@ -12,6 +12,7 @@ import net.corda.core.internal.uncheckedCast import net.corda.core.utilities.NetworkHostAndPort import org.slf4j.LoggerFactory import java.lang.reflect.Field +import java.lang.reflect.Modifier.isStatic import java.lang.reflect.ParameterizedType import java.net.Proxy import java.net.URL @@ -142,7 +143,7 @@ fun Any.toConfig(): Config = ConfigValueFactory.fromMap(toConfigMap()).toConfig( private fun Any.toConfigMap(): Map { val values = HashMap() for (field in javaClass.declaredFields) { - if (field.isSynthetic) continue + if (isStatic(field.modifiers) || field.isSynthetic) continue field.isAccessible = true val value = field.get(this) ?: continue val configValue = if (value is String || value is Boolean || value is Number) { diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt index 8737ac9160..452e25ce78 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt @@ -181,6 +181,11 @@ class ConfigParsingTest { assertThat(OldData("old").toConfig()).isEqualTo(config("newValue" to "old")) } + @Test + fun `static field`() { + assertThat(DataWithCompanion(3).toConfig()).isEqualTo(config("value" to 3)) + } + private inline fun , reified L : ListData, V : Any> testPropertyType( value1: V, value2: V, @@ -259,6 +264,12 @@ class ConfigParsingTest { data class OldData( @OldConfig("oldValue") val newValue: String) + data class DataWithCompanion(val value: Int) { + companion object { + @Suppress("unused") + val companionValue = 2 + } + } enum class TestEnum { Value1, Value2 } } \ No newline at end of file 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 4b5c17a069..525790ea22 100644 --- a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt @@ -1,20 +1,20 @@ package net.corda.node import co.paralleluniverse.fibers.Suspendable -import net.corda.core.internal.div import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC +import net.corda.core.internal.div import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow -import net.corda.testing.ALICE import net.corda.node.internal.NodeStartup import net.corda.node.services.FlowPermissions.Companion.startFlowPermission +import net.corda.nodeapi.User import net.corda.nodeapi.internal.ServiceInfo import net.corda.nodeapi.internal.ServiceType -import net.corda.nodeapi.User +import net.corda.testing.ALICE +import net.corda.testing.ProjectStructure.projectRootDir import net.corda.testing.driver.ListenProcessDeathException import net.corda.testing.driver.NetworkMapStartStrategy -import net.corda.testing.ProjectStructure.projectRootDir import net.corda.testing.driver.driver import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy @@ -60,7 +60,7 @@ class BootTests { fun `node quits on failure to register with network map`() { val tooManyAdvertisedServices = (1..100).map { ServiceInfo(ServiceType.notary.getSubType("$it")) }.toSet() driver(networkMapStartStrategy = NetworkMapStartStrategy.Nominated(ALICE.name)) { - val future = startNode(providedName = ALICE.name, advertisedServices = tooManyAdvertisedServices) + val future = startNode(providedName = ALICE.name) assertFailsWith(ListenProcessDeathException::class) { future.getOrThrow() } } } 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 779604259d..0d92fe6df5 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -70,7 +70,6 @@ import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.VaultSoftLockManager import net.corda.node.utilities.* import net.corda.node.utilities.AddOrRemove.ADD -import net.corda.nodeapi.internal.ServiceInfo import org.apache.activemq.artemis.utils.ReusableLatch import org.slf4j.Logger import rx.Observable @@ -101,7 +100,6 @@ import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair // In theory the NodeInfo for the node should be passed in, instead, however currently this is constructed by the // AbstractNode. It should be possible to generate the NodeInfo outside of AbstractNode, so it can be passed in. abstract class AbstractNode(config: NodeConfiguration, - val advertisedServices: Set, val platformClock: Clock, protected val versionInfo: VersionInfo, @VisibleForTesting val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() { 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 696098df29..84b126980a 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -21,7 +21,6 @@ import net.corda.node.services.RPCUserService import net.corda.node.services.RPCUserServiceImpl import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.api.SchemaService -import net.corda.nodeapi.internal.ServiceInfo import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.node.services.messaging.ArtemisMessagingServer.Companion.ipDetectRequestProperty @@ -60,14 +59,11 @@ import kotlin.system.exitProcess * loads important data off disk and starts listening for connections. * * @param configuration This is typically loaded from a TypeSafe HOCON configuration file. - * @param advertisedServices The services this node advertises. This must be a subset of the services it runs, - * but nodes are not required to advertise services they run (hence subset). */ open class Node(override val configuration: FullNodeConfiguration, - advertisedServices: Set, versionInfo: VersionInfo, val initialiseSerialization: Boolean = true -) : AbstractNode(configuration, advertisedServices, createClock(configuration), versionInfo) { +) : AbstractNode(configuration, createClock(configuration), versionInfo) { companion object { private val logger = loggerFor() var renderBasicInfoToConsole = true 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 2294024e90..67a06aa2e9 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -12,7 +12,6 @@ import net.corda.node.services.transactions.bftSMaRtSerialFilter import net.corda.node.shell.InteractiveShell import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NetworkRegistrationHelper -import net.corda.nodeapi.internal.ServiceInfo import net.corda.nodeapi.internal.addShutdownHook import org.fusesource.jansi.Ansi import org.fusesource.jansi.AnsiConsole @@ -86,13 +85,10 @@ open class NodeStartup(val args: Array) { open protected fun preNetworkRegistration(conf: FullNodeConfiguration) = Unit - open protected fun createNode(conf: FullNodeConfiguration, versionInfo: VersionInfo, services: Set): Node { - return Node(conf, services, versionInfo) - } + open protected fun createNode(conf: FullNodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo) open protected fun startNode(conf: FullNodeConfiguration, versionInfo: VersionInfo, startTime: Long, cmdlineOptions: CmdLineOptions) { - val advertisedServices = conf.calculateServices() - val node = createNode(conf, versionInfo, advertisedServices) + val node = createNode(conf, versionInfo) if (cmdlineOptions.justGenerateNodeInfo) { // Perform the minimum required start-up logic to be able to write a nodeInfo to disk node.generateNodeInfo() 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 64b460b2ba..a72465ccee 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 @@ -7,7 +7,6 @@ import net.corda.node.internal.NetworkMapInfo import net.corda.node.services.messaging.CertificateChainCheckPolicy import net.corda.nodeapi.User import net.corda.nodeapi.config.NodeSSLConfiguration -import net.corda.nodeapi.internal.ServiceInfo import java.net.URL import java.nio.file.Path import java.util.* @@ -82,7 +81,6 @@ data class FullNodeConfiguration( // TODO This field is slightly redundant as p2pAddress is sufficient to hold the address of the node's MQ broker. // Instead this should be a Boolean indicating whether that broker is an internal one started by the node or an external one val messagingServerAddress: NetworkHostAndPort?, - val extraAdvertisedServiceIds: List, override val notary: NotaryConfig?, override val certificateChainCheckPolicies: List, override val devMode: Boolean = false, @@ -103,13 +101,6 @@ data class FullNodeConfiguration( require(myLegalName.commonName == null) { "Common name must be null: $myLegalName" } require(minimumPlatformVersion >= 1) { "minimumPlatformVersion cannot be less than 1" } } - - fun calculateServices(): Set { - return extraAdvertisedServiceIds - .filter(String::isNotBlank) - .map { ServiceInfo.parse(it) } - .toSet() - } } enum class VerifierType { 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 dea328709a..c61ef66ead 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 @@ -24,7 +24,6 @@ import net.corda.core.utilities.* import net.corda.node.services.api.FlowAppAuditEvent import net.corda.node.services.api.FlowPermissionAuditEvent import net.corda.node.services.api.ServiceHubInternal -import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.statemachine.FlowSessionState.Initiating import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.DatabaseTransaction @@ -262,10 +261,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, // TODO Dummy implementation of access to application specific permission controls and audit logging override fun checkFlowPermission(permissionName: String, extraAuditData: Map) { - // This is a hack to allow cash app access list of permitted issuer currency. - // TODO: replace this with cordapp configuration. - val config = serviceHub.configuration as? FullNodeConfiguration - val permissionGranted = config?.extraAdvertisedServiceIds?.contains(permissionName) != false + val permissionGranted = true // TODO define permission control service on ServiceHubInternal and actually check authorization. val checkPermissionEvent = FlowPermissionAuditEvent( serviceHub.clock.instant(), flowInitiator, @@ -276,6 +272,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, permissionName, permissionGranted) serviceHub.auditService.recordAuditEvent(checkPermissionEvent) + @Suppress("ConstantConditionIf") if (!permissionGranted) { throw FlowPermissionException("User $flowInitiator not permissioned for $permissionName on flow $id") } @@ -398,14 +395,8 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, waitForConfirmation: Boolean, retryable: Boolean = false ): FlowSessionInternal { - val session = openSessions[Pair(sessionFlow, otherParty)] - if (session == null) { - throw IllegalStateException("Expected an Uninitiated session for $otherParty") - } - val state = session.state - if (state !is FlowSessionState.Uninitiated) { - throw IllegalStateException("Tried to initiate a session $session, but it's already initiating/initiated") - } + val session = openSessions[Pair(sessionFlow, otherParty)] ?: throw IllegalStateException("Expected an Uninitiated session for $otherParty") + val state = session.state as? FlowSessionState.Uninitiated ?: throw IllegalStateException("Tried to initiate a session $session, but it's already initiating/initiated") logger.trace { "Initiating a new session with ${state.otherParty}" } session.state = FlowSessionState.Initiating(state.otherParty) session.retryable = retryable diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 79e224fe76..1d19ed5833 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -275,9 +275,8 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { // that Bob was waiting on before the reboot occurred. bobNode = mockNet.createNode(bobAddr.id, object : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, notaryIdentity: Pair?, - entropyRoot: BigInteger): MockNetwork.MockNode { - return MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, bobAddr.id, notaryIdentity, entropyRoot) + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): MockNetwork.MockNode { + return MockNetwork.MockNode(config, network, networkMapAddr, bobAddr.id, notaryIdentity, entropyRoot) } }, BOB.name) @@ -312,18 +311,15 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { // Creates a mock node with an overridden storage service that uses a RecordingMap, that lets us test the order // of gets and puts. - private fun makeNodeWithTracking( - networkMapAddress: SingleMessageRecipient?, - name: CordaX500Name): StartedNode { + private fun makeNodeWithTracking(name: CordaX500Name): StartedNode { // Create a node in the mock network ... return mockNet.createNode(nodeFactory = object : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, - notaryIdentity: Pair?, + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): MockNetwork.MockNode { - return object : MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, id, notaryIdentity, entropyRoot) { + return object : MockNetwork.MockNode(config, network, networkMapAddr, id, notaryIdentity, entropyRoot) { // That constructs a recording tx storage override fun makeTransactionStorage(): WritableTransactionStorage { return RecordingTransactionStorage(database, super.makeTransactionStorage()) @@ -338,9 +334,9 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { mockNet = MockNetwork(false) val notaryNode = mockNet.createNotaryNode() - val aliceNode = makeNodeWithTracking(notaryNode.network.myAddress, ALICE.name) - val bobNode = makeNodeWithTracking(notaryNode.network.myAddress, BOB.name) - val bankNode = makeNodeWithTracking(notaryNode.network.myAddress, BOC.name) + val aliceNode = makeNodeWithTracking(ALICE.name) + val bobNode = makeNodeWithTracking(BOB.name) + val bankNode = makeNodeWithTracking(BOC.name) val issuer = bankNode.info.chooseIdentity().ref(1, 2, 3) mockNet.runNetwork() notaryNode.internals.ensureRegistered() @@ -443,9 +439,9 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { mockNet = MockNetwork(false) val notaryNode = mockNet.createNotaryNode() - val aliceNode = makeNodeWithTracking(notaryNode.network.myAddress, ALICE.name) - val bobNode = makeNodeWithTracking(notaryNode.network.myAddress, BOB.name) - val bankNode = makeNodeWithTracking(notaryNode.network.myAddress, BOC.name) + val aliceNode = makeNodeWithTracking(ALICE.name) + val bobNode = makeNodeWithTracking(BOB.name) + val bankNode = makeNodeWithTracking(BOC.name) val issuer = bankNode.info.chooseIdentity().ref(1, 2, 3) mockNet.runNetwork() diff --git a/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt index d46c32c5a0..bc40165c06 100644 --- a/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt @@ -30,7 +30,6 @@ class FullNodeConfigurationTest { p2pAddress = NetworkHostAndPort("localhost", 0), rpcAddress = NetworkHostAndPort("localhost", 1), messagingServerAddress = null, - extraAdvertisedServiceIds = emptyList(), notary = null, certificateChainCheckPolicies = emptyList(), devMode = true, diff --git a/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt index 5e0f97e770..b3b9a9f769 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt @@ -8,7 +8,6 @@ import net.corda.core.serialization.deserialize import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode import net.corda.node.services.api.NetworkMapCacheInternal -import net.corda.nodeapi.internal.ServiceInfo import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.send @@ -25,6 +24,7 @@ import net.corda.node.services.network.NetworkMapService.Companion.SUBSCRIPTION_ import net.corda.node.utilities.AddOrRemove import net.corda.node.utilities.AddOrRemove.ADD import net.corda.node.utilities.AddOrRemove.REMOVE +import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode @@ -270,11 +270,10 @@ abstract class AbstractNetworkMapServiceTest override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): MockNode { - return object : MockNode(config, network, null, advertisedServices, id, notaryIdentity, entropyRoot) { + return object : MockNode(config, network, null, id, notaryIdentity, entropyRoot) { override fun makeNetworkMapService(network: MessagingService, networkMapCache: NetworkMapCacheInternal) = NullNetworkMapService } } diff --git a/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt index 7e8e6a34a1..612c8e943a 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt @@ -2,12 +2,11 @@ package net.corda.node.services.network import net.corda.core.messaging.SingleMessageRecipient import net.corda.node.services.api.NetworkMapCacheInternal -import net.corda.nodeapi.internal.ServiceInfo import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService +import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode -import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import java.math.BigInteger import java.security.KeyPair @@ -32,11 +31,10 @@ class PersistentNetworkMapServiceTest : AbstractNetworkMapServiceTest, id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): MockNode { - return object : MockNode(config, network, networkMapAddr, advertisedServices, id, notaryIdentity, entropyRoot) { + return object : MockNode(config, network, networkMapAddr, id, notaryIdentity, entropyRoot) { override fun makeNetworkMapService(network: MessagingService, networkMapCache: NetworkMapCacheInternal) = SwizzleNetworkMapService(network, networkMapCache) } } diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 57ca2cebba..00b66b30b9 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -135,7 +135,7 @@ class FlowFrameworkTests { node2.internals.acceptableLiveFiberCountOnStop = 1 node2.dispose() mockNet.runNetwork() - val restoredFlow = node2.restartAndGetRestoredFlow(node1) + val restoredFlow = node2.restartAndGetRestoredFlow() assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello") } @@ -184,7 +184,7 @@ class FlowFrameworkTests { node2.smm.executor.flush() node2.internals.disableDBCloseOnStop() node2.dispose() // kill receiver - val restoredFlow = node2.restartAndGetRestoredFlow(node1) + val restoredFlow = node2.restartAndGetRestoredFlow() assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello") } @@ -214,7 +214,7 @@ class FlowFrameworkTests { assertEquals(1, node2.checkpointStorage.checkpoints().size) // confirm checkpoint node2.services.networkMapCache.clearNetworkMapCache() } - val node2b = mockNet.createNode(node2.internals.id, advertisedServices = *node2.internals.advertisedServices.toTypedArray()) + val node2b = mockNet.createNode(node2.internals.id) node2.internals.manuallyCloseDB() val (firstAgain, fut1) = node2b.getSingleFlow() // Run the network which will also fire up the second flow. First message should get deduped. So message data stays in sync. @@ -685,10 +685,10 @@ class FlowFrameworkTests { //////////////////////////////////////////////////////////////////////////////////////////////////////////// //region Helpers - private inline fun > StartedNode.restartAndGetRestoredFlow(networkMapNode: StartedNode<*>? = null) = internals.run { + private inline fun > StartedNode.restartAndGetRestoredFlow() = internals.run { disableDBCloseOnStop() // Handover DB to new node copy stop() - val newNode = mockNet.createNode(id, advertisedServices = *advertisedServices.toTypedArray()) + val newNode = mockNet.createNode(id) newNode.internals.acceptableLiveFiberCountOnStop = 1 manuallyCloseDB() mockNet.runNetwork() // allow NetworkMapService messages to stabilise and thus start the state machine diff --git a/samples/attachment-demo/build.gradle b/samples/attachment-demo/build.gradle index 5fa296e26d..c331834529 100644 --- a/samples/attachment-demo/build.gradle +++ b/samples/attachment-demo/build.gradle @@ -42,7 +42,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { node { name "O=Notary Service,L=Zurich,C=CH" notary = [validating : true] - advertisedServices = [] p2pPort 10002 rpcPort 10003 cordapps = [] @@ -50,7 +49,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "O=Bank A,L=London,C=GB" - advertisedServices = [] p2pPort 10005 rpcPort 10006 cordapps = [] @@ -58,7 +56,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "O=Bank B,L=New York,C=US" - advertisedServices = [] p2pPort 10008 rpcPort 10009 webPort 10010 diff --git a/samples/bank-of-corda-demo/build.gradle b/samples/bank-of-corda-demo/build.gradle index 043ead8c0b..da04426168 100644 --- a/samples/bank-of-corda-demo/build.gradle +++ b/samples/bank-of-corda-demo/build.gradle @@ -55,14 +55,13 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { node { name "O=Notary Service,L=Zurich,C=CH" notary = [validating : true] - advertisedServices = [] p2pPort 10002 rpcPort 10003 cordapps = ["net.corda:finance:$corda_release_version"] } node { name "O=BankOfCorda,L=London,C=GB" - advertisedServices = ["corda.issuer.USD"] + extraConfig = [issuableCurrencies : ["USD"]] p2pPort 10005 rpcPort 10006 webPort 10007 @@ -79,7 +78,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "O=BigCorporation,L=New York,C=US" - advertisedServices = [] p2pPort 10008 rpcPort 10009 webPort 10010 diff --git a/samples/irs-demo/build.gradle b/samples/irs-demo/build.gradle index 7d1dccfb8d..2341793e1f 100644 --- a/samples/irs-demo/build.gradle +++ b/samples/irs-demo/build.gradle @@ -55,7 +55,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { node { name "O=Notary Service,L=Zurich,C=CH" notary = [validating : true] - advertisedServices = ["corda.interest_rates"] p2pPort 10002 rpcPort 10003 webPort 10004 @@ -64,7 +63,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "O=Bank A,L=London,C=GB" - advertisedServices = [] p2pPort 10005 rpcPort 10006 webPort 10007 @@ -73,7 +71,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "O=Bank B,L=New York,C=US" - advertisedServices = [] p2pPort 10008 rpcPort 10009 webPort 10010 diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt index 74529cd530..6c929c47b7 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt @@ -49,9 +49,9 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, // This puts together a mock network of SimulatedNodes. open class SimulatedNode(config: NodeConfiguration, mockNet: MockNetwork, networkMapAddress: SingleMessageRecipient?, - advertisedServices: Set, id: Int, notaryIdentity: Pair?, + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger) - : MockNetwork.MockNode(config, mockNet, networkMapAddress, advertisedServices, id, notaryIdentity, entropyRoot) { + : MockNetwork.MockNode(config, mockNet, networkMapAddress, id, notaryIdentity, entropyRoot) { override val started: StartedNode? get() = uncheckedCast(super.started) override fun findMyLocation(): WorldMapLocation? { return configuration.myLegalName.locality.let { CityDatabase[it] } @@ -62,15 +62,14 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, var counter = 0 override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, notaryIdentity: Pair?, - entropyRoot: BigInteger): SimulatedNode { + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): SimulatedNode { val letter = 'A' + counter val (city, country) = bankLocations[counter++ % bankLocations.size] val cfg = testNodeConfiguration( baseDirectory = config.baseDirectory, myLegalName = CordaX500Name(organisation = "Bank $letter", locality = city, country = country)) - return SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, notaryIdentity, entropyRoot) + return SimulatedNode(cfg, network, networkMapAddr, id, notaryIdentity, entropyRoot) } fun createAll(): List { @@ -85,25 +84,23 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, object NetworkMapNodeFactory : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, notaryIdentity: Pair?, - entropyRoot: BigInteger): SimulatedNode { + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): SimulatedNode { val cfg = testNodeConfiguration( baseDirectory = config.baseDirectory, myLegalName = DUMMY_MAP.name) - return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, notaryIdentity, entropyRoot) {} + return object : SimulatedNode(cfg, network, networkMapAddr, id, notaryIdentity, entropyRoot) {} } } object NotaryNodeFactory : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, notaryIdentity: Pair?, - entropyRoot: BigInteger): SimulatedNode { + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): SimulatedNode { requireNotNull(config.notary) val cfg = testNodeConfiguration( baseDirectory = config.baseDirectory, myLegalName = DUMMY_NOTARY.name, notaryConfig = config.notary) - return SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, notaryIdentity, entropyRoot) + return SimulatedNode(cfg, network, networkMapAddr, id, notaryIdentity, entropyRoot) } } @@ -112,12 +109,11 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, val RATES_SERVICE_NAME = CordaX500Name(organisation = "Rates Service Provider", locality = "Madrid", country = "ES") override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, notaryIdentity: Pair?, - entropyRoot: BigInteger): SimulatedNode { + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): SimulatedNode { val cfg = testNodeConfiguration( baseDirectory = config.baseDirectory, myLegalName = RATES_SERVICE_NAME) - return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, notaryIdentity, entropyRoot) { + return object : SimulatedNode(cfg, network, networkMapAddr, id, notaryIdentity, entropyRoot) { override fun start() = super.start().apply { registerInitiatedFlow(NodeInterestRates.FixQueryHandler::class.java) registerInitiatedFlow(NodeInterestRates.FixSignHandler::class.java) @@ -133,12 +129,11 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, object RegulatorFactory : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, notaryIdentity: Pair?, - entropyRoot: BigInteger): SimulatedNode { + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): SimulatedNode { val cfg = testNodeConfiguration( baseDirectory = config.baseDirectory, myLegalName = DUMMY_REGULATOR.name) - return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, notaryIdentity, entropyRoot) { + return object : SimulatedNode(cfg, network, networkMapAddr, id, notaryIdentity, entropyRoot) { // TODO: Regulatory nodes don't actually exist properly, this is a last minute demo request. // So we just fire a message at a node that doesn't know how to handle it, and it'll ignore it. // But that's fine for visualisation purposes. diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index ec982a20a7..502b2bdee8 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -68,13 +68,11 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { node { name "O=Notary Service,L=Zurich,C=CH" notary = [validating : true] - advertisedServices = [] p2pPort 10002 cordapps = ["net.corda:finance:$corda_release_version"] } node { name "O=Bank A,L=London,C=GB" - advertisedServices = [] p2pPort 10004 webPort 10005 rpcPort 10006 @@ -83,7 +81,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "O=Bank B,L=New York,C=US" - advertisedServices = [] p2pPort 10007 webPort 10008 rpcPort 10009 @@ -92,7 +89,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "O=Bank C,L=Tokyo,C=JP" - advertisedServices = [] p2pPort 10010 webPort 10011 rpcPort 10012 diff --git a/samples/trader-demo/build.gradle b/samples/trader-demo/build.gradle index 5d0a57663e..8d224d11ab 100644 --- a/samples/trader-demo/build.gradle +++ b/samples/trader-demo/build.gradle @@ -56,13 +56,11 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { node { name "O=Notary Service,L=Zurich,C=CH" notary = [validating : true] - advertisedServices = [] p2pPort 10002 cordapps = ["net.corda:finance:$corda_release_version"] } node { name "O=Bank A,L=London,C=GB" - advertisedServices = [] p2pPort 10005 rpcPort 10006 cordapps = ["net.corda:finance:$corda_release_version"] @@ -70,7 +68,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "O=Bank B,L=New York,C=US" - advertisedServices = [] p2pPort 10008 rpcPort 10009 cordapps = ["net.corda:finance:$corda_release_version"] @@ -78,7 +75,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "O=BankOfCorda,L=New York,C=US" - advertisedServices = [] p2pPort 10011 rpcPort 10012 cordapps = ["net.corda:finance:$corda_release_version"] diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index 9382ce4b3a..9a1ce481ca 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -30,7 +30,6 @@ import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.nodeapi.User import net.corda.nodeapi.config.parseAs import net.corda.nodeapi.config.toConfig -import net.corda.nodeapi.internal.ServiceInfo import net.corda.nodeapi.internal.addShutdownHook import net.corda.testing.* import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO @@ -78,7 +77,6 @@ interface DriverDSLExposedInterface : CordformContext { * when called from Java code. * @param providedName Optional name of the node, which will be its legal name in [Party]. Defaults to something * random. Note that this must be unique as the driver uses it as a primary key! - * @param advertisedServices The set of services to be advertised by the node. Defaults to empty set. * @param verifierType The type of transaction verifier to use. See: [VerifierType] * @param rpcUsers List of users who are authorised to use the RPC system. Defaults to empty list. * @param startInSameProcess Determines if the node should be started inside the same process the Driver is running @@ -88,7 +86,6 @@ interface DriverDSLExposedInterface : CordformContext { fun startNode( defaultParameters: NodeParameters = NodeParameters(), providedName: CordaX500Name? = defaultParameters.providedName, - advertisedServices: Set = defaultParameters.advertisedServices, rpcUsers: List = defaultParameters.rpcUsers, verifierType: VerifierType = defaultParameters.verifierType, customOverrides: Map = defaultParameters.customOverrides, @@ -267,7 +264,6 @@ sealed class PortAllocation { */ data class NodeParameters( val providedName: CordaX500Name? = null, - val advertisedServices: Set = emptySet(), val rpcUsers: List = emptyList(), val verifierType: VerifierType = VerifierType.InMemory, val customOverrides: Map = emptyMap(), @@ -275,7 +271,6 @@ data class NodeParameters( val maximumHeapSize: String = "200m" ) { fun setProvidedName(providedName: CordaX500Name?) = copy(providedName = providedName) - fun setAdvertisedServices(advertisedServices: Set) = copy(advertisedServices = advertisedServices) fun setRpcUsers(rpcUsers: List) = copy(rpcUsers = rpcUsers) fun setVerifierType(verifierType: VerifierType) = copy(verifierType = verifierType) fun setCustomerOverrides(customOverrides: Map) = copy(customOverrides = customOverrides) @@ -686,7 +681,6 @@ class DriverDSL( override fun startNode( defaultParameters: NodeParameters, providedName: CordaX500Name?, - advertisedServices: Set, rpcUsers: List, verifierType: VerifierType, customOverrides: Map, @@ -710,7 +704,6 @@ class DriverDSL( "p2pAddress" to p2pAddress.toString(), "rpcAddress" to rpcAddress.toString(), "webAddress" to webAddress.toString(), - "extraAdvertisedServiceIds" to advertisedServices.map { it.toString() }, "networkMapService" to networkMapServiceConfigLookup(name), "useTestClock" to useTestClock, "rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers.map { it.toMap() }, @@ -741,7 +734,6 @@ class DriverDSL( baseDirectory = baseDirectory(name), allowMissingConfig = true, configOverrides = node.config + notary + mapOf( - "extraAdvertisedServiceIds" to node.advertisedServices, "networkMapService" to networkMapServiceConfigLookup(name), "rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers ) @@ -935,7 +927,7 @@ class DriverDSL( // Write node.conf writeConfig(nodeConf.baseDirectory, "node.conf", config) // TODO pass the version in? - val node = Node(nodeConf, nodeConf.calculateServices(), MOCK_VERSION_INFO, initialiseSerialization = false).start() + val node = Node(nodeConf, MOCK_VERSION_INFO, initialiseSerialization = false).start() val nodeThread = thread(name = nodeConf.myLegalName.organisation) { node.internals.run() } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/CordformUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/CordformUtils.kt index 80f078d2bf..dcf0255a9a 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/CordformUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/CordformUtils.kt @@ -8,7 +8,6 @@ import net.corda.core.identity.CordaX500Name import net.corda.node.services.config.NotaryConfig import net.corda.nodeapi.User import net.corda.nodeapi.config.toConfig -import net.corda.nodeapi.internal.ServiceInfo fun CordformDefinition.node(configure: CordformNode.() -> Unit) { addNode { cordformNode -> cordformNode.configure() } @@ -23,7 +22,3 @@ fun CordformNode.rpcUsers(vararg users: User) { fun CordformNode.notary(notaryConfig: NotaryConfig) { notary = notaryConfig.toConfig().root().unwrapped() } - -fun CordformNode.advertisedServices(vararg services: ServiceInfo) { - advertisedServices = services.map { it.toString() } -} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index 43471696df..88824e118e 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -115,15 +115,13 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, * but can be overriden to cause nodes to have stable or colliding identity/service keys. */ fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, notaryIdentity: Pair?, - entropyRoot: BigInteger): N + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): N } object DefaultFactory : Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, - advertisedServices: Set, id: Int, notaryIdentity: Pair?, - entropyRoot: BigInteger): MockNode { - return MockNode(config, network, networkMapAddr, advertisedServices, id, notaryIdentity, entropyRoot) + id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): MockNode { + return MockNode(config, network, networkMapAddr, id, notaryIdentity, entropyRoot) } } @@ -159,11 +157,10 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, open class MockNode(config: NodeConfiguration, val mockNet: MockNetwork, override val networkMapAddress: SingleMessageRecipient?, - advertisedServices: Set, val id: Int, internal val notaryIdentity: Pair?, val entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue())) : - AbstractNode(config, advertisedServices, TestClock(), MOCK_VERSION_INFO, mockNet.busyLatch) { + AbstractNode(config, TestClock(), MOCK_VERSION_INFO, mockNet.busyLatch) { var counter = entropyRoot override val log: Logger = loggerFor() override val serverThread: AffinityExecutor = @@ -303,7 +300,6 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, nodeFactory = nodeFactory ?: defaultFactory, legalName = MOCK_NET_MAP.name, notaryIdentity = null, - advertisedServices = arrayOf(), entropyRoot = BigInteger.valueOf(random63BitValue()), configOverrides = {}, start = true @@ -315,18 +311,16 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, fun createUnstartedNode(forcedID: Int? = null, legalName: CordaX500Name? = null, notaryIdentity: Pair? = null, entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), - vararg advertisedServices: ServiceInfo, configOverrides: (NodeConfiguration) -> Any? = {}): MockNode { - return createUnstartedNode(forcedID, defaultFactory, legalName, notaryIdentity, entropyRoot, *advertisedServices, configOverrides = configOverrides) + return createUnstartedNode(forcedID, defaultFactory, legalName, notaryIdentity, entropyRoot, configOverrides = configOverrides) } fun createUnstartedNode(forcedID: Int? = null, nodeFactory: Factory, legalName: CordaX500Name? = null, notaryIdentity: Pair? = null, entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), - vararg advertisedServices: ServiceInfo, configOverrides: (NodeConfiguration) -> Any? = {}): N { val networkMapAddress = networkMapNode.network.myAddress - return createNodeImpl(networkMapAddress, forcedID, nodeFactory, false, legalName, notaryIdentity, entropyRoot, advertisedServices, configOverrides) + return createNodeImpl(networkMapAddress, forcedID, nodeFactory, false, legalName, notaryIdentity, entropyRoot, configOverrides) } /** @@ -340,25 +334,22 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, fun createNode(forcedID: Int? = null, legalName: CordaX500Name? = null, notaryIdentity: Pair? = null, entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), - vararg advertisedServices: ServiceInfo, configOverrides: (NodeConfiguration) -> Any? = {}): StartedNode { - return createNode(forcedID, defaultFactory, legalName, notaryIdentity, entropyRoot, *advertisedServices, configOverrides = configOverrides) + return createNode(forcedID, defaultFactory, legalName, notaryIdentity, entropyRoot, configOverrides = configOverrides) } /** Like the other [createNode] but takes a [Factory] and propagates its [MockNode] subtype. */ fun createNode(forcedID: Int? = null, nodeFactory: Factory, legalName: CordaX500Name? = null, notaryIdentity: Pair? = null, entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), - vararg advertisedServices: ServiceInfo, configOverrides: (NodeConfiguration) -> Any? = {}): StartedNode { val networkMapAddress = networkMapNode.network.myAddress - return uncheckedCast(createNodeImpl(networkMapAddress, forcedID, nodeFactory, true, legalName, notaryIdentity, entropyRoot, advertisedServices, configOverrides).started)!! + return uncheckedCast(createNodeImpl(networkMapAddress, forcedID, nodeFactory, true, legalName, notaryIdentity, entropyRoot, configOverrides).started)!! } private fun createNodeImpl(networkMapAddress: SingleMessageRecipient?, forcedID: Int?, nodeFactory: Factory, start: Boolean, legalName: CordaX500Name?, notaryIdentity: Pair?, entropyRoot: BigInteger, - advertisedServices: Array, configOverrides: (NodeConfiguration) -> Any?): N { val id = forcedID ?: nextNodeId++ val config = testNodeConfiguration( @@ -367,7 +358,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, whenever(it.dataSourceProperties).thenReturn(makeTestDataSourceProperties("node_${id}_net_$networkId")) configOverrides(it) } - return nodeFactory.create(config, this, networkMapAddress, advertisedServices.toSet(), id, notaryIdentity, entropyRoot).apply { + return nodeFactory.create(config, this, networkMapAddress, id, notaryIdentity, entropyRoot).apply { if (start) { start() if (threadPerNode && networkMapAddress != null) nodeReadyFuture.getOrThrow() // XXX: What about manually-started nodes? @@ -425,7 +416,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, return when (msgRecipient) { is SingleMessageRecipient -> nodes.single { it.started!!.network.myAddress == msgRecipient } is InMemoryMessagingNetwork.ServiceHandle -> { - nodes.firstOrNull { it.advertisedServices.any { it.name == msgRecipient.party.name } } + nodes.firstOrNull { it.started!!.info.isLegalIdentity(msgRecipient.party) } ?: throw IllegalArgumentException("Couldn't find node advertising service with owning party name: ${msgRecipient.party.name} ") } else -> throw IllegalArgumentException("Method not implemented for different type of message recipients") diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt index 3a8f838ffb..3d4d4e9d63 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt @@ -14,7 +14,6 @@ import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.nodeapi.User import net.corda.nodeapi.config.parseAs import net.corda.nodeapi.config.toConfig -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.DUMMY_MAP import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.driver.addressMustNotBeBoundFuture @@ -83,11 +82,10 @@ abstract class NodeBasedTest : TestDependencyInjectionBase() { */ fun startNetworkMapNode(legalName: CordaX500Name = DUMMY_MAP.name, platformVersion: Int = 1, - advertisedServices: Set = emptySet(), rpcUsers: List = emptyList(), configOverrides: Map = emptyMap()): StartedNode { check(_networkMapNode == null || _networkMapNode!!.info.legalIdentitiesAndCerts.first().name == legalName) - return startNodeInternal(legalName, platformVersion, advertisedServices, rpcUsers, configOverrides).apply { + return startNodeInternal(legalName, platformVersion, rpcUsers, configOverrides).apply { _networkMapNode = this } } @@ -95,7 +93,6 @@ abstract class NodeBasedTest : TestDependencyInjectionBase() { @JvmOverloads fun startNode(legalName: CordaX500Name, platformVersion: Int = 1, - advertisedServices: Set = emptySet(), rpcUsers: List = emptyList(), configOverrides: Map = emptyMap(), noNetworkMap: Boolean = false, @@ -119,7 +116,6 @@ abstract class NodeBasedTest : TestDependencyInjectionBase() { val node = startNodeInternal( legalName, platformVersion, - advertisedServices, rpcUsers, networkMapConf + configOverrides, noNetworkMap) @@ -175,7 +171,6 @@ abstract class NodeBasedTest : TestDependencyInjectionBase() { private fun startNodeInternal(legalName: CordaX500Name, platformVersion: Int, - advertisedServices: Set, rpcUsers: List, configOverrides: Map, noNetworkMap: Boolean = false): StartedNode { @@ -189,14 +184,15 @@ abstract class NodeBasedTest : TestDependencyInjectionBase() { "myLegalName" to legalName.toString(), "p2pAddress" to p2pAddress, "rpcAddress" to localPort[1].toString(), - "extraAdvertisedServiceIds" to advertisedServices.map { it.toString() }, "rpcUsers" to rpcUsers.map { it.toMap() }, "noNetworkMap" to noNetworkMap ) + configOverrides ) val parsedConfig = config.parseAs() - val node = Node(parsedConfig, parsedConfig.calculateServices(), MOCK_VERSION_INFO.copy(platformVersion = platformVersion), + val node = Node( + parsedConfig, + MOCK_VERSION_INFO.copy(platformVersion = platformVersion), initialiseSerialization = false).start() nodes += node thread(name = legalName.organisation) { diff --git a/tools/demobench/README.md b/tools/demobench/README.md index dc42716039..ad1550b9e5 100644 --- a/tools/demobench/README.md +++ b/tools/demobench/README.md @@ -70,9 +70,6 @@ node in a new tab. ![Configure Bank Node](demobench-configure-bank.png) -This time, there will be additional services available. Select `corda.cash` and -`corda.issuer.GBP`, and then press the `Start node` button. - When you press the `Launch Web Server` this time, your browser should open to a page saying: > ### Installed CorDapps diff --git a/tools/demobench/build.gradle b/tools/demobench/build.gradle index 727a5c8237..c2d1142cc7 100644 --- a/tools/demobench/build.gradle +++ b/tools/demobench/build.gradle @@ -71,6 +71,7 @@ dependencies { testCompile "junit:junit:$junit_version" testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" + testCompile "org.assertj:assertj-core:${assertj_version}" testCompile "org.mockito:mockito-core:$mockito_version" } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt index a03e83d88c..165388b2b0 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt @@ -5,13 +5,12 @@ import net.corda.core.internal.div import net.corda.core.internal.list import net.corda.core.utilities.loggerFor import net.corda.demobench.model.JVMConfig -import net.corda.demobench.model.NodeConfig -import net.corda.demobench.model.forceDirectory +import net.corda.demobench.model.NodeConfigWrapper import net.corda.demobench.readErrorLines import tornadofx.* import java.io.IOException import java.nio.file.Files -import java.nio.file.StandardCopyOption.* +import java.nio.file.StandardCopyOption.REPLACE_EXISTING import java.util.concurrent.Executors class Explorer internal constructor(private val explorerController: ExplorerController) : AutoCloseable { @@ -23,29 +22,32 @@ class Explorer internal constructor(private val explorerController: ExplorerCont private var process: Process? = null @Throws(IOException::class) - fun open(config: NodeConfig, onExit: (NodeConfig) -> Unit) { - val explorerDir = config.explorerDir.toFile() + fun open(config: NodeConfigWrapper, onExit: (NodeConfigWrapper) -> Unit) { + val explorerDir = config.explorerDir - if (!explorerDir.forceDirectory()) { - log.warn("Failed to create working directory '{}'", explorerDir.absolutePath) + try { + explorerDir.createDirectories() + } catch (e: IOException) { + log.warn("Failed to create working directory '{}'", explorerDir.toAbsolutePath()) onExit(config) return } + val legalName = config.nodeConfig.myLegalName try { installApps(config) - val user = config.users.elementAt(0) + val user = config.nodeConfig.rpcUsers[0] val p = explorerController.process( "--host=localhost", - "--port=${config.rpcPort}", + "--port=${config.nodeConfig.rpcAddress.port}", "--username=${user.username}", "--password=${user.password}") - .directory(explorerDir) + .directory(explorerDir.toFile()) .start() process = p - log.info("Launched Node Explorer for '{}'", config.legalName) + log.info("Launched Node Explorer for '{}'", legalName) // Close these streams because no-one is using them. safeClose(p.outputStream) @@ -57,21 +59,21 @@ class Explorer internal constructor(private val explorerController: ExplorerCont process = null if (errors.isEmpty()) { - log.info("Node Explorer for '{}' has exited (value={})", config.legalName, exitValue) + log.info("Node Explorer for '{}' has exited (value={})", legalName, exitValue) } else { - log.error("Node Explorer for '{}' has exited (value={}, {})", config.legalName, exitValue, errors) + log.error("Node Explorer for '{}' has exited (value={}, {})", legalName, exitValue, errors) } onExit(config) } } catch (e: IOException) { - log.error("Failed to launch Node Explorer for '{}': {}", config.legalName, e.message) + log.error("Failed to launch Node Explorer for '{}': {}", legalName, e.message) onExit(config) throw e } } - private fun installApps(config: NodeConfig) { + private fun installApps(config: NodeConfigWrapper) { // Make sure that the explorer has cordapps on its class path. This is only necessary because currently apps // require the original class files to deserialise states: Kryo serialisation doesn't let us write generic // tools that work with serialised data structures. But the AMQP serialisation revamp will fix this by diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt index 4d8876b2ed..f34ebfef6d 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt @@ -1,76 +1,46 @@ package net.corda.demobench.model import com.typesafe.config.Config -import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort +import net.corda.nodeapi.config.parseAs import tornadofx.* import java.io.IOException import java.nio.file.Files import java.nio.file.Path class InstallFactory : Controller() { - private val nodeController by inject() - private val serviceController by inject() @Throws(IOException::class) fun toInstallConfig(config: Config, baseDir: Path): InstallConfig { - val p2pPort = config.parsePort("p2pAddress") - val rpcPort = config.parsePort("rpcAddress") - val webPort = config.parsePort("webAddress") - val h2Port = config.getInt("h2port") - val x500name = CordaX500Name.parse(config.getString("myLegalName")) - val extraServices = config.parseExtraServices("extraAdvertisedServiceIds") - val tempDir = Files.createTempDirectory(baseDir, ".node") - - val nodeConfig = NodeConfig( - tempDir, - x500name, - p2pPort, - rpcPort, - webPort, - h2Port, - extraServices, - config.getObjectList("rpcUsers").map { toUser(it.unwrapped()) }.toList() - ) - - if (config.hasPath("networkMapService")) { - val nmap = config.getConfig("networkMapService") - nodeConfig.networkMap = NetworkMapConfig(CordaX500Name.parse(nmap.getString("legalName")), nmap.parsePort("address")) - } else { - log.info("Node '${nodeConfig.legalName}' is the network map") + fun NetworkHostAndPort.checkPort() { + require(nodeController.isPortValid(port)) { "Invalid port $port" } } - return InstallConfig(tempDir, nodeConfig) - } + val nodeConfig = config.parseAs() + nodeConfig.p2pAddress.checkPort() + nodeConfig.rpcAddress.checkPort() + nodeConfig.webAddress.checkPort() - private fun Config.parsePort(path: String): Int { - val address = this.getString(path) - val port = NetworkHostAndPort.parse(address).port - require(nodeController.isPortValid(port), { "Invalid port $port from '$path'." }) - return port - } + val tempDir = Files.createTempDirectory(baseDir, ".node") - private fun Config.parseExtraServices(path: String): MutableList { - val services = serviceController.services.values.toSortedSet() - return this.getStringList(path) - .filter { !it.isNullOrEmpty() } - .map { svc -> - require(svc in services, { "Unknown service '$svc'." }) - svc - }.toMutableList() - } + if (nodeConfig.isNetworkMap) { + log.info("Node '${nodeConfig.myLegalName}' is the network map") + } + return InstallConfig(tempDir, NodeConfigWrapper(tempDir, nodeConfig)) + } } /** * Wraps the configuration information for a Node * which isn't ready to be instantiated yet. */ -class InstallConfig internal constructor(val baseDir: Path, private val config: NodeConfig) : HasPlugins { +class InstallConfig internal constructor(val baseDir: Path, private val config: NodeConfigWrapper) : HasPlugins { val key = config.key - override val pluginDir: Path = baseDir.resolve("plugins") + override val pluginDir: Path = baseDir / "plugins" fun deleteBaseDir(): Boolean = baseDir.toFile().deleteRecursively() - fun installTo(installDir: Path) = config.moveTo(installDir) + fun installTo(installDir: Path) = config.copy(baseDir = installDir) } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NetworkMapConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NetworkMapConfig.kt deleted file mode 100644 index 3960ee9a24..0000000000 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NetworkMapConfig.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.demobench.model - -import net.corda.core.identity.CordaX500Name - -open class NetworkMapConfig(val legalName: CordaX500Name, val p2pPort: Int) { - val key: String = legalName.organisation.toKey() -} - -fun String.stripWhitespace() = String(this.filter { !it.isWhitespace() }.toCharArray()) -fun String.toKey() = stripWhitespace().toLowerCase() diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt index 8cde8f3bcf..e536b3bfb4 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt @@ -1,87 +1,77 @@ package net.corda.demobench.model -import com.typesafe.config.* +import com.typesafe.config.ConfigRenderOptions import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.copyToDirectory +import net.corda.core.internal.createDirectories +import net.corda.core.internal.div +import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.User -import java.io.File -import java.nio.file.Files +import net.corda.nodeapi.config.toConfig import java.nio.file.Path import java.nio.file.StandardCopyOption -class NodeConfig constructor( - baseDir: Path, - legalName: CordaX500Name, - p2pPort: Int, - val rpcPort: Int, - val webPort: Int, - val h2Port: Int, - val extraServices: MutableList = mutableListOf(), - val users: List = listOf(defaultUser), - var networkMap: NetworkMapConfig? = null -) : NetworkMapConfig(legalName, p2pPort), HasPlugins { - +/** + * This is a subset of FullNodeConfiguration, containing only those configs which we need. The node uses reference.conf + * to fill in the defaults so we're not required to specify them here. + */ +data class NodeConfig( + val myLegalName: CordaX500Name, + val p2pAddress: NetworkHostAndPort, + val rpcAddress: NetworkHostAndPort, + /** This is not used by the node but by the webserver which looks at node.conf. */ + val webAddress: NetworkHostAndPort, + val notary: NotaryService?, + val networkMapService: NetworkMapConfig?, + val h2port: Int, + val rpcUsers: List = listOf(defaultUser), + /** This is an extra config used by the Cash app. */ + val issuableCurrencies: List = emptyList() +) { companion object { val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false) val defaultUser = user("guest") } - val nearestCity: String = legalName.locality - val nodeDir: Path = baseDir.resolve(key) - override val pluginDir: Path = nodeDir.resolve("plugins") - val explorerDir: Path = baseDir.resolve("$key-explorer") + @Suppress("unused") + private val detectPublicIp = false + @Suppress("unused") + private val useTestClock = true + val isNetworkMap: Boolean get() = networkMapService == null + + fun toText(): String = toConfig().root().render(renderOptions) +} + +/** + * This is a mirror of NetworkMapInfo. + */ +data class NetworkMapConfig(val legalName: CordaX500Name, val address: NetworkHostAndPort) + +/** + * This is a subset of NotaryConfig. It implements [ExtraService] to avoid unnecessary copying. + */ +data class NotaryService(val validating: Boolean) : ExtraService { + override fun toString(): String = "${if (validating) "V" else "Non-v"}alidating Notary" +} + +// TODO Think of a better name +data class NodeConfigWrapper(val baseDir: Path, val nodeConfig: NodeConfig) : HasPlugins { + val key: String = nodeConfig.myLegalName.organisation.toKey() + val nodeDir: Path = baseDir / key + val explorerDir: Path = baseDir / "$key-explorer" + override val pluginDir: Path = nodeDir / "plugins" var state: NodeState = NodeState.STARTING - val isCashIssuer: Boolean = extraServices.any { - it.startsWith("corda.issuer.") - } - - fun isNetworkMap(): Boolean = networkMap == null - - /* - * The configuration object depends upon the networkMap, - * which is mutable. - */ - fun toFileConfig(): Config { - return ConfigFactory.empty() - .withValue("myLegalName", valueFor(legalName.toString())) - .withValue("p2pAddress", addressValueFor(p2pPort)) - .withValue("extraAdvertisedServiceIds", valueFor(extraServices)) - .withFallback(optional("networkMapService", networkMap, { c, n -> - c.withValue("address", addressValueFor(n.p2pPort)) - .withValue("legalName", valueFor(n.legalName.toString())) - })) - .withValue("webAddress", addressValueFor(webPort)) - .withValue("rpcAddress", addressValueFor(rpcPort)) - .withValue("rpcUsers", valueFor(users.map(User::toMap).toList())) - .withValue("h2port", valueFor(h2Port)) - .withValue("useTestClock", valueFor(true)) - .withValue("detectPublicIp", valueFor(false)) - } - - fun toText(): String = toFileConfig().root().render(renderOptions) - - fun moveTo(baseDir: Path) = NodeConfig( - baseDir, legalName, p2pPort, rpcPort, webPort, h2Port, extraServices, users, networkMap - ) - - fun install(plugins: Collection) { - if (plugins.isNotEmpty() && pluginDir.toFile().forceDirectory()) { - plugins.forEach { - Files.copy(it, pluginDir.resolve(it.fileName.toString()), StandardCopyOption.REPLACE_EXISTING) - } + fun install(cordapps: Collection) { + if (cordapps.isEmpty()) return + pluginDir.createDirectories() + for (cordapp in cordapps) { + cordapp.copyToDirectory(pluginDir, StandardCopyOption.REPLACE_EXISTING) } } - } -private fun valueFor(any: T): ConfigValue? = ConfigValueFactory.fromAnyRef(any) +fun user(name: String) = User(name, "letmein", setOf("ALL")) -private fun addressValueFor(port: Int) = valueFor("localhost:$port") - -private inline fun optional(path: String, obj: T?, body: (Config, T) -> Config): Config { - val config = ConfigFactory.empty() - return if (obj == null) config else body(config, obj).atPath(path) -} - -fun File.forceDirectory(): Boolean = this.isDirectory || this.mkdirs() +fun String.toKey() = filter { !it.isWhitespace() }.toLowerCase() 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 5bd33b700e..4e7b6a6cc7 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 @@ -1,6 +1,12 @@ package net.corda.demobench.model +import javafx.beans.binding.IntegerExpression import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.copyToDirectory +import net.corda.core.internal.createDirectories +import net.corda.core.internal.div +import net.corda.core.internal.noneOrSingle +import net.corda.core.utilities.NetworkHostAndPort import net.corda.demobench.plugin.PluginController import net.corda.demobench.pty.R3Pty import tornadofx.* @@ -22,18 +28,17 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { private val jvm by inject() private val pluginController by inject() - private val serviceController by inject() private var baseDir: Path = baseDirFor(ManagementFactory.getRuntimeMXBean().startTime) private val cordaPath: Path = jvm.applicationDir.resolve("corda").resolve("corda.jar") private val command = jvm.commandFor(cordaPath).toTypedArray() - private val nodes = LinkedHashMap() + private val nodes = LinkedHashMap() private val port = AtomicInteger(firstPort) private var networkMapConfig: NetworkMapConfig? = null - val activeNodes: List + val activeNodes: List get() = nodes.values.filter { (it.state == NodeState.RUNNING) || (it.state == NodeState.STARTING) } @@ -50,38 +55,45 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { /** * Validate a Node configuration provided by [net.corda.demobench.views.NodeTabView]. */ - fun validate(nodeData: NodeData): NodeConfig? { + fun validate(nodeData: NodeData): NodeConfigWrapper? { + fun IntegerExpression.toLocalAddress() = NetworkHostAndPort("localhost", value) + val location = nodeData.nearestCity.value - val config = NodeConfig( - baseDir, - CordaX500Name( + val nodeConfig = NodeConfig( + myLegalName = CordaX500Name( organisation = nodeData.legalName.value.trim(), locality = location.description, country = location.countryCode ), - nodeData.p2pPort.value, - nodeData.rpcPort.value, - nodeData.webPort.value, - nodeData.h2Port.value, - nodeData.extraServices.map { serviceController.services[it]!! }.toMutableList() + p2pAddress = nodeData.p2pPort.toLocalAddress(), + rpcAddress = nodeData.rpcPort.toLocalAddress(), + webAddress = nodeData.webPort.toLocalAddress(), + notary = nodeData.extraServices.filterIsInstance().noneOrSingle(), + networkMapService = networkMapConfig, // The first node becomes the network map + h2port = nodeData.h2Port.value, + issuableCurrencies = nodeData.extraServices.filterIsInstance().map { it.currency.toString() } ) - if (nodes.putIfAbsent(config.key, config) != null) { - log.warning("Node with key '${config.key}' already exists.") + val wrapper = NodeConfigWrapper(baseDir, nodeConfig) + + if (nodes.putIfAbsent(wrapper.key, wrapper) != null) { + log.warning("Node with key '${wrapper.key}' already exists.") return null } - // The first node becomes our network map - chooseNetworkMap(config) + if (nodeConfig.isNetworkMap) { + networkMapConfig = nodeConfig.let { NetworkMapConfig(it.myLegalName, it.p2pAddress) } + log.info("Network map provided by: ${nodeConfig.myLegalName}") + } - return config + return wrapper } - fun dispose(config: NodeConfig) { + fun dispose(config: NodeConfigWrapper) { config.state = NodeState.DEAD - if (config.networkMap == null) { - log.warning("Network map service (Node '${config.legalName}') has exited.") + if (config.nodeConfig.isNetworkMap) { + log.warning("Network map service (Node '${config.nodeConfig.myLegalName}') has exited.") } } @@ -95,39 +107,26 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { fun hasNetworkMap(): Boolean = networkMapConfig != null - private fun chooseNetworkMap(config: NodeConfig) { - if (hasNetworkMap()) { - config.networkMap = networkMapConfig - } else { - networkMapConfig = config - log.info("Network map provided by: ${config.legalName}") - } - } + fun runCorda(pty: R3Pty, config: NodeConfigWrapper): Boolean { + try { + config.nodeDir.createDirectories() - fun runCorda(pty: R3Pty, config: NodeConfig): Boolean { - val nodeDir = config.nodeDir.toFile() + // Install any built-in plugins into the working directory. + pluginController.populate(config) - if (nodeDir.forceDirectory()) { - try { - // Install any built-in plugins into the working directory. - pluginController.populate(config) + // Write this node's configuration file into its working directory. + val confFile = config.nodeDir / "node.conf" + Files.write(confFile, config.nodeConfig.toText().toByteArray()) - // Write this node's configuration file into its working directory. - val confFile = nodeDir.resolve("node.conf") - confFile.writeText(config.toText()) - - // Execute the Corda node - val cordaEnv = System.getenv().toMutableMap().apply { - jvm.setCapsuleCacheDir(this) - } - pty.run(command, cordaEnv, nodeDir.toString()) - log.info("Launched node: ${config.legalName}") - return true - } catch (e: Exception) { - log.log(Level.SEVERE, "Failed to launch Corda: ${e.message}", e) - return false + // Execute the Corda node + val cordaEnv = System.getenv().toMutableMap().apply { + jvm.setCapsuleCacheDir(this) } - } else { + pty.run(command, cordaEnv, config.nodeDir.toString()) + log.info("Launched node: ${config.nodeConfig.myLegalName}") + return true + } catch (e: Exception) { + log.log(Level.SEVERE, "Failed to launch Corda: ${e.message}", e) return false } } @@ -144,15 +143,15 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { /** * Add a [NodeConfig] object that has been loaded from a profile. */ - fun register(config: NodeConfig): Boolean { + fun register(config: NodeConfigWrapper): Boolean { if (nodes.putIfAbsent(config.key, config) != null) { return false } - updatePort(config) + updatePort(config.nodeConfig) - if ((networkMapConfig == null) && config.isNetworkMap()) { - networkMapConfig = config + if (networkMapConfig == null && config.nodeConfig.isNetworkMap) { + networkMapConfig = config.nodeConfig.let { NetworkMapConfig(it.myLegalName, it.p2pAddress) } } return true @@ -162,12 +161,12 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { * Creates a node directory that can host a running instance of Corda. */ @Throws(IOException::class) - fun install(config: InstallConfig): NodeConfig { + fun install(config: InstallConfig): NodeConfigWrapper { val installed = config.installTo(baseDir) pluginController.userPluginsFor(config).forEach { - val pluginDir = Files.createDirectories(installed.pluginDir) - val plugin = Files.copy(it, pluginDir.resolve(it.fileName.toString())) + installed.pluginDir.createDirectories() + val plugin = it.copyToDirectory(installed.pluginDir) log.info("Installed: $plugin") } @@ -179,7 +178,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { } private fun updatePort(config: NodeConfig) { - val nextPort = 1 + arrayOf(config.p2pPort, config.rpcPort, config.webPort, config.h2Port).max() as Int + val nextPort = 1 + arrayOf(config.p2pAddress.port, config.rpcAddress.port, config.webAddress.port, config.h2port).max() as Int port.getAndUpdate { Math.max(nextPort, it) } } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt index 90147c5b43..7abdd09775 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt @@ -4,8 +4,10 @@ import javafx.beans.property.SimpleIntegerProperty import javafx.beans.property.SimpleListProperty import javafx.beans.property.SimpleObjectProperty import javafx.beans.property.SimpleStringProperty +import javafx.collections.FXCollections.observableArrayList import net.corda.finance.utils.CityDatabase import tornadofx.* +import java.util.* object SuggestedDetails { val banks = listOf( @@ -35,7 +37,7 @@ class NodeData { val rpcPort = SimpleIntegerProperty() val webPort = SimpleIntegerProperty() val h2Port = SimpleIntegerProperty() - val extraServices = SimpleListProperty(mutableListOf().observable()) + val extraServices = SimpleListProperty(observableArrayList()) } class NodeDataModel : ItemViewModel(NodeData()) { @@ -46,3 +48,9 @@ class NodeDataModel : ItemViewModel(NodeData()) { val webPort = bind { item?.webPort } val h2Port = bind { item?.h2Port } } + +interface ExtraService + +data class CurrencyIssuer(val currency: Currency) : ExtraService { + override fun toString(): String = "Issuer $currency" +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/ServiceController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/ServiceController.kt deleted file mode 100644 index 729f305805..0000000000 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/ServiceController.kt +++ /dev/null @@ -1,44 +0,0 @@ -package net.corda.demobench.model - -import tornadofx.* -import java.io.IOException -import java.io.InputStreamReader -import java.net.URL -import java.util.logging.Level - -class ServiceController(resourceName: String = "/services.conf") : Controller() { - - val services: Map = loadConf(resources.url(resourceName)) - - val notaries: Map = services.filter { it.value.startsWith("corda.notary.") } - - val issuers: Map = services.filter { it.value.startsWith("corda.issuer.") } - - /* - * Load our list of known extra Corda services. - */ - private fun loadConf(url: URL?): Map { - return if (url == null) { - emptyMap() - } else { - try { - val map = linkedMapOf() - InputStreamReader(url.openStream()).useLines { sq -> - sq.forEach { line -> - val service = line.split(":").map { it.trim() } - if (service.size != 2) { - log.warning("Encountered corrupted line '$line' while reading services from config: $url") - } else { - map[service[1]] = service[0] - log.info("Supports: $service") - } - } - map - } - } catch (e: IOException) { - log.log(Level.SEVERE, "Failed to load $url: ${e.message}", e) - emptyMap() - } - } - } -} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/User.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/User.kt deleted file mode 100644 index c55f3bc5d3..0000000000 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/User.kt +++ /dev/null @@ -1,15 +0,0 @@ -@file:JvmName("User") - -package net.corda.demobench.model - -import net.corda.core.internal.uncheckedCast -import net.corda.nodeapi.User -import java.util.* - -fun toUser(map: Map) = User( - map.getOrElse("username", { "none" }) as String, - map.getOrElse("password", { "none" }) as String, - LinkedHashSet(uncheckedCast>(map.getOrElse("permissions", { emptyList() }))) -) - -fun user(name: String) = User(name, "letmein", setOf("ALL")) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/PluginController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/PluginController.kt index 3e9d52f5ca..ac25d72c55 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/PluginController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/PluginController.kt @@ -5,7 +5,7 @@ import net.corda.core.internal.createDirectories import net.corda.core.internal.exists import net.corda.demobench.model.HasPlugins import net.corda.demobench.model.JVMConfig -import net.corda.demobench.model.NodeConfig +import net.corda.demobench.model.NodeConfigWrapper import tornadofx.* import java.io.IOException import java.nio.file.Files @@ -24,16 +24,14 @@ class PluginController : Controller() { * Install any built-in plugins that this node requires. */ @Throws(IOException::class) - fun populate(config: NodeConfig) { - if (!config.pluginDir.exists()) { - config.pluginDir.createDirectories() - } + fun populate(config: NodeConfigWrapper) { + config.pluginDir.createDirectories() if (finance.exists()) { finance.copyToDirectory(config.pluginDir, StandardCopyOption.REPLACE_EXISTING) log.info("Installed 'Finance' plugin") } // Nodes cannot issue cash unless they contain the "Bank of Corda" plugin. - if (config.isCashIssuer && bankOfCorda.exists()) { + if (config.nodeConfig.issuableCurrencies.isNotEmpty() && bankOfCorda.exists()) { bankOfCorda.copyToDirectory(config.pluginDir, StandardCopyOption.REPLACE_EXISTING) log.info("Installed 'Bank of Corda' plugin") } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt index 5d9a773466..c963a1c9ae 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt @@ -4,6 +4,8 @@ import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import javafx.stage.FileChooser import javafx.stage.FileChooser.ExtensionFilter +import net.corda.core.internal.createDirectories +import net.corda.core.internal.div import net.corda.demobench.model.InstallConfig import net.corda.demobench.model.InstallFactory import net.corda.demobench.model.JVMConfig @@ -58,8 +60,8 @@ class ProfileController : Controller() { FileSystems.newFileSystem(URI.create("jar:" + target.toURI()), mapOf("create" to "true")).use { fs -> configs.forEach { config -> // Write the configuration file. - val nodeDir = Files.createDirectories(fs.getPath(config.key)) - val file = Files.write(nodeDir.resolve("node.conf"), config.toText().toByteArray(UTF_8)) + val nodeDir = fs.getPath(config.key).createDirectories() + val file = Files.write(nodeDir / "node.conf", config.nodeConfig.toText().toByteArray(UTF_8)) log.info("Wrote: $file") // Write all of the non-built-in plugins. diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt index 339eed00ad..a92dbd4a3d 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt @@ -5,18 +5,17 @@ import net.corda.client.rpc.CordaRPCConnection import net.corda.core.messaging.CordaRPCOps import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor -import net.corda.demobench.model.NodeConfig +import net.corda.demobench.model.NodeConfigWrapper import java.util.* import java.util.concurrent.TimeUnit.SECONDS -class NodeRPC(config: NodeConfig, start: (NodeConfig, CordaRPCOps) -> Unit, invoke: (CordaRPCOps) -> Unit) : AutoCloseable { - +class NodeRPC(config: NodeConfigWrapper, start: (NodeConfigWrapper, CordaRPCOps) -> Unit, invoke: (CordaRPCOps) -> Unit) : AutoCloseable { private companion object { val log = loggerFor() val oneSecond = SECONDS.toMillis(1) } - private val rpcClient = CordaRPCClient(NetworkHostAndPort("localhost", config.rpcPort)) + private val rpcClient = CordaRPCClient(NetworkHostAndPort("localhost", config.nodeConfig.rpcAddress.port)) private var rpcConnection: CordaRPCConnection? = null private val timer = Timer() @@ -24,7 +23,7 @@ class NodeRPC(config: NodeConfig, start: (NodeConfig, CordaRPCOps) -> Unit, invo val setupTask = object : TimerTask() { override fun run() { try { - val user = config.users.elementAt(0) + val user = config.nodeConfig.rpcUsers[0] val connection = rpcClient.start(user.username, user.password) rpcConnection = connection val ops = connection.proxy @@ -42,7 +41,7 @@ class NodeRPC(config: NodeConfig, start: (NodeConfig, CordaRPCOps) -> Unit, invo } }, 0, oneSecond) } catch (e: Exception) { - log.warn("Node '{}' not ready yet (Error: {})", config.legalName, e.message) + log.warn("Node '{}' not ready yet (Error: {})", config.nodeConfig.myLegalName, e.message) } } } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt index f36edea872..c9f993a5c2 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt @@ -16,10 +16,14 @@ import javafx.scene.layout.Priority import javafx.stage.FileChooser import javafx.util.StringConverter import net.corda.core.internal.* -import net.corda.finance.utils.CityDatabase -import net.corda.finance.utils.WorldMapLocation import net.corda.demobench.model.* import net.corda.demobench.ui.CloseableTab +import net.corda.finance.CHF +import net.corda.finance.EUR +import net.corda.finance.GBP +import net.corda.finance.USD +import net.corda.finance.utils.CityDatabase +import net.corda.finance.utils.WorldMapLocation import org.controlsfx.control.CheckListView import tornadofx.* import java.nio.file.Path @@ -39,10 +43,10 @@ class NodeTabView : Fragment() { val cordappPathsFile: Path = jvm.dataHome / "cordapp-paths.txt" fun loadDefaultCordappPaths(): MutableList { - if (cordappPathsFile.exists()) - return cordappPathsFile.readAllLines().map { Paths.get(it) }.filter { it.exists() }.toMutableList() + return if (cordappPathsFile.exists()) + cordappPathsFile.readAllLines().map { Paths.get(it) }.filter { it.exists() }.toMutableList() else - return ArrayList() + ArrayList() } // This is shared between tabs. @@ -58,11 +62,9 @@ class NodeTabView : Fragment() { } private val nodeController by inject() - private val serviceController by inject() private val chooser = FileChooser() private val model = NodeDataModel() - private val availableServices: List = if (nodeController.hasNetworkMap()) serviceController.issuers.keys.toList() else serviceController.notaries.keys.toList() private val nodeTerminalView = find() private val nodeConfigView = stackpane { @@ -112,8 +114,13 @@ class NodeTabView : Fragment() { fieldset("Additional configuration") { styleClass.addAll("services-panel") + val extraServices = if (nodeController.hasNetworkMap()) { + listOf(USD, GBP, CHF, EUR).map { CurrencyIssuer(it) } + } else { + listOf(NotaryService(true), NotaryService(false)) + } - val servicesList = CheckListView(availableServices.observable()).apply { + val servicesList = CheckListView(extraServices.observable()).apply { vboxConstraints { vGrow = Priority.ALWAYS } model.item.extraServices.set(checkModel.checkedItems) if (!nodeController.hasNetworkMap()) { @@ -263,17 +270,17 @@ class NodeTabView : Fragment() { /** * Launches a preconfigured Corda node, e.g. from a saved profile. */ - fun launch(config: NodeConfig) { + fun launch(config: NodeConfigWrapper) { nodeController.register(config) launchNode(config) } - private fun launchNode(config: NodeConfig) { - val countryCode = CityDatabase.cityMap[config.nearestCity]?.countryCode + private fun launchNode(config: NodeConfigWrapper) { + val countryCode = CityDatabase.cityMap[config.nodeConfig.myLegalName.locality]?.countryCode if (countryCode != null) { nodeTab.graphic = ImageView(flags.get()[countryCode]).apply { fitWidth = 24.0; isPreserveRatio = true } } - nodeTab.text = config.legalName.organisation + nodeTab.text = config.nodeConfig.myLegalName.organisation nodeTerminalView.open(config) { exitCode -> Platform.runLater { if (exitCode == 0) { diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt index 6046aa0e11..d1e2f59492 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt @@ -20,6 +20,7 @@ import net.corda.core.messaging.vaultTrackBy import net.corda.core.node.services.vault.PageSpecification import net.corda.demobench.explorer.ExplorerController import net.corda.demobench.model.NodeConfig +import net.corda.demobench.model.NodeConfigWrapper import net.corda.demobench.model.NodeController import net.corda.demobench.model.NodeState import net.corda.demobench.pty.R3Pty @@ -69,8 +70,8 @@ class NodeTerminalView : Fragment() { private lateinit var logo: ImageView private lateinit var swingTerminal: SwingNode - fun open(config: NodeConfig, onExit: (Int) -> Unit) { - nodeName.text = config.legalName.organisation + fun open(config: NodeConfigWrapper, onExit: (Int) -> Unit) { + nodeName.text = config.nodeConfig.myLegalName.organisation swingTerminal = SwingNode() swingTerminal.setOnMouseClicked { @@ -85,13 +86,13 @@ class NodeTerminalView : Fragment() { root.isVisible = true SwingUtilities.invokeLater({ - val r3pty = R3Pty(config.legalName, TerminalSettingsProvider(), Dimension(160, 80), onExit) + val r3pty = R3Pty(config.nodeConfig.myLegalName, TerminalSettingsProvider(), Dimension(160, 80), onExit) pty = r3pty if (nodeController.runCorda(r3pty, config)) { swingTerminal.content = r3pty.terminal - configureDatabaseButton(config) + configureDatabaseButton(config.nodeConfig) configureExplorerButton(config) configureWebButton(config) @@ -105,7 +106,7 @@ class NodeTerminalView : Fragment() { * and close the RPC client if it has. */ if (!r3pty.isConnected) { - log.severe("Node '${config.legalName}' has failed to start.") + log.severe("Node '${config.nodeConfig.myLegalName}' has failed to start.") swingTerminal.content = null rpc?.close() } @@ -119,7 +120,7 @@ class NodeTerminalView : Fragment() { * launched the explorer and only reenable it when * the explorer has exited. */ - private fun configureExplorerButton(config: NodeConfig) { + private fun configureExplorerButton(config: NodeConfigWrapper) { launchExplorerButton.setOnAction { launchExplorerButton.isDisable = true @@ -131,7 +132,7 @@ class NodeTerminalView : Fragment() { private fun configureDatabaseButton(config: NodeConfig) { viewDatabaseButton.setOnAction { - viewer.openBrowser(config.h2Port) + viewer.openBrowser(config.h2port) } } @@ -144,7 +145,7 @@ class NodeTerminalView : Fragment() { * launched the web server and only reenable it when * the web server has exited. */ - private fun configureWebButton(config: NodeConfig) { + private fun configureWebButton(config: NodeConfigWrapper) { launchWebButton.setOnAction { if (webURL != null) { app.hostServices.showDocument(webURL.toString()) @@ -161,12 +162,12 @@ class NodeTerminalView : Fragment() { launchWebButton.text = "" launchWebButton.graphic = ProgressIndicator() - log.info("Starting web server for ${config.legalName}") + log.info("Starting web server for ${config.nodeConfig.myLegalName}") webServer.open(config).then { Platform.runLater { launchWebButton.graphic = null it.match(success = { - log.info("Web server for ${config.legalName} started on $it") + log.info("Web server for ${config.nodeConfig.myLegalName} started on $it") webURL = it launchWebButton.text = "Reopen\nweb site" app.hostServices.showDocument(it.toString()) @@ -178,13 +179,13 @@ class NodeTerminalView : Fragment() { } } - private fun launchRPC(config: NodeConfig) = NodeRPC( + private fun launchRPC(config: NodeConfigWrapper) = NodeRPC( config = config, start = this::initialise, invoke = this::pollCashBalances ) - private fun initialise(config: NodeConfig, ops: CordaRPCOps) { + private fun initialise(config: NodeConfigWrapper, ops: CordaRPCOps) { try { val (txInit, txNext) = ops.internalVerifiedTransactionsFeed() val (stateInit, stateNext) = ops.vaultTrackBy(paging = pageSpecification) @@ -212,7 +213,7 @@ class NodeTerminalView : Fragment() { } config.state = NodeState.RUNNING - log.info("Node '${config.legalName}' is now ready.") + log.info("Node '${config.nodeConfig.myLegalName}' is now ready.") header.isDisable = false } @@ -225,7 +226,7 @@ class NodeTerminalView : Fragment() { ) Platform.runLater { - balance.value = if (cashBalances.isNullOrEmpty()) "0" else cashBalances + balance.value = if (cashBalances.isEmpty()) "0" else cashBalances } } catch (e: ClassNotFoundException) { // TODO: Remove this special case once Rick's serialisation work means we can deserialise states that weren't on our own classpath. diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/web/WebServer.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/web/WebServer.kt index 4aec34a73b..5fe5f96fe1 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/web/WebServer.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/web/WebServer.kt @@ -2,11 +2,11 @@ package net.corda.demobench.web import com.google.common.util.concurrent.RateLimiter import net.corda.core.concurrent.CordaFuture -import net.corda.core.utilities.minutes import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.until import net.corda.core.utilities.loggerFor -import net.corda.demobench.model.NodeConfig +import net.corda.core.utilities.minutes +import net.corda.demobench.model.NodeConfigWrapper import net.corda.demobench.readErrorLines import java.io.IOException import java.net.HttpURLConnection @@ -25,7 +25,7 @@ class WebServer internal constructor(private val webServerController: WebServerC private var process: Process? = null @Throws(IOException::class) - fun open(config: NodeConfig): CordaFuture { + fun open(config: NodeConfigWrapper): CordaFuture { val nodeDir = config.nodeDir.toFile() if (!nodeDir.isDirectory) { @@ -33,13 +33,14 @@ class WebServer internal constructor(private val webServerController: WebServerC return openFuture() } + val legalName = config.nodeConfig.myLegalName try { val p = webServerController.process() .directory(nodeDir) .start() process = p - log.info("Launched Web Server for '{}'", config.legalName) + log.info("Launched Web Server for '{}'", legalName) // Close these streams because no-one is using them. safeClose(p.outputStream) @@ -51,22 +52,22 @@ class WebServer internal constructor(private val webServerController: WebServerC process = null if (errors.isEmpty()) { - log.info("Web Server for '{}' has exited (value={})", config.legalName, exitValue) + log.info("Web Server for '{}' has exited (value={})", legalName, exitValue) } else { - log.error("Web Server for '{}' has exited (value={}, {})", config.legalName, exitValue, errors) + log.error("Web Server for '{}' has exited (value={}, {})", legalName, exitValue, errors) } } val future = openFuture() thread { future.capture { - log.info("Waiting for web server for ${config.legalName} to start ...") - waitForStart(config.webPort) + log.info("Waiting for web server for $legalName to start ...") + waitForStart(config.nodeConfig.webAddress.port) } } return future } catch (e: IOException) { - log.error("Failed to launch Web Server for '{}': {}", config.legalName, e.message) + log.error("Failed to launch Web Server for '{}': {}", legalName, e.message) throw e } } diff --git a/tools/demobench/src/main/resources/services.conf b/tools/demobench/src/main/resources/services.conf deleted file mode 100644 index d8319ddccb..0000000000 --- a/tools/demobench/src/main/resources/services.conf +++ /dev/null @@ -1,6 +0,0 @@ -corda.notary.validating : Validating Notary -corda.notary.simple : Non-validating Notary -corda.issuer.USD : Issuer USD -corda.issuer.GBP : Issuer GBP -corda.issuer.CHF : Issuer CHF -corda.issuer.EUR : Issuer EUR \ No newline at end of file diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/LoggingTestSuite.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/LoggingTestSuite.kt index 946718c42a..33e9d4169b 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/LoggingTestSuite.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/LoggingTestSuite.kt @@ -3,7 +3,6 @@ package net.corda.demobench import net.corda.demobench.config.LoggingConfig import net.corda.demobench.model.JVMConfigTest import net.corda.demobench.model.NodeControllerTest -import net.corda.demobench.model.ServiceControllerTest import org.junit.BeforeClass import org.junit.runner.RunWith import org.junit.runners.Suite @@ -13,7 +12,6 @@ import org.junit.runners.Suite */ @RunWith(Suite::class) @Suite.SuiteClasses( - ServiceControllerTest::class, NodeControllerTest::class, JVMConfigTest::class ) diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NetworkMapConfigTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NetworkMapConfigTest.kt deleted file mode 100644 index 0c9892f340..0000000000 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NetworkMapConfigTest.kt +++ /dev/null @@ -1,22 +0,0 @@ -package net.corda.demobench.model - -import net.corda.core.identity.CordaX500Name -import org.bouncycastle.asn1.x500.X500Name -import org.junit.Ignore -import org.junit.Test -import kotlin.test.assertEquals - -class NetworkMapConfigTest { - @Ignore("This has been superseded by validation logic in CordaX500Name") - @Test - fun keyValue() { - val config = NetworkMapConfig(CordaX500Name.parse("O=My\tNasty Little\rLabel\n,L=London,C=GB"), 10000) - assertEquals("mynastylittlelabel", config.key) - } - - @Test - fun removeWhitespace() { - assertEquals("OneTwoThreeFour!", "One\tTwo \rThree\r\nFour!".stripWhitespace()) - } - -} diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt index 4970d68c11..e8fb14dff4 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt @@ -1,190 +1,30 @@ package net.corda.demobench.model -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.SerializationFeature import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigValueFactory import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.internal.NetworkMapInfo import net.corda.node.services.config.FullNodeConfiguration import net.corda.nodeapi.User import net.corda.nodeapi.config.parseAs +import net.corda.nodeapi.config.toConfig import net.corda.testing.DUMMY_NOTARY import net.corda.webserver.WebServerConfig +import org.assertj.core.api.Assertions.assertThat import org.junit.Test -import java.io.StringWriter import java.nio.file.Path import java.nio.file.Paths import kotlin.test.assertEquals import kotlin.test.assertFalse -import kotlin.test.assertNull import kotlin.test.assertTrue - class NodeConfigTest { - companion object { private val baseDir: Path = Paths.get(".").toAbsolutePath() private val myLegalName = CordaX500Name(organisation = "My Name", locality = "New York", country = "US") } - @Test - fun `test name`() { - val config = createConfig(legalName = myLegalName) - assertEquals(myLegalName, config.legalName) - assertEquals("myname", config.key) - } - - @Test - fun `test node directory`() { - val config = createConfig(legalName = myLegalName) - assertEquals(baseDir / "myname", config.nodeDir) - } - - @Test - fun `test explorer directory`() { - val config = createConfig(legalName = myLegalName) - assertEquals(baseDir / "myname-explorer", config.explorerDir) - } - - @Test - fun `test plugin directory`() { - val config = createConfig(legalName = myLegalName) - assertEquals(baseDir / "myname" / "plugins", config.pluginDir) - } - - @Test - fun `test P2P port`() { - val config = createConfig(p2pPort = 10001) - assertEquals(10001, config.p2pPort) - } - - @Test - fun `test rpc port`() { - val config = createConfig(rpcPort = 40002) - assertEquals(40002, config.rpcPort) - } - - @Test - fun `test web port`() { - val config = createConfig(webPort = 20001) - assertEquals(20001, config.webPort) - } - - @Test - fun `test H2 port`() { - val config = createConfig(h2Port = 30001) - assertEquals(30001, config.h2Port) - } - - @Test - fun `test services`() { - val config = createConfig(services = mutableListOf("my.service")) - assertEquals(listOf("my.service"), config.extraServices) - } - - @Test - fun `test users`() { - val config = createConfig(users = listOf(user("myuser"))) - assertEquals(listOf(user("myuser")), config.users) - } - - @Test - fun `test default state`() { - val config = createConfig() - assertEquals(NodeState.STARTING, config.state) - } - - @Test - fun `test network map`() { - val config = createConfig() - assertNull(config.networkMap) - assertTrue(config.isNetworkMap()) - } - - @Test - fun `test cash issuer`() { - val config = createConfig(services = mutableListOf("corda.issuer.GBP")) - assertTrue(config.isCashIssuer) - } - - @Test - fun `test not cash issuer`() { - val config = createConfig(services = mutableListOf("corda.issuerubbish")) - assertFalse(config.isCashIssuer) - } - - /** - * Reformat JSON via Jackson to ensure a consistent format for comparison purposes. - */ - private fun prettyPrint(content: String): String { - val mapper = ObjectMapper() - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL) - mapper.enable(SerializationFeature.INDENT_OUTPUT) - val sw = StringWriter() - val parsed = mapper.readTree(content) - mapper.writeValue(sw, parsed) - return sw.toString() - } - - @Test - fun `test config text`() { - val config = createConfig( - legalName = myLegalName, - p2pPort = 10001, - rpcPort = 40002, - webPort = 20001, - h2Port = 30001, - services = mutableListOf("my.service"), - users = listOf(user("jenny")) - ) - assertEquals(prettyPrint("{" - + "\"detectPublicIp\":false," - + "\"extraAdvertisedServiceIds\":[\"my.service\"]," - + "\"h2port\":30001," - + "\"myLegalName\":\"C=US,L=New York,O=My Name\"," - + "\"p2pAddress\":\"localhost:10001\"," - + "\"rpcAddress\":\"localhost:40002\"," - + "\"rpcUsers\":[" - + "{\"password\":\"letmein\",\"permissions\":[\"ALL\"],\"username\":\"jenny\"}" - + "]," - + "\"useTestClock\":true," - + "\"webAddress\":\"localhost:20001\"" - + "}"), prettyPrint(config.toText())) - } - - @Test - fun `test config text with network map`() { - val config = createConfig( - legalName = myLegalName, - p2pPort = 10001, - rpcPort = 40002, - webPort = 20001, - h2Port = 30001, - services = mutableListOf("my.service"), - users = listOf(user("jenny")) - ) - config.networkMap = NetworkMapConfig(DUMMY_NOTARY.name, 12345) - - assertEquals(prettyPrint("{" - + "\"detectPublicIp\":false," - + "\"extraAdvertisedServiceIds\":[\"my.service\"]," - + "\"h2port\":30001," - + "\"myLegalName\":\"C=US,L=New York,O=My Name\"," - + "\"networkMapService\":{\"address\":\"localhost:12345\",\"legalName\":\"C=CH,L=Zurich,O=Notary Service\"}," - + "\"p2pAddress\":\"localhost:10001\"," - + "\"rpcAddress\":\"localhost:40002\"," - + "\"rpcUsers\":[" - + "{\"password\":\"letmein\",\"permissions\":[\"ALL\"],\"username\":\"jenny\"}" - + "]," - + "\"useTestClock\":true," - + "\"webAddress\":\"localhost:20001\"" - + "}"), prettyPrint(config.toText())) - } - @Test fun `reading node configuration`() { val config = createConfig( @@ -192,13 +32,13 @@ class NodeConfigTest { p2pPort = 10001, rpcPort = 40002, webPort = 20001, - h2Port = 30001, - services = mutableListOf("my.service"), + h2port = 30001, + notary = NotaryService(validating = false), + networkMap = NetworkMapConfig(DUMMY_NOTARY.name, localPort(12345)), users = listOf(user("jenny")) ) - config.networkMap = NetworkMapConfig(DUMMY_NOTARY.name, 12345) - val nodeConfig = config.toFileConfig() + val nodeConfig = config.toConfig() .withValue("baseDirectory", ConfigValueFactory.fromAnyRef(baseDir.toString())) .withFallback(ConfigFactory.parseResources("reference.conf")) .resolve() @@ -207,10 +47,9 @@ class NodeConfigTest { assertEquals(myLegalName, fullConfig.myLegalName) assertEquals(localPort(40002), fullConfig.rpcAddress) assertEquals(localPort(10001), fullConfig.p2pAddress) - assertEquals(listOf("my.service"), fullConfig.extraAdvertisedServiceIds) assertEquals(listOf(user("jenny")), fullConfig.rpcUsers) assertEquals(NetworkMapInfo(localPort(12345), DUMMY_NOTARY.name), fullConfig.networkMapService) - assertTrue((fullConfig.dataSourceProperties["dataSource.url"] as String).contains("AUTO_SERVER_PORT=30001")) + assertThat(fullConfig.dataSourceProperties["dataSource.url"] as String).contains("AUTO_SERVER_PORT=30001") assertTrue(fullConfig.useTestClock) assertFalse(fullConfig.detectPublicIp) } @@ -222,13 +61,13 @@ class NodeConfigTest { p2pPort = 10001, rpcPort = 40002, webPort = 20001, - h2Port = 30001, - services = mutableListOf("my.service"), + h2port = 30001, + notary = NotaryService(validating = false), + networkMap = NetworkMapConfig(DUMMY_NOTARY.name, localPort(12345)), users = listOf(user("jenny")) ) - config.networkMap = NetworkMapConfig(DUMMY_NOTARY.name, 12345) - val nodeConfig = config.toFileConfig() + val nodeConfig = config.toConfig() .withValue("baseDirectory", ConfigValueFactory.fromAnyRef(baseDir.toString())) .withFallback(ConfigFactory.parseResources("web-reference.conf")) .resolve() @@ -240,35 +79,27 @@ class NodeConfigTest { assertEquals("cordacadevpass", webConfig.keyStorePassword) } - @Test - fun `test moving`() { - val config = createConfig(legalName = myLegalName) - - val elsewhere = baseDir / "elsewhere" - val moved = config.moveTo(elsewhere) - assertEquals(elsewhere / "myname", moved.nodeDir) - assertEquals(elsewhere / "myname-explorer", moved.explorerDir) - assertEquals(elsewhere / "myname" / "plugins", moved.pluginDir) - } - private fun createConfig( legalName: CordaX500Name = CordaX500Name(organisation = "Unknown", locality = "Nowhere", country = "GB"), p2pPort: Int = -1, rpcPort: Int = -1, webPort: Int = -1, - h2Port: Int = -1, - services: MutableList = mutableListOf("extra.service"), + h2port: Int = -1, + notary: NotaryService?, + networkMap: NetworkMapConfig?, users: List = listOf(user("guest")) - ) = NodeConfig( - baseDir, - legalName = legalName, - p2pPort = p2pPort, - rpcPort = rpcPort, - webPort = webPort, - h2Port = h2Port, - extraServices = services, - users = users - ) + ): NodeConfig { + return NodeConfig( + myLegalName = legalName, + p2pAddress = localPort(p2pPort), + rpcAddress = localPort(rpcPort), + webAddress = localPort(webPort), + h2port = h2port, + notary = notary, + networkMapService = networkMap, + rpcUsers = users + ) + } private fun localPort(port: Int) = NetworkHostAndPort("localhost", port) } diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt index f6c73aba2d..bae103a317 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt @@ -1,6 +1,7 @@ package net.corda.demobench.model import net.corda.core.identity.CordaX500Name +import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.User import net.corda.testing.DUMMY_NOTARY import org.junit.Test @@ -51,7 +52,7 @@ class NodeControllerTest { fun `test first validated node becomes network map`() { val data = NodeData() data.legalName.value = node1Name - data.p2pPort.value = 100000 + data.p2pPort.value = 10000 assertFalse(controller.hasNetworkMap()) controller.validate(data) @@ -90,7 +91,7 @@ class NodeControllerTest { @Test fun `test register network map node`() { val config = createConfig(commonName = "Organisation is Network Map") - assertTrue(config.isNetworkMap()) + assertTrue(config.nodeConfig.isNetworkMap) assertFalse(controller.hasNetworkMap()) controller.register(config) @@ -99,9 +100,10 @@ class NodeControllerTest { @Test fun `test register non-network-map node`() { - val config = createConfig(commonName = "Organisation is not Network Map") - config.networkMap = NetworkMapConfig(DUMMY_NOTARY.name, 10000) - assertFalse(config.isNetworkMap()) + val config = createConfig( + commonName = "Organisation is not Network Map", + networkMap = NetworkMapConfig(DUMMY_NOTARY.name, localPort(10000))) + assertFalse(config.nodeConfig.isNetworkMap) assertFalse(controller.hasNetworkMap()) controller.register(config) @@ -146,7 +148,7 @@ class NodeControllerTest { @Test fun `test H2 port is max`() { val portNumber = NodeController.firstPort + 3478 - val config = createConfig(h2Port = portNumber) + val config = createConfig(h2port = portNumber) assertEquals(NodeController.firstPort, controller.nextPort) controller.register(config) assertEquals(portNumber + 1, controller.nextPort) @@ -166,25 +168,30 @@ class NodeControllerTest { private fun createConfig( commonName: String = "Unknown", - p2pPort: Int = -1, - rpcPort: Int = -1, - webPort: Int = -1, - h2Port: Int = -1, - services: MutableList = mutableListOf("extra.service"), + p2pPort: Int = 0, + rpcPort: Int = 0, + webPort: Int = 0, + h2port: Int = 0, + notary: NotaryService? = null, + networkMap: NetworkMapConfig? = null, users: List = listOf(user("guest")) - ) = NodeConfig( - baseDir, - legalName = CordaX500Name( - organisation = commonName, - locality = "New York", - country = "US" - ), - p2pPort = p2pPort, - rpcPort = rpcPort, - webPort = webPort, - h2Port = h2Port, - extraServices = services, - users = users - ) + ): NodeConfigWrapper { + val nodeConfig = NodeConfig( + myLegalName = CordaX500Name( + organisation = commonName, + locality = "New York", + country = "US" + ), + p2pAddress = localPort(p2pPort), + rpcAddress = localPort(rpcPort), + webAddress = localPort(webPort), + h2port = h2port, + notary = notary, + networkMapService = networkMap, + rpcUsers = users + ) + return NodeConfigWrapper(baseDir, nodeConfig) + } + private fun localPort(port: Int) = NetworkHostAndPort("localhost", port) } diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/ServiceControllerTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/ServiceControllerTest.kt deleted file mode 100644 index f80f13b165..0000000000 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/ServiceControllerTest.kt +++ /dev/null @@ -1,44 +0,0 @@ -package net.corda.demobench.model - -import org.junit.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue - -class ServiceControllerTest { - - @Test - fun `test empty`() { - val controller = ServiceController("/empty-services.conf") - assertNotNull(controller.services) - assertTrue(controller.services.isEmpty()) - - assertNotNull(controller.notaries) - assertTrue(controller.notaries.isEmpty()) - } - - @Test - fun `test duplicates`() { - val controller = ServiceController("/duplicate-services.conf") - assertNotNull(controller.services) - assertEquals(listOf("corda.example"), controller.services.map { it.value }) - } - - @Test - fun `test notaries`() { - val controller = ServiceController("/notary-services.conf") - assertNotNull(controller.notaries) - assertEquals(listOf("corda.notary.simple"), controller.notaries.map { it.value }) - } - - @Test - fun `test services`() { - val controller = ServiceController() - assertNotNull(controller.services) - assertTrue(controller.services.isNotEmpty()) - - assertNotNull(controller.notaries) - assertTrue(controller.notaries.isNotEmpty()) - } - -} \ No newline at end of file diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/UserTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/UserTest.kt deleted file mode 100644 index 5376c0915e..0000000000 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/UserTest.kt +++ /dev/null @@ -1,47 +0,0 @@ -package net.corda.demobench.model - -import net.corda.nodeapi.User -import org.junit.Test -import kotlin.test.assertEquals - -class UserTest { - - @Test - fun createFromEmptyMap() { - val user = toUser(emptyMap()) - assertEquals("none", user.username) - assertEquals("none", user.password) - assertEquals(emptySet(), user.permissions) - } - - @Test - fun createFromMap() { - val map = mapOf( - "username" to "MyName", - "password" to "MyPassword", - "permissions" to listOf("Flow.MyFlow") - ) - val user = toUser(map) - assertEquals("MyName", user.username) - assertEquals("MyPassword", user.password) - assertEquals(setOf("Flow.MyFlow"), user.permissions) - } - - @Test - fun userToMap() { - val user = User("MyName", "MyPassword", setOf("Flow.MyFlow")) - val map = user.toMap() - assertEquals("MyName", map["username"]) - assertEquals("MyPassword", map["password"]) - assertEquals(listOf("Flow.MyFlow"), map["permissions"]) - } - - @Test - fun `default user`() { - val user = user("guest") - assertEquals("guest", user.username) - assertEquals("letmein", user.password) - assertEquals(setOf("ALL"), user.permissions) - } - -} diff --git a/tools/demobench/src/test/resources/duplicate-services.conf b/tools/demobench/src/test/resources/duplicate-services.conf deleted file mode 100644 index f7faf80aed..0000000000 --- a/tools/demobench/src/test/resources/duplicate-services.conf +++ /dev/null @@ -1,3 +0,0 @@ -corda.example : Example -corda.example : Example -corda.example : Example diff --git a/tools/demobench/src/test/resources/empty-services.conf b/tools/demobench/src/test/resources/empty-services.conf deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tools/demobench/src/test/resources/notary-services.conf b/tools/demobench/src/test/resources/notary-services.conf deleted file mode 100644 index 79c6835073..0000000000 --- a/tools/demobench/src/test/resources/notary-services.conf +++ /dev/null @@ -1,2 +0,0 @@ -corda.notary.simple : Notary Simple -corda.example : Example diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt index 5dbb09753c..d22e628596 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -22,8 +22,6 @@ import net.corda.finance.flows.CashExitFlow.ExitRequest import net.corda.finance.flows.CashIssueAndPaymentFlow.IssueAndPaymentRequest import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.nodeapi.User -import net.corda.nodeapi.internal.ServiceInfo -import net.corda.nodeapi.internal.ServiceType import net.corda.testing.ALICE import net.corda.testing.BOB import net.corda.testing.DUMMY_NOTARY @@ -71,19 +69,19 @@ class ExplorerSimulation(val options: OptionSet) { // TODO : Supported flow should be exposed somehow from the node instead of set of ServiceInfo. val notary = startNotaryNode(DUMMY_NOTARY.name, customOverrides = mapOf("nearestCity" to "Zurich"), validating = false) val alice = startNode(providedName = ALICE.name, rpcUsers = arrayListOf(user), - advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))), customOverrides = mapOf("nearestCity" to "Milan")) val bob = startNode(providedName = BOB.name, rpcUsers = arrayListOf(user), - advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))), customOverrides = mapOf("nearestCity" to "Madrid")) val ukBankName = CordaX500Name(organisation = "UK Bank Plc", locality = "London", country = "GB") val usaBankName = CordaX500Name(organisation = "USA Bank Corp", locality = "New York", country = "US") val issuerGBP = startNode(providedName = ukBankName, rpcUsers = arrayListOf(manager), - advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.GBP"))), - customOverrides = mapOf("nearestCity" to "London")) + customOverrides = mapOf( + "issuableCurrencies" to listOf("GBP"), + "nearestCity" to "London")) val issuerUSD = startNode(providedName = usaBankName, rpcUsers = arrayListOf(manager), - advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.USD"))), - customOverrides = mapOf("nearestCity" to "New York")) + customOverrides = mapOf( + "issuableCurrencies" to listOf("USD"), + "nearestCity" to "New York")) notaryNode = notary.get() aliceNode = alice.get() diff --git a/tools/explorer/src/test/kotlin/net/corda/explorer/model/IssuerModelTest.kt b/tools/explorer/src/test/kotlin/net/corda/explorer/model/IssuerModelTest.kt deleted file mode 100644 index a363e8cd3f..0000000000 --- a/tools/explorer/src/test/kotlin/net/corda/explorer/model/IssuerModelTest.kt +++ /dev/null @@ -1,27 +0,0 @@ -package net.corda.explorer.model - -import net.corda.finance.USD -import org.junit.Test -import java.util.* -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class IssuerModelTest { - @Test - fun `test issuer regex`() { - val regex = Regex("corda.issuer.(USD|GBP|CHF)") - assertTrue("corda.issuer.USD".matches(regex)) - assertTrue("corda.issuer.GBP".matches(regex)) - - assertFalse("corda.issuer.USD.GBP".matches(regex)) - assertFalse("corda.issuer.EUR".matches(regex)) - assertFalse("corda.issuer".matches(regex)) - - assertEquals(USD, Currency.getInstance("corda.issuer.USD".substringAfterLast("."))) - assertFailsWith(IllegalArgumentException::class) { - Currency.getInstance("corda.issuer.DOLLAR".substringBeforeLast(".")) - } - } -} \ No newline at end of file From 8c9045bd733012fbb995c81cf5763dae61e19272 Mon Sep 17 00:00:00 2001 From: szymonsztuka Date: Tue, 10 Oct 2017 15:03:05 +0100 Subject: [PATCH 146/185] deployNodes Gradle task appends properties from an optional file to node.conf (#1444) * deployNodes Gradle task appends properties from an optional file to node.conf --- docs/source/changelog.rst | 8 ++++++- .../java/net/corda/cordform/CordformNode.java | 9 +++++++ .../main/groovy/net/corda/plugins/Node.groovy | 24 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index c6bffa9c85..7f1e76f915 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -10,7 +10,7 @@ UNRELEASED * The ``Cordformation`` gradle plugin has been split into ``cordformation`` and ``cordapp``. The former builds and deploys nodes for development and testing, the latter turns a project into a cordapp project that generates JARs in - the standard CorDapp format. + the standard CorDapp format. * ``Cordform`` and node identity generation * Cordform may not specify a value for ``NetworkMap``, when that happens, during the task execution the following happens: @@ -30,6 +30,12 @@ UNRELEASED ``notaryNodeAddress``, ``notaryClusterAddresses`` and ``bftSMaRt`` have also been removed and replaced by a single ``notary`` config object. See :doc:`corda-configuration-file` for more details. +* Gradle task ``deployNodes`` can have an additional parameter `configFile` with the path to a properties file + to be appended to node.conf. + +* Cordformation node building DSL can have an additional parameter `configFile` with the path to a properties file + to be appended to node.conf. + .. _changelog_v1: Release 1.0 diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java index bca75347d8..7421fee251 100644 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java +++ b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java @@ -74,4 +74,13 @@ public class CordformNode implements NodeDefinition { public void rpcPort(Integer rpcPort) { config = config.withValue("rpcAddress", ConfigValueFactory.fromAnyRef(DEFAULT_HOST + ':' + rpcPort)); } + + /** + * Set the path to a file with optional properties, which are appended to the generated node.conf file. + * + * @param configFile The file path. + */ + public void configFile(String configFile) { + config = config.withValue("configFile", ConfigValueFactory.fromAnyRef(configFile)); + } } diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy index f4169bf456..f6963541d4 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy @@ -107,6 +107,7 @@ class Node extends CordformNode { installBuiltPlugin() installCordapps() installConfig() + appendOptionalConfig() } /** @@ -193,6 +194,29 @@ class Node extends CordformNode { } } + /** + * Appends installed config file with properties from an optional file. + */ + private void appendOptionalConfig() { + final configFileProperty = "configFile" + File optionalConfig + if (project.findProperty(configFileProperty)) { //provided by -PconfigFile command line property when running Gradle task + optionalConfig = new File(project.findProperty(configFileProperty)) + } else if (config.hasPath(configFileProperty)) { + optionalConfig = new File(config.getString(configFileProperty)) + } + if (optionalConfig) { + if (!optionalConfig.exists()) { + println "$configFileProperty '$optionalConfig' not found" + } else { + def confFile = new File(project.buildDir.getPath() + "/../" + nodeDir, 'node.conf') + optionalConfig.withInputStream { + input -> confFile << input + } + } + } + } + /** * Find the corda JAR amongst the dependencies. * From 3bee830604f0a808744f23a1316cd13d8c87c33b Mon Sep 17 00:00:00 2001 From: Alberto Arri <30873160+al-r3@users.noreply.github.com> Date: Tue, 10 Oct 2017 15:45:42 +0100 Subject: [PATCH 147/185] =?UTF-8?q?[CORDA-442]=20Removed=20the=20NetworkMa?= =?UTF-8?q?p=20option=20from=20Cordform,=20changed=20all=20the=20examples?= =?UTF-8?q?=E2=80=A6=20(#1827)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [CORDA-442] Removed the option for Cordformation to specify a networkMap. All the samples have been migrated to not specify a networkMap. --- build.gradle | 1 - docs/source/changelog.rst | 4 +- docs/source/deploying-a-node.rst | 18 ++++---- docs/source/example-code/build.gradle | 1 - docs/source/hello-world-running.rst | 2 +- docs/source/tutorial-cordapp.rst | 7 +--- .../corda/cordform/CordformDefinition.java | 4 +- .../groovy/net/corda/plugins/Cordform.groovy | 42 +++---------------- samples/attachment-demo/build.gradle | 1 - samples/bank-of-corda-demo/build.gradle | 2 - samples/irs-demo/build.gradle | 1 - .../net/corda/notarydemo/BFTNotaryCordform.kt | 8 +++- .../corda/notarydemo/RaftNotaryCordform.kt | 2 +- .../corda/notarydemo/SingleNotaryCordform.kt | 8 +++- samples/simm-valuation-demo/build.gradle | 1 - samples/trader-demo/build.gradle | 2 - .../testing/internal/demorun/DemoRunner.kt | 3 -- 17 files changed, 35 insertions(+), 72 deletions(-) diff --git a/build.gradle b/build.gradle index f8a21b7789..31740da52f 100644 --- a/build.gradle +++ b/build.gradle @@ -231,7 +231,6 @@ tasks.withType(Test) { task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "O=Controller,OU=corda,L=London,C=GB" node { name "O=Controller,OU=corda,L=London,C=GB" notary = [validating : true] diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 7f1e76f915..9f989db79a 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -12,8 +12,8 @@ UNRELEASED deploys nodes for development and testing, the latter turns a project into a cordapp project that generates JARs in the standard CorDapp format. -* ``Cordform`` and node identity generation - * Cordform may not specify a value for ``NetworkMap``, when that happens, during the task execution the following happens: +* ``Cordform`` and node identity generation: + * Removed the parameter ``NetworkMap`` from Cordform. Now at the end of the deployment the following happens: 1. Each node is started and its signed serialized NodeInfo is written to disk in the node base directory. 2. Every serialized ``NodeInfo`` above is copied in every other node "additional-node-info" folder under the NodeInfo folder. diff --git a/docs/source/deploying-a-node.rst b/docs/source/deploying-a-node.rst index 4457b7d38c..4125947fd8 100644 --- a/docs/source/deploying-a-node.rst +++ b/docs/source/deploying-a-node.rst @@ -11,13 +11,12 @@ Cordform is the local node deployment system for CorDapps. The nodes generated a debugging, and testing node configurations, but not for production or testnet deployment. Here is an example Gradle task called ``deployNodes`` that uses the Cordform plugin to deploy three nodes, plus a -notary/network map node: +notary node: .. sourcecode:: groovy task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "O=Controller,OU=corda,L=London,C=UK" node { name "O=Controller,OU=corda,L=London,C=UK" notary = [validating : true] @@ -52,9 +51,7 @@ notary/network map node: } } -You can extend ``deployNodes`` to generate any number of nodes you like. The only requirement is that you must specify -one node as running the network map service, by putting their name in the ``networkMap`` field. In our example, the -``Controller`` is set as the network map service. +You can extend ``deployNodes`` to generate any number of nodes you like. .. warning:: When adding nodes, make sure that there are no port clashes! @@ -85,9 +82,14 @@ run all the nodes at once. Each node in the ``nodes`` folder has the following s .. sourcecode:: none . nodeName - ├── corda.jar // The Corda runtime - ├── node.conf // The node's configuration - └── plugins // Any installed CorDapps + ├── corda.jar // The Corda runtime + ├── node.conf // The node's configuration + ├── plugins // Any installed CorDapps + └── additional-node-infos // Directory containing all the addresses and certificates of the other nodes. + +.. note:: During the build process each node generates a NodeInfo file which is written in its own root directory, +the plug-in proceeds and copies each node NodeInfo to every other node ``additional-node-infos`` directory. +The NodeInfo file contains a node hostname and port, legal name and security certificate. .. note:: Outside of development environments, do not store your node directories in the build folder. diff --git a/docs/source/example-code/build.gradle b/docs/source/example-code/build.gradle index a3ea77f4d8..0568552add 100644 --- a/docs/source/example-code/build.gradle +++ b/docs/source/example-code/build.gradle @@ -73,7 +73,6 @@ task integrationTest(type: Test) { task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "O=Notary Service,OU=corda,L=London,C=GB" node { name "O=Notary Service,OU=corda,L=London,C=GB" notary = [validating : true] diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index ae1592fa82..b5bf916e18 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -25,7 +25,6 @@ Let's take a look at the nodes we're going to deploy. Open the project's ``build task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "O=Controller,L=London,C=GB" node { name "O=Controller,L=London,C=GB" notary = [validating : true] @@ -82,6 +81,7 @@ the three node folders. Each node folder has the following structure: |____corda-webserver.jar // The node's webserver |____dependencies |____node.conf // The node's configuration file + |____additional-node-infos/ // Directory containing all the other nodes' addresses and identities |____plugins |____java/kotlin-source-0.1.jar // Our IOU CorDapp diff --git a/docs/source/tutorial-cordapp.rst b/docs/source/tutorial-cordapp.rst index ed203a66e5..c61e20f4be 100644 --- a/docs/source/tutorial-cordapp.rst +++ b/docs/source/tutorial-cordapp.rst @@ -17,7 +17,7 @@ if: We will deploy the CorDapp on 4 test nodes: -* **Controller**, which hosts the network map service and a validating notary service +* **Controller**, which hosts a validating notary service * **PartyA** * **PartyB** * **PartyC** @@ -276,7 +276,7 @@ IntelliJ The node driver defined in ``/src/test/kotlin/com/example/Main.kt`` allows you to specify how many nodes you would like to run and the configuration settings for each node. For the example CorDapp, the driver starts up four nodes - and adds an RPC user for all but the "Controller" node (which serves as the notary and network map service): + and adds an RPC user for all but the "Controller" node (which serves as the notary): .. sourcecode:: kotlin @@ -489,9 +489,6 @@ You must now edit the configuration file for each node, including the controller and make the following changes: * Change the Artemis messaging address to the machine's IP address (e.g. ``p2pAddress="10.18.0.166:10006"``) -* Change the network map service's address to the IP address of the machine where the controller node is running - (e.g. ``networkMapService { address="10.18.0.166:10002" legalName="O=Controller,L=London,C=GB" ``). The controller - will not have the ``networkMapService`` configuration entry After starting each node, the nodes will be able to see one another and agree IOUs among themselves. diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java index 51d44def93..047d7e6289 100644 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java +++ b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java @@ -7,11 +7,9 @@ import java.util.function.Consumer; public abstract class CordformDefinition { public final Path driverDirectory; public final ArrayList> nodeConfigurers = new ArrayList<>(); - public final String networkMapNodeName; - public CordformDefinition(Path driverDirectory, String networkMapNodeName) { + public CordformDefinition(Path driverDirectory) { this.driverDirectory = driverDirectory; - this.networkMapNodeName = networkMapNodeName; } public void addNode(Consumer configurer) { diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordform.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordform.groovy index 7bfd9e0f0d..7baf20a45a 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordform.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordform.groovy @@ -3,9 +3,7 @@ package net.corda.plugins import static org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME import net.corda.cordform.CordformContext import net.corda.cordform.CordformDefinition -import net.corda.cordform.CordformNode import org.apache.tools.ant.filters.FixCrLfFilter -import org.bouncycastle.asn1.x500.X500Name import org.gradle.api.DefaultTask import org.gradle.api.plugins.JavaPluginConvention import org.gradle.api.tasks.TaskAction @@ -24,7 +22,6 @@ class Cordform extends DefaultTask { String definitionClass protected def directory = Paths.get("build", "nodes") private def nodes = new ArrayList() - protected String networkMapNodeName /** * Set the directory to install nodes into. @@ -36,16 +33,6 @@ class Cordform extends DefaultTask { this.directory = Paths.get(directory) } - /** - * Set the network map node. - * - * @warning Ensure the node name is one of the configured nodes. - * @param nodeName The name of the node that will host the network map. - */ - void networkMap(String nodeName) { - networkMapNodeName = nodeName - } - /** * Add a node configuration. * @@ -110,12 +97,15 @@ class Cordform extends DefaultTask { */ @TaskAction void build() { - String networkMapNodeName = initializeConfigurationAndGetNetworkMapNodeName() + initializeConfiguration() installRunScript() - finalizeConfiguration(networkMapNodeName) + nodes.each { + it.build() + } + generateNodeInfos() } - private initializeConfigurationAndGetNetworkMapNodeName() { + private initializeConfiguration() { if (null != definitionClass) { def cd = loadCordformDefinition() cd.nodeConfigurers.each { nc -> @@ -129,30 +119,10 @@ class Cordform extends DefaultTask { project.projectDir.toPath().resolve(getNodeByName(nodeName).nodeDir.toPath()) } } - return cd.networkMapNodeName.toString() } else { nodes.each { it.rootDir directory } - return this.networkMapNodeName - } - } - - private finalizeConfiguration(String networkMapNodeName) { - Node networkMapNode = getNodeByName(networkMapNodeName) - if (networkMapNode == null) { - nodes.each { - it.build() - } - generateNodeInfos() - logger.info("Starting without networkMapNode, this an experimental feature") - } else { - nodes.each { - if (it != networkMapNode) { - it.networkMapAddress(networkMapNode.getP2PAddress(), networkMapNodeName) - } - it.build() - } } } diff --git a/samples/attachment-demo/build.gradle b/samples/attachment-demo/build.gradle index c331834529..b977d45ffb 100644 --- a/samples/attachment-demo/build.gradle +++ b/samples/attachment-demo/build.gradle @@ -38,7 +38,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { ext.rpcUsers = [['username': "demo", 'password': "demo", 'permissions': ["StartFlow.net.corda.attachmentdemo.AttachmentDemoFlow"]]] directory "./build/nodes" - networkMap "O=Notary Service,L=Zurich,C=CH" node { name "O=Notary Service,L=Zurich,C=CH" notary = [validating : true] diff --git a/samples/bank-of-corda-demo/build.gradle b/samples/bank-of-corda-demo/build.gradle index da04426168..be0c49807d 100644 --- a/samples/bank-of-corda-demo/build.gradle +++ b/samples/bank-of-corda-demo/build.gradle @@ -50,8 +50,6 @@ dependencies { task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" // This name "Notary" is hard-coded into BankOfCordaClientApi so if you change it here, change it there too. - // In this demo the node that runs a standalone notary also acts as the network map server. - networkMap "O=Notary Service,L=Zurich,C=CH" node { name "O=Notary Service,L=Zurich,C=CH" notary = [validating : true] diff --git a/samples/irs-demo/build.gradle b/samples/irs-demo/build.gradle index 2341793e1f..abbfaf3772 100644 --- a/samples/irs-demo/build.gradle +++ b/samples/irs-demo/build.gradle @@ -51,7 +51,6 @@ dependencies { task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "O=Notary Service,L=Zurich,C=CH" node { name "O=Notary Service,L=Zurich,C=CH" notary = [validating : true] diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt index 477adecd20..92669b5714 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt @@ -13,7 +13,11 @@ import net.corda.node.services.transactions.minCorrectReplicas import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.testing.ALICE import net.corda.testing.BOB -import net.corda.testing.internal.demorun.* +import net.corda.testing.internal.demorun.name +import net.corda.testing.internal.demorun.node +import net.corda.testing.internal.demorun.notary +import net.corda.testing.internal.demorun.rpcUsers +import net.corda.testing.internal.demorun.runNodes fun main(args: Array) = BFTNotaryCordform.runNodes() @@ -22,7 +26,7 @@ private val notaryNames = createNotaryNames(clusterSize) // This is not the intended final design for how to use CordformDefinition, please treat this as experimental and DO // NOT use this as a design to copy. -object BFTNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", notaryNames[0].toString()) { +object BFTNotaryCordform : CordformDefinition("build" / "notary-demo-nodes") { private val clusterName = CordaX500Name(BFTNonValidatingNotaryService.id, "BFT", "Zurich", "CH") init { diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt index 1d78c51266..198867dcf1 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt @@ -22,7 +22,7 @@ private val notaryNames = createNotaryNames(3) // This is not the intended final design for how to use CordformDefinition, please treat this as experimental and DO // NOT use this as a design to copy. -object RaftNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", notaryNames[0].toString()) { +object RaftNotaryCordform : CordformDefinition("build" / "notary-demo-nodes") { private val clusterName = CordaX500Name(RaftValidatingNotaryService.id, "Raft", "Zurich", "CH") init { diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt index beec66a8fc..22cc63540d 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt @@ -11,7 +11,11 @@ import net.corda.notarydemo.flows.RPCStartableNotaryFlowClient import net.corda.testing.ALICE import net.corda.testing.BOB import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.internal.demorun.* +import net.corda.testing.internal.demorun.name +import net.corda.testing.internal.demorun.node +import net.corda.testing.internal.demorun.notary +import net.corda.testing.internal.demorun.rpcUsers +import net.corda.testing.internal.demorun.runNodes fun main(args: Array) = SingleNotaryCordform.runNodes() @@ -19,7 +23,7 @@ val notaryDemoUser = User("demou", "demop", setOf(startFlowPermission Date: Tue, 10 Oct 2017 15:50:23 +0100 Subject: [PATCH 148/185] Bumped gradle plugins to 2.0.2 to publish latest changes. --- constants.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.properties b/constants.properties index 4591e03dea..f0fbe73c08 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=2.0.1 +gradlePluginsVersion=2.0.2 kotlinVersion=1.1.50 guavaVersion=21.0 bouncycastleVersion=1.57 From f19ff141dd780143a4069a0b78e3f91807b24fa0 Mon Sep 17 00:00:00 2001 From: Matthew Nesbit Date: Mon, 9 Oct 2017 10:44:32 +0100 Subject: [PATCH 149/185] Update code to use AppServiceHub in services and support for services when using MockServices hub. --- docs/source/changelog.rst | 4 +++ .../net/corda/docs/CustomNotaryTutorial.kt | 4 +-- .../kotlin/net/corda/docs/CustomVaultQuery.kt | 6 ++-- docs/source/tutorial-custom-notary.rst | 2 +- .../net/corda/irs/api/NodeInterestRates.kt | 5 ++- .../corda/irs/api/NodeInterestRatesTest.kt | 4 ++- .../net/corda/testing/node/MockServices.kt | 31 ++++++++++++++++++- 7 files changed, 45 insertions(+), 11 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 9f989db79a..8b17db03ac 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -36,6 +36,10 @@ UNRELEASED * Cordformation node building DSL can have an additional parameter `configFile` with the path to a properties file to be appended to node.conf. +* ``CordaService`` annotated classes should be upgraded to take a constructor parameter of type ``AppServiceHub`` which + allows services to start flows marked with the ``StartableByService`` annotation. For backwards compatability + service classes with only ``ServiceHub`` constructors will still work. + .. _changelog_v1: Release 1.0 diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt index 8d1c0d0274..d7331d146c 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt @@ -4,7 +4,7 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.TransactionVerificationException import net.corda.core.flows.* -import net.corda.core.node.ServiceHub +import net.corda.core.node.AppServiceHub import net.corda.core.node.services.CordaService import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.TrustedAuthorityNotaryService @@ -15,7 +15,7 @@ import java.security.SignatureException // START 1 @CordaService -class MyCustomValidatingNotaryService(override val services: ServiceHub, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { +class MyCustomValidatingNotaryService(override val services: AppServiceHub, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { override val timeWindowChecker = TimeWindowChecker(services.clock) override val uniquenessProvider = PersistentUniquenessProvider() 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/CustomVaultQuery.kt index 47a5ca00a0..1ae88a6015 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/CustomVaultQuery.kt @@ -4,7 +4,7 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Amount import net.corda.core.flows.* import net.corda.core.identity.Party -import net.corda.core.node.ServiceHub +import net.corda.core.node.AppServiceHub import net.corda.core.node.services.CordaService import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken @@ -23,7 +23,7 @@ import java.util.* object CustomVaultQuery { @CordaService - class Service(val services: ServiceHub) : SingletonSerializeAsToken() { + class Service(val services: AppServiceHub) : SingletonSerializeAsToken() { private companion object { val log = loggerFor() } @@ -49,7 +49,7 @@ object CustomVaultQuery { val session = services.jdbcSession() val prepStatement = session.prepareStatement(nativeQuery) val rs = prepStatement.executeQuery() - var topUpLimits: MutableList> = mutableListOf() + val topUpLimits: MutableList> = mutableListOf() while (rs.next()) { val currencyStr = rs.getString(1) val amount = rs.getLong(2) diff --git a/docs/source/tutorial-custom-notary.rst b/docs/source/tutorial-custom-notary.rst index 01e5da1b71..cabefdd203 100644 --- a/docs/source/tutorial-custom-notary.rst +++ b/docs/source/tutorial-custom-notary.rst @@ -9,7 +9,7 @@ Writing a custom notary service Similarly to writing an oracle service, the first step is to create a service class in your CorDapp and annotate it with ``@CordaService``. The Corda node scans for any class with this annotation and initialises them. The only requirement -is that the class provide a constructor with a single parameter of type ``ServiceHub``. +is that the class provide a constructor with a single parameter of type ``AppServiceHub``. .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt :language: kotlin diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt index a28d299d24..579de8cde6 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt @@ -2,11 +2,10 @@ package net.corda.irs.api import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Command -import net.corda.core.crypto.MerkleTreeException import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.* import net.corda.core.internal.ThreadBox -import net.corda.core.node.ServiceHub +import net.corda.core.node.AppServiceHub import net.corda.core.node.services.CordaService import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.FilteredTransaction @@ -78,7 +77,7 @@ object NodeInterestRates { @ThreadSafe // DOCSTART 3 @CordaService - class Oracle(private val services: ServiceHub) : SingletonSerializeAsToken() { + class Oracle(private val services: AppServiceHub) : SingletonSerializeAsToken() { private val mutex = ThreadBox(InnerState()) init { diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index 38fc4e0b56..3af7044caa 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -24,6 +24,7 @@ import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties import net.corda.testing.node.MockServices.Companion.makeTestIdentityService +import net.corda.testing.node.createMockCordaService import org.junit.After import org.junit.Assert.* import org.junit.Before @@ -65,7 +66,8 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { setCordappPackages("net.corda.finance.contracts") database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) database.transaction { - oracle = NodeInterestRates.Oracle(services).apply { knownFixes = TEST_DATA } + oracle = createMockCordaService(services, NodeInterestRates::Oracle) + oracle.knownFixes = TEST_DATA } } 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 3d49484fef..ae838119a2 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 @@ -1,10 +1,15 @@ package net.corda.testing.node +import com.google.common.collect.MutableClassToInstanceMap import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.* +import net.corda.core.flows.FlowLogic import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.PartyAndCertificate import net.corda.core.messaging.DataFeed +import net.corda.core.messaging.FlowHandle +import net.corda.core.messaging.FlowProgressHandle +import net.corda.core.node.AppServiceHub import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub import net.corda.core.node.StateLoader @@ -173,7 +178,11 @@ open class MockServices( return vaultService } - override fun cordaService(type: Class): T = throw IllegalArgumentException("${type.name} not found") + val cordappServices = MutableClassToInstanceMap.create() + override fun cordaService(type: Class): T { + require(type.isAnnotationPresent(CordaService::class.java)) { "${type.name} is not a Corda service" } + return cordappServices.getInstance(type) ?: throw IllegalArgumentException("Corda service ${type.name} does not exist") + } override fun jdbcSession(): Connection = throw UnsupportedOperationException() } @@ -244,3 +253,23 @@ open class MockTransactionStorage : WritableTransactionStorage, SingletonSeriali override fun getTransaction(id: SecureHash): SignedTransaction? = txns[id] } + +fun createMockCordaService(serviceHub: MockServices, serviceConstructor: (AppServiceHub) -> T): T { + class MockAppServiceHubImpl(val serviceHub: MockServices, serviceConstructor: (AppServiceHub) -> T) : AppServiceHub, ServiceHub by serviceHub { + val serviceInstance: T + + init { + serviceInstance = serviceConstructor(this) + serviceHub.cordappServices.putInstance(serviceInstance.javaClass, serviceInstance) + } + + override fun startFlow(flow: FlowLogic): FlowHandle { + throw UnsupportedOperationException() + } + + override fun startTrackedFlow(flow: FlowLogic): FlowProgressHandle { + throw UnsupportedOperationException() + } + } + return MockAppServiceHubImpl(serviceHub, serviceConstructor).serviceInstance +} \ No newline at end of file From 16b26970a9f84c002df46e0b0c1ce76393022327 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Wed, 11 Oct 2017 09:54:19 +0100 Subject: [PATCH 150/185] Add kryo-hook javaagent --- experimental/kryo-hook/build.gradle | 53 ++++++ .../kotlin/net/corda/kryohook/KryoHook.kt | 159 ++++++++++++++++++ 2 files changed, 212 insertions(+) create mode 100644 experimental/kryo-hook/build.gradle create mode 100644 experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/KryoHook.kt diff --git a/experimental/kryo-hook/build.gradle b/experimental/kryo-hook/build.gradle new file mode 100644 index 0000000000..cf52f3c9bb --- /dev/null +++ b/experimental/kryo-hook/build.gradle @@ -0,0 +1,53 @@ +buildscript { + // For sharing constants between builds + Properties constants = new Properties() + file("$projectDir/../../constants.properties").withInputStream { constants.load(it) } + + ext.kotlin_version = constants.getProperty("kotlinVersion") + ext.javaassist_version = "3.12.1.GA" + + repositories { + mavenLocal() + mavenCentral() + jcenter() + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +repositories { + mavenLocal() + mavenCentral() + jcenter() +} + +apply plugin: 'kotlin' +apply plugin: 'kotlin-kapt' +apply plugin: 'idea' + +description 'A javaagent to allow hooking into Kryo' + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + compile "javassist:javassist:$javaassist_version" + compile "com.esotericsoftware:kryo:4.0.0" + compile "co.paralleluniverse:quasar-core:$quasar_version:jdk8" +} + +jar { + archiveName = "${project.name}.jar" + manifest { + attributes( + 'Premain-Class': 'net.corda.kryohook.KryoHookAgent', + 'Can-Redefine-Classes': 'true', + 'Can-Retransform-Classes': 'true', + 'Can-Set-Native-Method-Prefix': 'true', + 'Implementation-Title': "KryoHook", + 'Implementation-Version': rootProject.version + ) + } + from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } +} diff --git a/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/KryoHook.kt b/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/KryoHook.kt new file mode 100644 index 0000000000..8d57dc8dbc --- /dev/null +++ b/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/KryoHook.kt @@ -0,0 +1,159 @@ +package net.corda.kryohook + +import co.paralleluniverse.strands.Strand +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Output +import javassist.ClassPool +import javassist.CtClass +import java.io.ByteArrayInputStream +import java.lang.StringBuilder +import java.lang.instrument.ClassFileTransformer +import java.lang.instrument.Instrumentation +import java.security.ProtectionDomain +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicInteger + +class KryoHookAgent { + companion object { + @JvmStatic + fun premain(argumentsString: String?, instrumentation: Instrumentation) { + Runtime.getRuntime().addShutdownHook(Thread { + val statsTrees = KryoHook.events.values.flatMap { + readTrees(it, 0).second + } + val builder = StringBuilder() + statsTrees.forEach { + prettyStatsTree(0, it, builder) + } + print(builder.toString()) + }) + instrumentation.addTransformer(KryoHook) + } + } +} + +fun prettyStatsTree(indent: Int, statsTree: StatsTree, builder: StringBuilder) { + when (statsTree) { + is StatsTree.Object -> { + builder.append(kotlin.CharArray(indent) { ' ' }) + builder.append(statsTree.className) + builder.append(" ") + builder.append(statsTree.size) + builder.append("\n") + for (child in statsTree.children) { + prettyStatsTree(indent + 2, child, builder) + } + } + } +} + +object KryoHook : ClassFileTransformer { + val classPool = ClassPool.getDefault() + + val hookClassName = javaClass.name + + override fun transform( + loader: ClassLoader?, + className: String, + classBeingRedefined: Class<*>?, + protectionDomain: ProtectionDomain?, + classfileBuffer: ByteArray + ): ByteArray? { + if (className.startsWith("java") || className.startsWith("javassist") || className.startsWith("kotlin")) { + return null + } + return try { + val clazz = classPool.makeClass(ByteArrayInputStream(classfileBuffer)) + instrumentClass(clazz)?.toBytecode() + } catch (throwable: Throwable) { + println("SOMETHING WENT WRONG") + throwable.printStackTrace(System.out) + null + } + } + + private fun instrumentClass(clazz: CtClass): CtClass? { + for (method in clazz.declaredBehaviors) { + if (method.name == "write") { + val parameterTypeNames = method.parameterTypes.map { it.name } + if (parameterTypeNames == listOf("com.esotericsoftware.kryo.Kryo", "com.esotericsoftware.kryo.io.Output", "java.lang.Object")) { + if (method.isEmpty) continue + println("Instrumenting ${clazz.name}") + method.insertBefore("$hookClassName.${this::writeEnter.name}($1, $2, $3);") + method.insertAfter("$hookClassName.${this::writeExit.name}($1, $2, $3);") + return clazz + } + } + } + return null + } + + val events = ConcurrentHashMap>() + val eventCount = AtomicInteger(0) + + @JvmStatic + fun writeEnter(kryo: Kryo, output: Output, obj: Any) { + events.getOrPut(Strand.currentStrand().id) { ArrayList() }.add( + StatsEvent.Enter(obj.javaClass.name, output.total()) + ) + if (eventCount.incrementAndGet() % 100 == 0) { + println("EVENT COUNT ${eventCount}") + } + } + @JvmStatic + fun writeExit(kryo: Kryo, output: Output, obj: Any) { + events.get(Strand.currentStrand().id)!!.add( + StatsEvent.Exit(obj.javaClass.name, output.total()) + ) + } +} + +sealed class StatsEvent { + data class Enter(val className: String, val offset: Long) : StatsEvent() + data class Exit(val className: String, val offset: Long) : StatsEvent() +} + +sealed class StatsTree { + data class Object( + val className: String, + val size: Long, + val children: List + ) : StatsTree() +} + + +fun readTree(events: List, index: Int): Pair { + val event = events[index] + when (event) { + is StatsEvent.Enter -> { + val (nextIndex, children) = readTrees(events, index + 1) + val exit = events[nextIndex] as StatsEvent.Exit + require(event.className == exit.className) + return Pair(nextIndex + 1, StatsTree.Object(event.className, exit.offset - event.offset, children)) + } + is StatsEvent.Exit -> { + throw IllegalStateException("Wasn't expecting Exit") + } + } +} + +fun readTrees(events: List, index: Int): Pair> { + val trees = ArrayList() + var i = index + while (true) { + val event = events.getOrNull(i) + when (event) { + is StatsEvent.Enter -> { + val (nextIndex, tree) = readTree(events, i) + trees.add(tree) + i = nextIndex + } + is StatsEvent.Exit -> { + return Pair(i, trees) + } + null -> { + return Pair(i, trees) + } + } + } +} From c56c9fd455542ebcf8dbb5b76b97bbcb7f453d25 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Wed, 11 Oct 2017 10:06:27 +0100 Subject: [PATCH 151/185] Add README.md, some docs --- .../kotlin/net/corda/kryohook/KryoHook.kt | 17 ++++++++++---- .../main/kotlin/net/corda/kryohook/README.md | 23 +++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/README.md diff --git a/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/KryoHook.kt b/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/KryoHook.kt index 8d57dc8dbc..e9c6f61e34 100644 --- a/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/KryoHook.kt +++ b/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/KryoHook.kt @@ -47,6 +47,12 @@ fun prettyStatsTree(indent: Int, statsTree: StatsTree, builder: StringBuilder) { } } +/** + * The hook simply records the write() entries and exits together with the output offset at the time of the call. + * This is recorded in a StrandID -> List map. + * + * Later we "parse" these lists into a tree. + */ object KryoHook : ClassFileTransformer { val classPool = ClassPool.getDefault() @@ -88,17 +94,14 @@ object KryoHook : ClassFileTransformer { return null } + // StrandID -> StatsEvent map val events = ConcurrentHashMap>() - val eventCount = AtomicInteger(0) @JvmStatic fun writeEnter(kryo: Kryo, output: Output, obj: Any) { events.getOrPut(Strand.currentStrand().id) { ArrayList() }.add( StatsEvent.Enter(obj.javaClass.name, output.total()) ) - if (eventCount.incrementAndGet() % 100 == 0) { - println("EVENT COUNT ${eventCount}") - } } @JvmStatic fun writeExit(kryo: Kryo, output: Output, obj: Any) { @@ -108,11 +111,17 @@ object KryoHook : ClassFileTransformer { } } +/** + * TODO we could add events on entries/exits to field serializers to get more info on what's being serialised. + */ sealed class StatsEvent { data class Enter(val className: String, val offset: Long) : StatsEvent() data class Exit(val className: String, val offset: Long) : StatsEvent() } +/** + * TODO add Field constructor. + */ sealed class StatsTree { data class Object( val className: String, diff --git a/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/README.md b/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/README.md new file mode 100644 index 0000000000..ec7899a290 --- /dev/null +++ b/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/README.md @@ -0,0 +1,23 @@ +What is this +------------ + +This is a javaagent that hooks into Kryo serializers to record a breakdown of how many bytes objects take in the output. + +The dump is quite ugly now, but the in-memory representation is a simple tree so we could put some nice visualisation on +top if we want. + +How do I run it +--------------- + +Build the agent: +``` +./gradlew experimental:kryo-hook:jar +``` + +Add this JVM flag to what you're running: + +``` +-javaagent:/experimental/kryo-hook/build/libs/kryo-hook.jar +``` + +The agent will dump the output when the JVM shuts down. From c2cda569e15ce300abbd023fe5c40bf1957eef21 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Wed, 11 Oct 2017 10:06:52 +0100 Subject: [PATCH 152/185] Add kryo-hook to settings.gradle --- settings.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/settings.gradle b/settings.gradle index a3b8270486..ec0d5adcc6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -18,6 +18,7 @@ include 'webserver:webcapsule' include 'experimental' include 'experimental:sandbox' include 'experimental:quasar-hook' +include 'experimental:kryo-hook' include 'verifier' include 'test-common' include 'test-utils' From b354d335a330b5668fd692fc0971228788bc5e89 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Wed, 11 Oct 2017 10:35:11 +0100 Subject: [PATCH 153/185] Remove extra blank line --- .../kryo-hook/src/main/kotlin/net/corda/kryohook/KryoHook.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/KryoHook.kt b/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/KryoHook.kt index e9c6f61e34..5afbd0e4ff 100644 --- a/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/KryoHook.kt +++ b/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/KryoHook.kt @@ -130,7 +130,6 @@ sealed class StatsTree { ) : StatsTree() } - fun readTree(events: List, index: Int): Pair { val event = events[index] when (event) { From 4ee250a19b04291e5d24dd5336ba5a008e856aa2 Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Wed, 11 Oct 2017 10:35:21 +0100 Subject: [PATCH 154/185] Retire setCordappPackages. (#1860) --- .../client/rpc/CordaRPCJavaClientTest.java | 8 ++--- .../client/rpc/BlacklistKotlinClosureTest.kt | 4 +-- .../corda/client/rpc/CordaRPCClientTest.kt | 6 +--- .../confidential/IdentitySyncFlowTests.kt | 4 +-- .../core/flows/CollectSignaturesFlowTests.kt | 10 +++--- .../core/flows/ContractUpgradeFlowTest.kt | 4 +-- .../net/corda/core/flows/FinalityFlowTests.kt | 4 +-- .../internal/ResolveTransactionsFlowTest.kt | 4 +-- .../net/corda/docs/CustomVaultQueryTest.kt | 4 +-- .../docs/FxTransactionBuildTutorialTest.kt | 4 +-- .../WorkflowTransactionBuildTutorialTest.kt | 6 +--- .../corda/finance/contracts/universal/Cap.kt | 22 ++++-------- .../finance/contracts/universal/Caplet.kt | 16 --------- .../contracts/universal/FXFwdTimeOption.kt | 16 --------- .../finance/contracts/universal/FXSwap.kt | 16 --------- .../corda/finance/contracts/universal/IRS.kt | 16 --------- .../contracts/universal/RollOutTests.kt | 15 -------- .../finance/contracts/universal/Swaption.kt | 16 --------- .../contracts/universal/ZeroCouponBond.kt | 16 --------- .../finance/contracts/CommercialPaperTests.kt | 1 - .../contracts/asset/ObligationTests.kt | 1 - .../corda/finance/flows/CashExitFlowTests.kt | 4 +-- .../corda/finance/flows/CashIssueFlowTests.kt | 5 +-- .../finance/flows/CashPaymentFlowTests.kt | 3 +- ...tachmentsClassLoaderStaticContractTests.kt | 6 ---- .../node/services/RaftNotaryServiceTests.kt | 14 +------- .../net/corda/node/internal/AbstractNode.kt | 16 +-------- .../kotlin/net/corda/node/internal/Node.kt | 14 ++++++-- .../node/internal/cordapp/CordappLoader.kt | 19 ++++------- .../services/vault/VaultQueryJavaTests.java | 7 ++-- .../net/corda/node/CordaRPCOpsImplTest.kt | 6 +--- .../corda/node/internal/CordaServiceTest.kt | 10 ++---- .../node/messaging/TwoPartyTradeFlowTests.kt | 34 ++++++++----------- .../corda/node/services/NotaryChangeTests.kt | 4 +-- .../events/NodeSchedulerServiceTest.kt | 4 +-- .../services/events/ScheduledFlowTests.kt | 4 +-- .../persistence/HibernateConfigurationTest.kt | 7 ++-- .../statemachine/FlowFrameworkTests.kt | 4 +-- .../transactions/NotaryServiceTests.kt | 4 +-- .../ValidatingNotaryServiceTests.kt | 4 +-- .../services/vault/NodeVaultServiceTest.kt | 16 +++++---- .../node/services/vault/VaultQueryTests.kt | 19 +++++------ .../node/services/vault/VaultWithCashTest.kt | 18 +++++----- .../corda/irs/api/NodeInterestRatesTest.kt | 5 +-- .../net/corda/netmap/simulation/Simulation.kt | 3 +- .../netmap/simulation/IRSSimulationTest.kt | 15 -------- .../net/corda/vega/SimmValuationTest.kt | 14 +------- .../net/corda/traderdemo/TraderDemoTest.kt | 15 +------- .../kotlin/net/corda/testing/NodeTestUtils.kt | 3 +- .../kotlin/net/corda/testing/driver/Driver.kt | 21 ++++++------ .../kotlin/net/corda/testing/node/MockNode.kt | 6 ++-- .../net/corda/testing/node/MockServices.kt | 5 ++- .../net/corda/testing/node/NodeBasedTest.kt | 6 ++-- .../kotlin/net/corda/testing/CoreTestUtils.kt | 16 --------- 54 files changed, 132 insertions(+), 392 deletions(-) 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 e596a61418..714b0d4316 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 @@ -28,11 +28,13 @@ import static kotlin.test.AssertionsKt.assertEquals; import static net.corda.finance.Currencies.DOLLARS; import static net.corda.finance.contracts.GetBalances.getCashBalance; import static net.corda.node.services.FlowPermissions.startFlowPermission; -import static net.corda.testing.CoreTestUtils.setCordappPackages; -import static net.corda.testing.CoreTestUtils.unsetCordappPackages; import static net.corda.testing.TestConstants.getALICE; public class CordaRPCJavaClientTest extends NodeBasedTest { + public CordaRPCJavaClientTest() { + super(Collections.singletonList("net.corda.finance.contracts")); + } + private List perms = Arrays.asList(startFlowPermission(CashPaymentFlow.class), startFlowPermission(CashIssueFlow.class)); private Set permSet = new HashSet<>(perms); private User rpcUser = new User("user1", "test", permSet); @@ -49,7 +51,6 @@ public class CordaRPCJavaClientTest extends NodeBasedTest { @Before public void setUp() throws ExecutionException, InterruptedException { - setCordappPackages("net.corda.finance.contracts"); CordaFuture> nodeFuture = startNotaryNode(getALICE().getName(), singletonList(rpcUser), true); node = nodeFuture.get(); node.getInternals().registerCustomSchemas(Collections.singleton(CashSchemaV1.INSTANCE)); @@ -59,7 +60,6 @@ public class CordaRPCJavaClientTest extends NodeBasedTest { @After public void done() throws IOException { connection.close(); - unsetCordappPackages(); } @Test diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/BlacklistKotlinClosureTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/BlacklistKotlinClosureTest.kt index 776b96f87f..89264f2e05 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/BlacklistKotlinClosureTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/BlacklistKotlinClosureTest.kt @@ -23,7 +23,7 @@ import org.junit.rules.ExpectedException @CordaSerializable data class Packet(val x: () -> Long) -class BlacklistKotlinClosureTest : NodeBasedTest() { +class BlacklistKotlinClosureTest : NodeBasedTest(listOf("net.corda.client.rpc")) { companion object { @Suppress("UNUSED") val logger = loggerFor() const val EVIL: Long = 666 @@ -66,7 +66,6 @@ class BlacklistKotlinClosureTest : NodeBasedTest() { @Before fun setUp() { - setCordappPackages("net.corda.client.rpc") aliceNode = startNode(ALICE.name, rpcUsers = listOf(rpcUser)).getOrThrow() bobNode = startNode(BOB.name, rpcUsers = listOf(rpcUser)).getOrThrow() bobNode.registerInitiatedFlow(RemoteFlowC::class.java) @@ -78,7 +77,6 @@ class BlacklistKotlinClosureTest : NodeBasedTest() { connection?.close() bobNode.internals.stop() aliceNode.internals.stop() - unsetCordappPackages() } @Test diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt index 422f08f8f3..be6f05715a 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt @@ -23,8 +23,6 @@ import net.corda.nodeapi.User import net.corda.testing.ALICE import net.corda.testing.chooseIdentity import net.corda.testing.node.NodeBasedTest -import net.corda.testing.setCordappPackages -import net.corda.testing.unsetCordappPackages import org.apache.activemq.artemis.api.core.ActiveMQSecurityException import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.After @@ -34,7 +32,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -class CordaRPCClientTest : NodeBasedTest() { +class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance.contracts")) { private val rpcUser = User("user1", "test", permissions = setOf( startFlowPermission(), startFlowPermission() @@ -49,7 +47,6 @@ class CordaRPCClientTest : NodeBasedTest() { @Before fun setUp() { - setCordappPackages("net.corda.finance.contracts") node = startNotaryNode(ALICE.name, rpcUsers = listOf(rpcUser)).getOrThrow() node.internals.registerCustomSchemas(setOf(CashSchemaV1)) client = CordaRPCClient(node.internals.configuration.rpcAddress!!) @@ -58,7 +55,6 @@ class CordaRPCClientTest : NodeBasedTest() { @After fun done() { connection?.close() - unsetCordappPackages() } @Test diff --git a/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt b/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt index 7482825440..ef276ef07e 100644 --- a/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt +++ b/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt @@ -28,15 +28,13 @@ class IdentitySyncFlowTests { @Before fun before() { - setCordappPackages("net.corda.finance.contracts.asset") // We run this in parallel threads to help catch any race conditions that may exist. - mockNet = MockNetwork(networkSendManuallyPumped = false, threadPerNode = true) + mockNet = MockNetwork(networkSendManuallyPumped = false, threadPerNode = true, cordappPackages = listOf("net.corda.finance.contracts.asset")) } @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test diff --git a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt index 619ddec8a6..984646750c 100644 --- a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -23,6 +23,10 @@ import kotlin.reflect.KClass import kotlin.test.assertFailsWith class CollectSignaturesFlowTests { + companion object { + private val cordappPackages = listOf("net.corda.testing.contracts") + } + lateinit var mockNet: MockNetwork lateinit var aliceNode: StartedNode lateinit var bobNode: StartedNode @@ -31,8 +35,7 @@ class CollectSignaturesFlowTests { @Before fun setup() { - setCordappPackages("net.corda.testing.contracts") - mockNet = MockNetwork() + mockNet = MockNetwork(cordappPackages = cordappPackages) val notaryNode = mockNet.createNotaryNode() aliceNode = mockNet.createPartyNode(ALICE.name) bobNode = mockNet.createPartyNode(BOB.name) @@ -45,7 +48,6 @@ class CollectSignaturesFlowTests { @After fun tearDown() { mockNet.stopNodes() - unsetCordappPackages() } private fun registerFlowOnAllNodes(flowClass: KClass>) { @@ -174,7 +176,7 @@ class CollectSignaturesFlowTests { @Test fun `fails when not signed by initiator`() { val onePartyDummyContract = DummyContract.generateInitial(1337, notary, aliceNode.info.chooseIdentity().ref(1)) - val miniCorpServices = MockServices(MINI_CORP_KEY) + val miniCorpServices = MockServices(cordappPackages, MINI_CORP_KEY) val ptx = miniCorpServices.signInitialTransaction(onePartyDummyContract) val flow = aliceNode.services.startFlow(CollectSignaturesFlow(ptx, emptySet())) mockNet.runNetwork() diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index 0720557b1f..782d3ee35f 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -40,8 +40,7 @@ class ContractUpgradeFlowTest { @Before fun setup() { - setCordappPackages("net.corda.testing.contracts", "net.corda.finance.contracts.asset", "net.corda.core.flows") - mockNet = MockNetwork() + mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts.asset", "net.corda.core.flows")) val notaryNode = mockNet.createNotaryNode() aliceNode = mockNet.createPartyNode(ALICE.name) bobNode = mockNet.createPartyNode(BOB.name) @@ -56,7 +55,6 @@ class ContractUpgradeFlowTest { @After fun tearDown() { mockNet.stopNodes() - unsetCordappPackages() } @Test diff --git a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt index fb865fd291..e17da0775b 100644 --- a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt @@ -23,8 +23,7 @@ class FinalityFlowTests { @Before fun setup() { - setCordappPackages("net.corda.finance.contracts.asset") - mockNet = MockNetwork() + mockNet = MockNetwork(cordappPackages = listOf("net.corda.finance.contracts.asset")) mockNet.createNotaryNode() aliceNode = mockNet.createPartyNode(ALICE.name) bobNode = mockNet.createPartyNode(BOB.name) @@ -36,7 +35,6 @@ class FinalityFlowTests { @After fun tearDown() { mockNet.stopNodes() - unsetCordappPackages() } @Test diff --git a/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt b/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt index bf16c93f53..e7f43c3096 100644 --- a/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt @@ -36,8 +36,7 @@ class ResolveTransactionsFlowTest { @Before fun setup() { - setCordappPackages("net.corda.testing.contracts") - mockNet = MockNetwork() + mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) notaryNode = mockNet.createNotaryNode() megaCorpNode = mockNet.createPartyNode(MEGA_CORP.name) miniCorpNode = mockNet.createPartyNode(MINI_CORP.name) @@ -52,7 +51,6 @@ class ResolveTransactionsFlowTest { @After fun tearDown() { mockNet.stopNodes() - unsetCordappPackages() } // DOCEND 3 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 d313217f40..847675db13 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 @@ -26,8 +26,7 @@ class CustomVaultQueryTest { @Before fun setup() { - setCordappPackages("net.corda.finance.contracts.asset") - mockNet = MockNetwork(threadPerNode = true) + mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance.contracts.asset")) mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) nodeA = mockNet.createPartyNode() nodeB = mockNet.createPartyNode() @@ -42,7 +41,6 @@ class CustomVaultQueryTest { @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test 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 be28ddbec0..97ddbc598c 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 @@ -24,8 +24,7 @@ class FxTransactionBuildTutorialTest { @Before fun setup() { - setCordappPackages("net.corda.finance.contracts.asset") - mockNet = MockNetwork(threadPerNode = true) + mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance.contracts.asset")) mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) nodeA = mockNet.createPartyNode() nodeB = mockNet.createPartyNode() @@ -38,7 +37,6 @@ class FxTransactionBuildTutorialTest { @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test 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 6b9f0d279d..089885d5f5 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 @@ -12,8 +12,6 @@ import net.corda.node.internal.StartedNode import net.corda.testing.DUMMY_NOTARY import net.corda.testing.chooseIdentity import net.corda.testing.node.MockNetwork -import net.corda.testing.setCordappPackages -import net.corda.testing.unsetCordappPackages import org.junit.After import org.junit.Before import org.junit.Test @@ -32,8 +30,7 @@ class WorkflowTransactionBuildTutorialTest { @Before fun setup() { - setCordappPackages("net.corda.docs") - mockNet = MockNetwork(threadPerNode = true) + mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.docs")) mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) nodeA = mockNet.createPartyNode() nodeB = mockNet.createPartyNode() @@ -43,7 +40,6 @@ class WorkflowTransactionBuildTutorialTest { @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Cap.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Cap.kt index 95c7e983fd..bedc953058 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Cap.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Cap.kt @@ -5,16 +5,18 @@ import net.corda.finance.contracts.FixOf import net.corda.finance.contracts.Frequency import net.corda.finance.contracts.Tenor import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before +import net.corda.testing.EnforceVerifyOrFail +import net.corda.testing.TransactionDSL +import net.corda.testing.TransactionDSLInterpreter import org.junit.Ignore import org.junit.Test import java.time.Instant import java.time.LocalDate +fun transaction(script: TransactionDSL.() -> EnforceVerifyOrFail) = run { + net.corda.testing.transaction(cordappPackages = listOf("net.corda.finance.contracts.universal"), dsl = script) +} + class Cap { val TEST_TX_TIME_1: Instant get() = Instant.parse("2017-09-02T12:00:00.00Z") @@ -167,16 +169,6 @@ class Cap { } } - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun issue() { transaction { diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Caplet.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Caplet.kt index 8793dd65b9..390ea97154 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Caplet.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Caplet.kt @@ -3,11 +3,6 @@ package net.corda.finance.contracts.universal import net.corda.finance.contracts.FixOf import net.corda.finance.contracts.Tenor import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Ignore import org.junit.Test import java.time.Instant @@ -53,17 +48,6 @@ class Caplet { val stateFixed = UniversalContract.State(listOf(DUMMY_NOTARY), contractFixed) val stateFinal = UniversalContract.State(listOf(DUMMY_NOTARY), contractFinal) - - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun issue() { transaction { diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXFwdTimeOption.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXFwdTimeOption.kt index ec78fc1b0c..7117b22b74 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXFwdTimeOption.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXFwdTimeOption.kt @@ -1,11 +1,6 @@ package net.corda.finance.contracts.universal import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Ignore import org.junit.Test import java.time.Instant @@ -50,17 +45,6 @@ class FXFwdTimeOption val inState = UniversalContract.State(listOf(DUMMY_NOTARY), initialContract) val outState1 = UniversalContract.State(listOf(DUMMY_NOTARY), outContract1) val outState2 = UniversalContract.State(listOf(DUMMY_NOTARY), outContract2) - - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun `issue - signature`() { transaction { diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXSwap.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXSwap.kt index 26189d2424..67983a6c0e 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXSwap.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXSwap.kt @@ -1,11 +1,6 @@ package net.corda.finance.contracts.universal import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Ignore import org.junit.Test import java.time.Instant @@ -41,17 +36,6 @@ class FXSwap { val outStateBad3 = UniversalContract.State(listOf(DUMMY_NOTARY), transferBad3) val inState = UniversalContract.State(listOf(DUMMY_NOTARY), contract) - - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun `issue - signature`() { diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/IRS.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/IRS.kt index ae55271902..9239146e8c 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/IRS.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/IRS.kt @@ -4,11 +4,6 @@ import net.corda.finance.contracts.FixOf import net.corda.finance.contracts.Frequency import net.corda.finance.contracts.Tenor import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Ignore import org.junit.Test import java.time.Instant @@ -132,17 +127,6 @@ class IRS { val stateAfterExecutionFirst = UniversalContract.State(listOf(DUMMY_NOTARY), contractAfterExecutionFirst) val statePaymentFirst = UniversalContract.State(listOf(DUMMY_NOTARY), paymentFirst) - - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun issue() { transaction { diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/RollOutTests.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/RollOutTests.kt index 40d4b7bb94..3a86f41cfa 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/RollOutTests.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/RollOutTests.kt @@ -2,11 +2,6 @@ package net.corda.finance.contracts.universal import net.corda.finance.contracts.Frequency import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Test import java.time.Instant import kotlin.test.assertEquals @@ -122,16 +117,6 @@ class RollOutTests { next() } - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun `arrangement equality transfer`() { assertEquals(contract_transfer1, contract_transfer2) diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Swaption.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Swaption.kt index 590e7a47ac..c0b464af33 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Swaption.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Swaption.kt @@ -3,11 +3,6 @@ package net.corda.finance.contracts.universal import net.corda.finance.contracts.Frequency import net.corda.finance.contracts.Tenor import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Ignore import org.junit.Test import java.time.Instant @@ -59,17 +54,6 @@ class Swaption { } val stateInitial = UniversalContract.State(listOf(DUMMY_NOTARY), contractInitial) - - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun issue() { transaction { diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/ZeroCouponBond.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/ZeroCouponBond.kt index 3d79d0df1b..8692518724 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/ZeroCouponBond.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/ZeroCouponBond.kt @@ -1,11 +1,6 @@ package net.corda.finance.contracts.universal import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.setCordappPackages -import net.corda.testing.transaction -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Test import java.time.Instant import kotlin.test.assertEquals @@ -43,17 +38,6 @@ class ZeroCouponBond { val outStateWrong = UniversalContract.State(listOf(DUMMY_NOTARY), transferWrong) val outStateMove = UniversalContract.State(listOf(DUMMY_NOTARY), contractMove) - - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.universal") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun basic() { assertEquals(Zero(), Zero()) 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 6175bca1b7..d1d6751aa1 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt @@ -231,7 +231,6 @@ class CommercialPaperTestsGeneric { // @Test @Ignore fun `issue move and then redeem`() { - setCordappPackages("net.corda.finance.contracts") initialiseTestSerialization() val aliceDatabaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(ALICE_KEY)) val databaseAlice = aliceDatabaseAndServices.first diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt index d5aa6d8cdc..c2d683a74b 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt @@ -593,7 +593,6 @@ class ObligationTests { } // Try defaulting an obligation that is now in the past - unsetCordappPackages() ledger { transaction("Settlement") { attachments(Obligation.PROGRAM_ID) diff --git a/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt b/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt index c8c92fad91..94b3f83658 100644 --- a/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt @@ -28,8 +28,7 @@ class CashExitFlowTests { @Before fun start() { - setCordappPackages("net.corda.finance.contracts.asset") - mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) + mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("net.corda.finance.contracts.asset")) notaryNode = mockNet.createNotaryNode() bankOfCordaNode = mockNet.createPartyNode(BOC.name) notary = notaryNode.services.getDefaultNotary() @@ -45,7 +44,6 @@ class CashExitFlowTests { @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test 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 3aeb782a1d..911a014d5f 100644 --- a/finance/src/test/kotlin/net/corda/finance/flows/CashIssueFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashIssueFlowTests.kt @@ -10,11 +10,9 @@ import net.corda.node.internal.StartedNode import net.corda.testing.chooseIdentity import net.corda.testing.getDefaultNotary import net.corda.testing.BOC -import net.corda.testing.DUMMY_NOTARY import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode -import net.corda.testing.setCordappPackages import org.junit.After import org.junit.Before import org.junit.Test @@ -30,8 +28,7 @@ class CashIssueFlowTests { @Before fun start() { - setCordappPackages("net.corda.finance.contracts.asset") - mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) + mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("net.corda.finance.contracts.asset")) notaryNode = mockNet.createNotaryNode() bankOfCordaNode = mockNet.createPartyNode(BOC.name) bankOfCorda = bankOfCordaNode.info.chooseIdentity() diff --git a/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt b/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt index a9b1352646..0c5b1e5efa 100644 --- a/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt @@ -31,8 +31,7 @@ class CashPaymentFlowTests { @Before fun start() { - setCordappPackages("net.corda.finance.contracts.asset") - mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) + mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("net.corda.finance.contracts.asset")) notaryNode = mockNet.createNotaryNode() bankOfCordaNode = mockNet.createPartyNode(BOC.name) bankOfCorda = bankOfCordaNode.info.chooseIdentity() 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 ce95ac21d8..3e72b1d0d4 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 @@ -9,7 +9,6 @@ import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.testing.* import net.corda.testing.node.MockServices -import org.junit.After import org.junit.Assert.* import org.junit.Before import org.junit.Test @@ -48,11 +47,6 @@ class AttachmentsClassLoaderStaticContractTests : TestDependencyInjectionBase() serviceHub = MockServices(cordappPackages = listOf("net.corda.nodeapi.internal")) } - @After - fun `clear packages`() { - unsetCordappPackages() - } - @Test fun `test serialization of WireTransaction with statically loaded contract`() { val tx = AttachmentDummyContract().generateInitial(MEGA_CORP.ref(0), 42, DUMMY_NOTARY) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt index 0b5d72d858..3ba78ff7bd 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt @@ -16,26 +16,14 @@ import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.node.NodeBasedTest -import org.junit.After -import org.junit.Before import org.junit.Test import java.util.* import kotlin.test.assertEquals import kotlin.test.assertFailsWith -class RaftNotaryServiceTests : NodeBasedTest() { +class RaftNotaryServiceTests : NodeBasedTest(listOf("net.corda.testing.contracts")) { private val notaryName = CordaX500Name(RaftValidatingNotaryService.id, "RAFT Notary Service", "London", "GB") - @Before - fun setup() { - setCordappPackages("net.corda.testing.contracts") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun `detect double spend`() { val (bankA) = listOf( 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 0d92fe6df5..e08b457c37 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -102,6 +102,7 @@ import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair abstract class AbstractNode(config: NodeConfiguration, val platformClock: Clock, protected val versionInfo: VersionInfo, + protected val cordappLoader: CordappLoader, @VisibleForTesting val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() { open val configuration = config.apply { require(minimumPlatformVersion <= versionInfo.platformVersion) { @@ -151,8 +152,6 @@ abstract class AbstractNode(config: NodeConfiguration, protected val runOnStop = ArrayList<() -> Any?>() protected lateinit var database: CordaPersistence lateinit var cordappProvider: CordappProviderImpl - protected val cordappLoader by lazy { makeCordappLoader() } - protected val _nodeReadyFuture = openFuture() /** Completes once the node has successfully registered with the network map service * or has loaded network map data from local database */ @@ -472,19 +471,6 @@ abstract class AbstractNode(config: NodeConfiguration, return tokenizableServices } - private fun makeCordappLoader(): CordappLoader { - val scanPackages = System.getProperty("net.corda.node.cordapp.scan.packages") - return if (CordappLoader.testPackages.isNotEmpty()) { - check(configuration.devMode) { "Package scanning can only occur in dev mode" } - CordappLoader.createDefaultWithTestPackages(configuration.baseDirectory, CordappLoader.testPackages) - } else if (scanPackages != null) { - check(configuration.devMode) { "Package scanning can only occur in dev mode" } - CordappLoader.createDefaultWithTestPackages(configuration.baseDirectory, scanPackages.split(",")) - } else { - CordappLoader.createDefault(configuration.baseDirectory) - } - } - protected open fun makeTransactionStorage(): WritableTransactionStorage = DBTransactionStorage() private fun makeVaultObservers() { 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 84b126980a..7257eea773 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -15,6 +15,7 @@ import net.corda.core.node.ServiceHub import net.corda.core.serialization.SerializationDefaults import net.corda.core.utilities.* import net.corda.node.VersionInfo +import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.serialization.KryoServerSerializationScheme import net.corda.node.serialization.NodeClock import net.corda.node.services.RPCUserService @@ -22,6 +23,7 @@ import net.corda.node.services.RPCUserServiceImpl import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.api.SchemaService import net.corda.node.services.config.FullNodeConfiguration +import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.node.services.messaging.ArtemisMessagingServer.Companion.ipDetectRequestProperty import net.corda.node.services.messaging.ArtemisMessagingServer.Companion.ipDetectResponseProperty @@ -62,8 +64,9 @@ import kotlin.system.exitProcess */ open class Node(override val configuration: FullNodeConfiguration, versionInfo: VersionInfo, - val initialiseSerialization: Boolean = true -) : AbstractNode(configuration, createClock(configuration), versionInfo) { + val initialiseSerialization: Boolean = true, + cordappLoader: CordappLoader = makeCordappLoader(configuration) +) : AbstractNode(configuration, createClock(configuration), versionInfo, cordappLoader) { companion object { private val logger = loggerFor() var renderBasicInfoToConsole = true @@ -86,6 +89,13 @@ open class Node(override val configuration: FullNodeConfiguration, } private val sameVmNodeCounter = AtomicInteger() + val scanPackagesSystemProperty = "net.corda.node.cordapp.scan.packages" + val scanPackagesSeparator = "," + private fun makeCordappLoader(configuration: NodeConfiguration): CordappLoader { + return System.getProperty(scanPackagesSystemProperty)?.let { scanPackages -> + CordappLoader.createDefaultWithTestPackages(configuration, scanPackages.split(scanPackagesSeparator)) + } ?: CordappLoader.createDefault(configuration.baseDirectory) + } } override val log: Logger get() = logger diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt index 493b965ba6..7cdba9ca71 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt @@ -14,6 +14,7 @@ import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import net.corda.core.utilities.loggerFor import net.corda.node.internal.classloading.requireAnnotation +import net.corda.node.services.config.NodeConfiguration import net.corda.nodeapi.internal.serialization.DefaultWhitelist import java.io.File import java.io.FileOutputStream @@ -64,14 +65,12 @@ class CordappLoader private constructor(private val cordappJarPaths: List) /** * Create a dev mode CordappLoader for test environments that creates and loads cordapps from the classpath * and plugins directory. This is intended mostly for use by the driver. - * - * @param baseDir See [createDefault.baseDir] - * @param testPackages See [createWithTestPackages.testPackages] */ @VisibleForTesting - @JvmOverloads - fun createDefaultWithTestPackages(baseDir: Path, testPackages: List = CordappLoader.testPackages) - = CordappLoader(getCordappsInDirectory(getPluginsPath(baseDir)) + testPackages.flatMap(this::createScanPackage)) + fun createDefaultWithTestPackages(configuration: NodeConfiguration, testPackages: List): CordappLoader { + check(configuration.devMode) { "Package scanning can only occur in dev mode" } + return CordappLoader(getCordappsInDirectory(getPluginsPath(configuration.baseDirectory)) + testPackages.flatMap(this::createScanPackage)) + } /** * Create a dev mode CordappLoader for test environments that creates and loads cordapps from the classpath. @@ -81,8 +80,7 @@ class CordappLoader private constructor(private val cordappJarPaths: List) * CorDapps. */ @VisibleForTesting - @JvmOverloads - fun createWithTestPackages(testPackages: List = CordappLoader.testPackages) + fun createWithTestPackages(testPackages: List) = CordappLoader(testPackages.flatMap(this::createScanPackage)) /** @@ -147,11 +145,6 @@ class CordappLoader private constructor(private val cordappJarPaths: List) } } - /** - * A list of test packages that will be scanned as CorDapps and compiled into CorDapp JARs for use in tests only. - */ - @VisibleForTesting - var testPackages: List = emptyList() private val generatedCordapps = mutableMapOf() /** A list of the core RPC flows present in Corda */ diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index 97dab2aaa7..0568b45a3e 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -43,15 +43,15 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { @Before public void setUp() { - setCordappPackages("net.corda.testing.contracts", "net.corda.finance.contracts.asset"); + List cordappPackages = Arrays.asList("net.corda.testing.contracts", "net.corda.finance.contracts.asset"); ArrayList keys = new ArrayList<>(); keys.add(getMEGA_CORP_KEY()); keys.add(getDUMMY_NOTARY_KEY()); Set requiredSchemas = Collections.singleton(CashSchemaV1.INSTANCE); IdentityService identitySvc = makeTestIdentityService(); @SuppressWarnings("unchecked") - Pair databaseAndServices = makeTestDatabaseAndMockServices(requiredSchemas, keys, () -> identitySvc, Collections.EMPTY_LIST); - issuerServices = new MockServices(getDUMMY_CASH_ISSUER_KEY(), getBOC_KEY()); + Pair databaseAndServices = makeTestDatabaseAndMockServices(requiredSchemas, keys, () -> identitySvc, cordappPackages); + issuerServices = new MockServices(cordappPackages, getDUMMY_CASH_ISSUER_KEY(), getBOC_KEY()); database = databaseAndServices.getFirst(); services = databaseAndServices.getSecond(); vaultService = services.getVaultService(); @@ -60,7 +60,6 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { @After public void cleanUp() throws IOException { database.close(); - unsetCordappPackages(); } /** diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index e03fd6972c..0020970296 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -62,9 +62,7 @@ class CordaRPCOpsImplTest { @Before fun setup() { - setCordappPackages("net.corda.finance.contracts.asset") - - mockNet = MockNetwork() + mockNet = MockNetwork(cordappPackages = listOf("net.corda.finance.contracts.asset")) aliceNode = mockNet.createNode() notaryNode = mockNet.createNotaryNode(validating = false) rpc = CordaRPCOpsImpl(aliceNode.services, aliceNode.smm, aliceNode.database) @@ -81,7 +79,6 @@ class CordaRPCOpsImplTest { @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test @@ -100,7 +97,6 @@ class CordaRPCOpsImplTest { } // Tell the monitoring service node to issue some cash - val recipient = aliceNode.info.chooseIdentity() val result = rpc.startFlow(::CashIssueFlow, Amount(quantity, GBP), ref, notary) mockNet.runNetwork() diff --git a/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt b/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt index 1a08a04e76..6135863388 100644 --- a/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/CordaServiceTest.kt @@ -15,8 +15,6 @@ import net.corda.finance.flows.CashIssueFlow import net.corda.node.internal.cordapp.DummyRPCFlow import net.corda.testing.DUMMY_NOTARY import net.corda.testing.node.MockNetwork -import net.corda.testing.setCordappPackages -import net.corda.testing.unsetCordappPackages import org.junit.After import org.junit.Before import org.junit.Test @@ -74,9 +72,7 @@ class TestCordaService2(val appServiceHub: AppServiceHub): SingletonSerializeAsT } @CordaService -class LegacyCordaService(val simpleServiceHub: ServiceHub): SingletonSerializeAsToken() { - -} +class LegacyCordaService(@Suppress("UNUSED_PARAMETER") simpleServiceHub: ServiceHub) : SingletonSerializeAsToken() class CordaServiceTest { lateinit var mockNet: MockNetwork @@ -85,8 +81,7 @@ class CordaServiceTest { @Before fun start() { - setCordappPackages("net.corda.node.internal","net.corda.finance") - mockNet = MockNetwork(threadPerNode = true) + mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.node.internal","net.corda.finance")) notaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name, validating = true) nodeA = mockNet.createNode() mockNet.startNodes() @@ -95,7 +90,6 @@ class CordaServiceTest { @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 1d19ed5833..9677213a16 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -45,6 +45,7 @@ import net.corda.testing.* import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.MockNetwork +import net.corda.testing.node.MockServices import net.corda.testing.node.pumpReceive import org.assertj.core.api.Assertions.assertThat import org.junit.After @@ -73,6 +74,7 @@ import kotlin.test.assertTrue @RunWith(Parameterized::class) class TwoPartyTradeFlowTests(val anonymous: Boolean) { companion object { + private val cordappPackages = listOf("net.corda.finance.contracts") @JvmStatic @Parameterized.Parameters fun data(): Collection { @@ -84,7 +86,6 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { @Before fun before() { - setCordappPackages("net.corda.finance.contracts") LogHelper.setLevel("platform.trade", "core.contract.TransactionGroup", "recordingmap") } @@ -92,7 +93,6 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { fun after() { mockNet.stopNodes() LogHelper.reset("platform.trade", "core.contract.TransactionGroup", "recordingmap") - unsetCordappPackages() } @Test @@ -100,9 +100,8 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { // We run this in parallel threads to help catch any race conditions that may exist. The other tests // we run in the unit test thread exclusively to speed things up, ensure deterministic results and // allow interruption half way through. - mockNet = MockNetwork(false, true) - - ledger(initialiseSerialization = false) { + mockNet = MockNetwork(false, true, cordappPackages = cordappPackages) + ledger(MockServices(cordappPackages), initialiseSerialization = false) { val notaryNode = mockNet.createNotaryNode() val aliceNode = mockNet.createPartyNode(ALICE.name) val bobNode = mockNet.createPartyNode(BOB.name) @@ -149,9 +148,8 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { @Test(expected = InsufficientBalanceException::class) fun `trade cash for commercial paper fails using soft locking`() { - mockNet = MockNetwork(false, true) - - ledger(initialiseSerialization = false) { + mockNet = MockNetwork(false, true, cordappPackages = cordappPackages) + ledger(MockServices(cordappPackages), initialiseSerialization = false) { val notaryNode = mockNet.createNotaryNode() val aliceNode = mockNet.createPartyNode(ALICE.name) val bobNode = mockNet.createPartyNode(BOB.name) @@ -204,8 +202,8 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { @Test fun `shutdown and restore`() { - mockNet = MockNetwork(false) - ledger(initialiseSerialization = false) { + mockNet = MockNetwork(false, cordappPackages = cordappPackages) + ledger(MockServices(cordappPackages), initialiseSerialization = false) { val notaryNode = mockNet.createNotaryNode() val aliceNode = mockNet.createPartyNode(ALICE.name) var bobNode = mockNet.createPartyNode(BOB.name) @@ -222,8 +220,6 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { bobNode.internals.disableDBCloseOnStop() val bobAddr = bobNode.network.myAddress as InMemoryMessagingNetwork.PeerHandle - val networkMapAddress = notaryNode.network.myAddress - mockNet.runNetwork() // Clear network map registration messages val notary = aliceNode.services.getDefaultNotary() @@ -331,8 +327,7 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { @Test fun `check dependencies of sale asset are resolved`() { - mockNet = MockNetwork(false) - + mockNet = MockNetwork(false, cordappPackages = cordappPackages) val notaryNode = mockNet.createNotaryNode() val aliceNode = makeNodeWithTracking(ALICE.name) val bobNode = makeNodeWithTracking(BOB.name) @@ -436,8 +431,7 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { @Test fun `track works`() { - mockNet = MockNetwork(false) - + mockNet = MockNetwork(false, cordappPackages = cordappPackages) val notaryNode = mockNet.createNotaryNode() val aliceNode = makeNodeWithTracking(ALICE.name) val bobNode = makeNodeWithTracking(BOB.name) @@ -517,16 +511,16 @@ class TwoPartyTradeFlowTests(val anonymous: Boolean) { @Test fun `dependency with error on buyer side`() { - mockNet = MockNetwork(false) - ledger(initialiseSerialization = false) { + mockNet = MockNetwork(false, cordappPackages = cordappPackages) + ledger(MockServices(cordappPackages), initialiseSerialization = false) { runWithError(true, false, "at least one cash input") } } @Test fun `dependency with error on seller side`() { - mockNet = MockNetwork(false) - ledger(initialiseSerialization = false) { + mockNet = MockNetwork(false, cordappPackages = cordappPackages) + ledger(MockServices(cordappPackages), initialiseSerialization = false) { runWithError(false, true, "Issuances have a time-window") } } diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index a758655ff8..957d6ac442 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -35,8 +35,7 @@ class NotaryChangeTests { @Before fun setUp() { - setCordappPackages("net.corda.testing.contracts") - mockNet = MockNetwork() + mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) oldNotaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) clientNodeA = mockNet.createNode() clientNodeB = mockNet.createNode() @@ -50,7 +49,6 @@ class NotaryChangeTests { @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test diff --git a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt index ad05ba1ad2..d5c966d31c 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt @@ -72,7 +72,6 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { @Before fun setup() { - setCordappPackages("net.corda.testing.contracts") initialiseTestSerialization() countDown = CountDownLatch(1) smmHasRemovedAllFlows = CountDownLatch(1) @@ -98,7 +97,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { network = mockMessagingService), TestReference { override val vaultService: VaultServiceInternal = NodeVaultService(testClock, kms, stateLoader, database.hibernateConfig) override val testReference = this@NodeSchedulerServiceTest - override val cordappProvider: CordappProviderImpl = CordappProviderImpl(CordappLoader.createWithTestPackages()).start(attachments) + override val cordappProvider = CordappProviderImpl(CordappLoader.createWithTestPackages(listOf("net.corda.testing.contracts"))).start(attachments) } smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1) scheduler = NodeSchedulerService(services, schedulerGatedExecutor, serverThread = smmExecutor) @@ -124,7 +123,6 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { smmExecutor.awaitTermination(60, TimeUnit.SECONDS) database.close() resetTestSerialization() - unsetCordappPackages() } class TestState(val flowLogicRef: FlowLogicRef, val instant: Instant, val myIdentity: Party) : LinearState, SchedulableState { diff --git a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt index 013eea5d74..be2fc12b96 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt @@ -91,8 +91,7 @@ class ScheduledFlowTests { @Before fun setup() { - setCordappPackages("net.corda.testing.contracts") - mockNet = MockNetwork(threadPerNode = true) + mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.testing.contracts")) notaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) val a = mockNet.createUnstartedNode() val b = mockNet.createUnstartedNode() @@ -107,7 +106,6 @@ class ScheduledFlowTests { @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt index e554ea59be..f0a381c411 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt @@ -73,14 +73,14 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { @Before fun setUp() { - setCordappPackages("net.corda.testing.contracts", "net.corda.finance.contracts.asset") - issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY, BOB_KEY, BOC_KEY) + val cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts.asset") + issuerServices = MockServices(cordappPackages, DUMMY_CASH_ISSUER_KEY, BOB_KEY, BOC_KEY) val dataSourceProps = makeTestDataSourceProperties() val defaultDatabaseProperties = makeTestDatabaseProperties() database = configureDatabase(dataSourceProps, defaultDatabaseProperties, NodeSchemaService(), ::makeTestIdentityService) database.transaction { hibernateConfig = database.hibernateConfig - services = object : MockServices(BOB_KEY, BOC_KEY, DUMMY_NOTARY_KEY) { + services = object : MockServices(cordappPackages, BOB_KEY, BOC_KEY, DUMMY_NOTARY_KEY) { override val vaultService = makeVaultService(database.hibernateConfig) override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { for (stx in txs) { @@ -105,7 +105,6 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { @After fun cleanUp() { database.close() - unsetCordappPackages() } private fun setUpDb() { diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 00b66b30b9..d4391109e3 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -65,8 +65,7 @@ class FlowFrameworkTests { @Before fun start() { - setCordappPackages("net.corda.finance.contracts", "net.corda.testing.contracts") - mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) + mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("net.corda.finance.contracts", "net.corda.testing.contracts")) node1 = mockNet.createNode() node2 = mockNet.createNode() @@ -87,7 +86,6 @@ class FlowFrameworkTests { fun cleanUp() { mockNet.stopNodes() receivedSessionMessages.clear() - unsetCordappPackages() } @Test diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt index 579f653018..f30063aac8 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt @@ -33,8 +33,7 @@ class NotaryServiceTests { @Before fun setup() { - setCordappPackages("net.corda.testing.contracts") - mockNet = MockNetwork() + mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) notaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name, validating = false) clientNode = mockNet.createNode() mockNet.runNetwork() // Clear network map registration messages @@ -45,7 +44,6 @@ class NotaryServiceTests { @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt index 87410fecb8..740c51be74 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt @@ -33,8 +33,7 @@ class ValidatingNotaryServiceTests { @Before fun setup() { - setCordappPackages("net.corda.testing.contracts") - mockNet = MockNetwork() + mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) notaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) clientNode = mockNet.createNode() mockNet.runNetwork() // Clear network map registration messages @@ -45,7 +44,6 @@ class ValidatingNotaryServiceTests { @After fun cleanUp() { mockNet.stopNodes() - unsetCordappPackages() } @Test 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 5250e7f500..4740ac776d 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 @@ -45,27 +45,30 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue class NodeVaultServiceTest : TestDependencyInjectionBase() { + companion object { + private val cordappPackages = listOf("net.corda.finance.contracts.asset") + } + lateinit var services: MockServices - lateinit var issuerServices: MockServices + private lateinit var issuerServices: MockServices val vaultService get() = services.vaultService as NodeVaultService lateinit var database: CordaPersistence @Before fun setUp() { - setCordappPackages("net.corda.finance.contracts.asset") LogHelper.setLevel(NodeVaultService::class) val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(BOC_KEY, DUMMY_CASH_ISSUER_KEY), - customSchemas = setOf(CashSchemaV1)) + customSchemas = setOf(CashSchemaV1), + cordappPackages = cordappPackages) database = databaseAndServices.first services = databaseAndServices.second - issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY, BOC_KEY) + issuerServices = MockServices(cordappPackages, DUMMY_CASH_ISSUER_KEY, BOC_KEY) } @After fun tearDown() { database.close() LogHelper.reset(NodeVaultService::class) - unsetCordappPackages() } @Suspendable @@ -440,8 +443,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { @Test fun addNoteToTransaction() { - val megaCorpServices = MockServices(MEGA_CORP_KEY) - + val megaCorpServices = MockServices(cordappPackages, MEGA_CORP_KEY) database.transaction { val freshKey = services.myInfo.chooseIdentity().owningKey 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 9e484833d6..56d6e90992 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 @@ -45,6 +45,9 @@ import java.time.temporal.ChronoUnit import java.util.* class VaultQueryTests : TestDependencyInjectionBase() { + companion object { + private val cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts") + } private lateinit var services: MockServices private lateinit var notaryServices: MockServices @@ -59,23 +62,21 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Before fun setUp() { - setCordappPackages("net.corda.testing.contracts", "net.corda.finance.contracts") - // register additional identities identitySvc.verifyAndRegisterIdentity(CASH_NOTARY_IDENTITY) identitySvc.verifyAndRegisterIdentity(BOC_IDENTITY) val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(MEGA_CORP_KEY, DUMMY_NOTARY_KEY), createIdentityService = { identitySvc }, - customSchemas = setOf(CashSchemaV1, CommercialPaperSchemaV1, DummyLinearStateSchemaV1)) + customSchemas = setOf(CashSchemaV1, CommercialPaperSchemaV1, DummyLinearStateSchemaV1), + cordappPackages = cordappPackages) database = databaseAndServices.first services = databaseAndServices.second - notaryServices = MockServices(DUMMY_NOTARY_KEY, DUMMY_CASH_ISSUER_KEY, BOC_KEY, MEGA_CORP_KEY) + notaryServices = MockServices(cordappPackages, DUMMY_NOTARY_KEY, DUMMY_CASH_ISSUER_KEY, BOC_KEY, MEGA_CORP_KEY) } @After fun tearDown() { database.close() - unsetCordappPackages() } /** @@ -1490,18 +1491,16 @@ class VaultQueryTests : TestDependencyInjectionBase() { // GBP issuer val gbpCashIssuerKey = entropyToKeyPair(BigInteger.valueOf(1001)) val gbpCashIssuer = Party(CordaX500Name(organisation = "British Pounds Cash Issuer", locality = "London", country = "GB"), gbpCashIssuerKey.public).ref(1) - val gbpCashIssuerServices = MockServices(gbpCashIssuerKey) + val gbpCashIssuerServices = MockServices(cordappPackages, gbpCashIssuerKey) // USD issuer val usdCashIssuerKey = entropyToKeyPair(BigInteger.valueOf(1002)) val usdCashIssuer = Party(CordaX500Name(organisation = "US Dollars Cash Issuer", locality = "New York", country = "US"), usdCashIssuerKey.public).ref(1) - val usdCashIssuerServices = MockServices(usdCashIssuerKey) + val usdCashIssuerServices = MockServices(cordappPackages, usdCashIssuerKey) // CHF issuer val chfCashIssuerKey = entropyToKeyPair(BigInteger.valueOf(1003)) val chfCashIssuer = Party(CordaX500Name(organisation = "Swiss Francs Cash Issuer", locality = "Zurich", country = "CH"), chfCashIssuerKey.public).ref(1) - val chfCashIssuerServices = MockServices(chfCashIssuerKey) - + val chfCashIssuerServices = MockServices(cordappPackages, chfCashIssuerKey) database.transaction { - services.fillWithSomeTestCash(100.POUNDS, gbpCashIssuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (gbpCashIssuer)) services.fillWithSomeTestCash(100.DOLLARS, usdCashIssuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (usdCashIssuer)) services.fillWithSomeTestCash(100.SWISS_FRANCS, chfCashIssuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (chfCashIssuer)) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index 60e853d023..a42e13d55c 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -1,4 +1,4 @@ -package net.corda.node.services.vaultService +package net.corda.node.services.vault import net.corda.core.contracts.ContractState import net.corda.core.contracts.LinearState @@ -34,6 +34,10 @@ import kotlin.test.assertEquals // TODO: Move this to the cash contract tests once mock services are further split up. class VaultWithCashTest : TestDependencyInjectionBase() { + companion object { + private val cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts.asset") + } + lateinit var services: MockServices lateinit var issuerServices: MockServices val vaultService: VaultService get() = services.vaultService @@ -42,22 +46,20 @@ class VaultWithCashTest : TestDependencyInjectionBase() { @Before fun setUp() { - setCordappPackages("net.corda.testing.contracts", "net.corda.finance.contracts.asset") - LogHelper.setLevel(VaultWithCashTest::class) val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(DUMMY_CASH_ISSUER_KEY, DUMMY_NOTARY_KEY), - customSchemas = setOf(CashSchemaV1)) + customSchemas = setOf(CashSchemaV1), + cordappPackages = cordappPackages) database = databaseAndServices.first services = databaseAndServices.second - issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY, MEGA_CORP_KEY) - notaryServices = MockServices(DUMMY_NOTARY_KEY) + issuerServices = MockServices(cordappPackages, DUMMY_CASH_ISSUER_KEY, MEGA_CORP_KEY) + notaryServices = MockServices(cordappPackages, DUMMY_NOTARY_KEY) } @After fun tearDown() { LogHelper.reset(VaultWithCashTest::class) database.close() - unsetCordappPackages() } @Test @@ -81,7 +83,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { @Test fun `issue and spend total correctly and irrelevant ignored`() { - val megaCorpServices = MockServices(MEGA_CORP_KEY) + val megaCorpServices = MockServices(cordappPackages, MEGA_CORP_KEY) val freshKey = services.keyManagementService.freshKey() val usefulTX = diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index 3af7044caa..4e9d129c26 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -14,7 +14,6 @@ import net.corda.finance.contracts.Fix import net.corda.finance.contracts.FixOf import net.corda.finance.contracts.asset.CASH import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.ownedBy import net.corda.irs.flows.RatesFixFlow import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase @@ -63,7 +62,6 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { @Before fun setUp() { - setCordappPackages("net.corda.finance.contracts") database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) database.transaction { oracle = createMockCordaService(services, NodeInterestRates::Oracle) @@ -74,7 +72,6 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { @After fun tearDown() { database.close() - unsetCordappPackages() } @Test @@ -203,7 +200,7 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { @Test fun `network tearoff`() { - val mockNet = MockNetwork(initialiseSerialization = false) + val mockNet = MockNetwork(initialiseSerialization = false, cordappPackages = listOf("net.corda.finance.contracts")) val n1 = mockNet.createNotaryNode() val oracleNode = mockNet.createNode().apply { internals.registerInitiatedFlow(NodeInterestRates.FixQueryHandler::class.java) diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt index 6c929c47b7..90a3ca2d8b 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt @@ -141,7 +141,7 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, } } - val mockNet = MockNetwork(networkSendManuallyPumped, runAsync) + val mockNet = MockNetwork(networkSendManuallyPumped, runAsync, cordappPackages = listOf("net.corda.irs.contract", "net.corda.finance.contract")) // This one must come first. val networkMap = mockNet.startNetworkMapNode(nodeFactory = NetworkMapNodeFactory) val notary = mockNet.createNotaryNode(validating = false, nodeFactory = NotaryNodeFactory) @@ -255,7 +255,6 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, val networkInitialisationFinished = allOf(*mockNet.nodes.map { it.nodeReadyFuture.toCompletableFuture() }.toTypedArray()) fun start(): Future { - setCordappPackages("net.corda.irs.contract", "net.corda.finance.contract") mockNet.startNodes() // Wait for all the nodes to have finished registering with the network map service. return networkInitialisationFinished.thenCompose { startMainSimulation() } diff --git a/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt b/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt index 4bd32364a4..6da9d13091 100644 --- a/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt +++ b/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt @@ -2,25 +2,10 @@ package net.corda.netmap.simulation import net.corda.core.utilities.getOrThrow import net.corda.testing.LogHelper -import net.corda.testing.setCordappPackages -import net.corda.testing.unsetCordappPackages -import org.junit.After -import org.junit.Before import org.junit.Test class IRSSimulationTest { // TODO: These tests should be a lot more complete. - - @Before - fun setup() { - setCordappPackages("net.corda.irs.contract") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun `runs to completion`() { LogHelper.setLevel("+messages") // FIXME: Don't manipulate static state in tests. diff --git a/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt b/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt index e113f235b7..87b9fa7839 100644 --- a/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt +++ b/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt @@ -11,8 +11,6 @@ import net.corda.vega.api.PortfolioApiUtils import net.corda.vega.api.SwapDataModel import net.corda.vega.api.SwapDataView import org.assertj.core.api.Assertions.assertThat -import org.junit.After -import org.junit.Before import org.junit.Test import java.math.BigDecimal import java.time.LocalDate @@ -26,19 +24,9 @@ class SimmValuationTest : IntegrationTestCategory { val testTradeId = "trade1" } - @Before - fun setup() { - setCordappPackages("net.corda.vega.contracts") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - @Test fun `runs SIMM valuation demo`() { - driver(isDebug = true) { + driver(isDebug = true, extraCordappPackagesToScan = listOf("net.corda.vega.contracts")) { startNotaryNode(DUMMY_NOTARY.name, validating = false).getOrThrow() val nodeAFuture = startNode(providedName = nodeALegalName) val nodeBFuture = startNode(providedName = nodeBLegalName) diff --git a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt index 0efa101ec7..6fb3556c25 100644 --- a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt +++ b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt @@ -17,23 +17,10 @@ import net.corda.traderdemo.flow.BuyerFlow import net.corda.traderdemo.flow.CommercialPaperIssueFlow import net.corda.traderdemo.flow.SellerFlow import org.assertj.core.api.Assertions.assertThat -import org.junit.After -import org.junit.Before import org.junit.Test import java.util.concurrent.Executors -class TraderDemoTest : NodeBasedTest() { - - @Before - fun setup() { - setCordappPackages("net.corda.finance.contracts.asset", "net.corda.finance.contracts") - } - - @After - fun tearDown() { - unsetCordappPackages() - } - +class TraderDemoTest : NodeBasedTest(listOf("net.corda.finance.contracts.asset", "net.corda.finance.contracts")) { @Test fun `runs trader demo`() { val demoUser = User("demo", "demo", setOf(startFlowPermission())) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt index 7fe3ee5c09..551a8075b8 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt @@ -46,8 +46,9 @@ fun transaction( transactionLabel: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(notary = DUMMY_NOTARY), initialiseSerialization: Boolean = true, + cordappPackages: List = emptyList(), dsl: TransactionDSL.() -> EnforceVerifyOrFail -) = ledger(initialiseSerialization = initialiseSerialization) { +) = ledger(services = MockServices(cordappPackages), initialiseSerialization = initialiseSerialization) { dsl(TransactionDSL(TestTransactionDSLInterpreter(this.interpreter, transactionBuilder))) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index 9a1ce481ca..0a8cf4ecbe 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -24,6 +24,7 @@ import net.corda.core.utilities.* import net.corda.node.internal.Node import net.corda.node.internal.NodeStartup import net.corda.node.internal.StartedNode +import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.services.config.* import net.corda.node.services.network.NetworkMapService import net.corda.node.utilities.ServiceIdentityGenerator @@ -511,7 +512,6 @@ class ShutdownManager(private val executorService: ExecutorService) { } fun shutdown() { - unsetCordappPackages() val shutdownActionFutures = state.locked { if (isShutdown) { emptyList Unit>>() @@ -595,14 +595,14 @@ class DriverDSL( val isDebug: Boolean, val networkMapStartStrategy: NetworkMapStartStrategy, val startNodesInProcess: Boolean, - val extraCordappPackagesToScan: List + extraCordappPackagesToScan: List ) : DriverDSLInternalInterface { private val dedicatedNetworkMapAddress = portAllocation.nextHostAndPort() private var _executorService: ScheduledExecutorService? = null val executorService get() = _executorService!! private var _shutdownManager: ShutdownManager? = null override val shutdownManager get() = _shutdownManager!! - private val packagesToScanString = extraCordappPackagesToScan + getCallerPackage() + private val cordappPackages = extraCordappPackagesToScan + getCallerPackage() class State { val processes = ArrayList>() @@ -821,8 +821,6 @@ class DriverDSL( override fun start() { _executorService = Executors.newScheduledThreadPool(2, ThreadFactoryBuilder().setNameFormat("driver-pool-thread-%d").build()) _shutdownManager = ShutdownManager(executorService) - // We set this property so that in-process nodes find cordapps. Out-of-process nodes need this passed in when started. - setCordappPackages(*packagesToScanString.toTypedArray()) if (networkMapStartStrategy.startDedicated) { startDedicatedNetworkMapService().andForget(log) // Allow it to start concurrently with other nodes. } @@ -859,7 +857,7 @@ class DriverDSL( private fun startNodeInternal(config: Config, webAddress: NetworkHostAndPort, startInProcess: Boolean?, maximumHeapSize: String): CordaFuture { val nodeConfiguration = config.parseAs() if (startInProcess ?: startNodesInProcess) { - val nodeAndThreadFuture = startInProcessNode(executorService, nodeConfiguration, config) + val nodeAndThreadFuture = startInProcessNode(executorService, nodeConfiguration, config, cordappPackages) shutdownManager.registerShutdown( nodeAndThreadFuture.map { (node, thread) -> { @@ -877,7 +875,7 @@ class DriverDSL( } } else { val debugPort = if (isDebug) debugPortAllocation.nextPort() else null - val processFuture = startOutOfProcessNode(executorService, nodeConfiguration, config, quasarJarPath, debugPort, systemProperties, packagesToScanString.joinToString(","), maximumHeapSize) + val processFuture = startOutOfProcessNode(executorService, nodeConfiguration, config, quasarJarPath, debugPort, systemProperties, cordappPackages, maximumHeapSize) registerProcess(processFuture) return processFuture.flatMap { process -> val processDeathFuture = poll(executorService, "process death") { @@ -920,14 +918,15 @@ class DriverDSL( private fun startInProcessNode( executorService: ScheduledExecutorService, nodeConf: FullNodeConfiguration, - config: Config + config: Config, + cordappPackages: List ): CordaFuture, Thread>> { return executorService.fork { log.info("Starting in-process Node ${nodeConf.myLegalName.organisation}") // Write node.conf writeConfig(nodeConf.baseDirectory, "node.conf", config) // TODO pass the version in? - val node = Node(nodeConf, MOCK_VERSION_INFO, initialiseSerialization = false).start() + val node = Node(nodeConf, MOCK_VERSION_INFO, initialiseSerialization = false, cordappLoader = CordappLoader.createDefaultWithTestPackages(nodeConf, cordappPackages)).start() val nodeThread = thread(name = nodeConf.myLegalName.organisation) { node.internals.run() } @@ -942,7 +941,7 @@ class DriverDSL( quasarJarPath: String, debugPort: Int?, overriddenSystemProperties: Map, - packagesToScanString: String, + cordappPackages: List, maximumHeapSize: String ): CordaFuture { val processFuture = executorService.fork { @@ -953,7 +952,7 @@ class DriverDSL( val systemProperties = overriddenSystemProperties + mapOf( "name" to nodeConf.myLegalName, "visualvm.display.name" to "corda-${nodeConf.myLegalName}", - "net.corda.node.cordapp.scan.packages" to packagesToScanString, + Node.scanPackagesSystemProperty to cordappPackages.joinToString(Node.scanPackagesSeparator), "java.io.tmpdir" to System.getProperty("java.io.tmpdir") // Inherit from parent process ) // See experimental/quasar-hook/README.md for how to generate. diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index 88824e118e..84dc90fe02 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -26,6 +26,7 @@ import net.corda.core.utilities.loggerFor import net.corda.finance.utils.WorldMapLocation import net.corda.node.internal.AbstractNode import net.corda.node.internal.StartedNode +import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.api.SchemaService import net.corda.node.services.config.BFTSMaRtConfiguration @@ -82,7 +83,8 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random(), private val defaultFactory: Factory<*> = MockNetwork.DefaultFactory, - private val initialiseSerialization: Boolean = true) : Closeable { + private val initialiseSerialization: Boolean = true, + private val cordappPackages: List = emptyList()) : Closeable { companion object { // TODO In future PR we're removing the concept of network map node so the details of this mock are not important. val MOCK_NET_MAP = Party(CordaX500Name(organisation = "Mock Network Map", locality = "Madrid", country = "ES"), DUMMY_KEY_1.public) @@ -160,7 +162,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, val id: Int, internal val notaryIdentity: Pair?, val entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue())) : - AbstractNode(config, TestClock(), MOCK_VERSION_INFO, mockNet.busyLatch) { + AbstractNode(config, TestClock(), MOCK_VERSION_INFO, CordappLoader.createDefaultWithTestPackages(config, mockNet.cordappPackages), mockNet.busyLatch) { var counter = entropyRoot override val log: Logger = loggerFor() override val serverThread: AffinityExecutor = 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 ae838119a2..47fd8589a3 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 @@ -167,9 +167,8 @@ open class MockServices( return NodeInfo(emptyList(), listOf(identity), 1, serial = 1L) } override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2) - val mockCordappProvider: MockCordappProvider = MockCordappProvider(CordappLoader.createWithTestPackages(cordappPackages + CordappLoader.testPackages)).start(attachments) as MockCordappProvider - override val cordappProvider: CordappProvider = mockCordappProvider - + val mockCordappProvider = MockCordappProvider(CordappLoader.createWithTestPackages(cordappPackages)).start(attachments) as MockCordappProvider + override val cordappProvider: CordappProvider get() = mockCordappProvider lateinit var hibernatePersister: HibernateObserver fun makeVaultService(hibernateConfig: HibernateConfiguration = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties(), { identityService })): VaultServiceInternal { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt index 3d4d4e9d63..36dfb43a45 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt @@ -9,6 +9,7 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow import net.corda.node.internal.Node import net.corda.node.internal.StartedNode +import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.services.config.* import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.nodeapi.User @@ -31,7 +32,7 @@ import kotlin.concurrent.thread * purposes. Use the driver if you need to run the nodes in separate processes otherwise this class will suffice. */ // TODO Some of the logic here duplicates what's in the driver -abstract class NodeBasedTest : TestDependencyInjectionBase() { +abstract class NodeBasedTest(private val cordappPackages: List = emptyList()) : TestDependencyInjectionBase() { companion object { private val WHITESPACE = "\\s++".toRegex() } @@ -193,7 +194,8 @@ abstract class NodeBasedTest : TestDependencyInjectionBase() { val node = Node( parsedConfig, MOCK_VERSION_INFO.copy(platformVersion = platformVersion), - initialiseSerialization = false).start() + initialiseSerialization = false, + cordappLoader = CordappLoader.createDefaultWithTestPackages(parsedConfig, cordappPackages)).start() nodes += node thread(name = legalName.organisation) { node.internals.run() diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index 4e20194249..385b195e04 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -17,7 +17,6 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.loggerFor import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER -import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.services.config.configureDevKeyAndTrustStores import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.utilities.CertificateAndKeyPair @@ -169,18 +168,3 @@ fun NodeInfo.chooseIdentityAndCert(): PartyAndCertificate = legalIdentitiesAndCe fun NodeInfo.chooseIdentity(): Party = chooseIdentityAndCert().party /** Returns the identity of the first notary found on the network */ fun ServiceHub.getDefaultNotary(): Party = networkMapCache.notaryIdentities.first() - -/** - * Set the package to scan for cordapps - this overrides the default behaviour of scanning the cordapps directory - * @param packageNames A package name that you wish to scan for cordapps - */ -fun setCordappPackages(vararg packageNames: String) { - CordappLoader.testPackages = packageNames.toList() -} - -/** - * Unsets the default overriding behaviour of [setCordappPackages] - */ -fun unsetCordappPackages() { - CordappLoader.testPackages = emptyList() -} \ No newline at end of file From ef0f0acc4a02b960d79648d61bf92e5022d99d65 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Wed, 11 Oct 2017 11:13:46 +0100 Subject: [PATCH 155/185] Make integration tests pass in AMQP mode, part 1 (#1855) --- docs/source/serialization.rst | 9 +++++---- .../amqp/SerializationOutputTests.kt | 16 +++++++++++++++- .../corda/node/services/messaging/Messaging.kt | 1 + .../services/messaging/NodeMessagingClient.kt | 13 ++++++------- .../testing/node/InMemoryMessagingNetwork.kt | 2 -- 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/docs/source/serialization.rst b/docs/source/serialization.rst index 92d542dd71..b9eb1b7e78 100644 --- a/docs/source/serialization.rst +++ b/docs/source/serialization.rst @@ -63,7 +63,7 @@ The long term goal is to migrate the current serialization format for everything #. A desire to support open-ended polymorphism, where the number of subclasses of a superclass can expand over time and do not need to be defined in the schema *upfront*, which is key to many Corda concepts, such as contract states. #. Increased security from deserialized objects being constructed through supported constructors rather than having - data poked directy into their fields without an opportunity to validate consistency or intercept attempts to manipulate + data poked directly into their fields without an opportunity to validate consistency or intercept attempts to manipulate supposed invariants. Documentation on that format, and how JVM classes are translated to AMQP, will be linked here when it is available. @@ -259,9 +259,10 @@ Kotlin Objects `````````````` #. Kotlin ``object`` s are singletons and treated differently. They are recorded into the stream with no properties - and deserialize back to the singleton instance. - -Currently, the same is not true of Java singletons, and they will deserialize to new instances of the class. + and deserialize back to the singleton instance. Currently, the same is not true of Java singletons, + and they will deserialize to new instances of the class. + #. Kotlin's anonymous ``object`` s are not currently supported. I.e. constructs like: + ``object : Contract {...}`` will not serialize correctly and need to be re-written as an explicit class declaration. The Carpenter ````````````` diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt index 4b1011ad49..31136f0723 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt @@ -534,8 +534,22 @@ class SerializationOutputTests { } } - val FOO_PROGRAM_ID = "net.corda.nodeapi.internal.serialization.amqp.SerializationOutputTests.FooContract" + @Test + fun `test custom object`() { + serdes(FooContract) + } + @Test + @Ignore("Cannot serialize due to known Kotlin/serialization limitation") + fun `test custom anonymous object`() { + val anonymous: Contract = object : Contract { + override fun verify(tx: LedgerTransaction) { + } + } + serdes(anonymous) + } + + private val FOO_PROGRAM_ID = "net.corda.nodeapi.internal.serialization.amqp.SerializationOutputTests.FooContract" class FooState : ContractState { override val participants: List = emptyList() } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt b/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt index b05dc10c02..a708166239 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt @@ -213,6 +213,7 @@ data class TopicSession(val topic: String, val sessionID: Long = MessagingServic * These IDs and timestamps should not be assumed to be globally unique, although due to the nanosecond precision of * the timestamp field they probably will be, even if an implementation just uses a hash prefix as the message id. */ +@CordaSerializable interface Message { val topicSession: TopicSession val data: ByteArray diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt index 02ee5500ec..5a9cd04730 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt @@ -133,6 +133,11 @@ class NodeMessagingClient(override val config: NodeConfiguration, persistentEntityClass = RetryMessage::class.java ) } + + private class NodeClientMessage(override val topicSession: TopicSession, override val data: ByteArray, override val uniqueMessageId: UUID) : Message { + override val debugTimestamp: Instant = Instant.now() + override fun toString() = "$topicSession#${String(data)}" + } } private class InnerState { @@ -599,13 +604,7 @@ class NodeMessagingClient(override val config: NodeConfiguration, override fun createMessage(topicSession: TopicSession, data: ByteArray, uuid: UUID): Message { // TODO: We could write an object that proxies directly to an underlying MQ message here and avoid copying. - return object : Message { - override val topicSession: TopicSession = topicSession - override val data: ByteArray = data - override val debugTimestamp: Instant = Instant.now() - override val uniqueMessageId: UUID = uuid - override fun toString() = "$topicSession#${String(data)}" - } + return NodeClientMessage(topicSession, data, uuid) } private fun createOutOfProcessVerifierService(): TransactionVerifierService { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt index 4be46a59f5..a44cf1c4a5 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt @@ -279,7 +279,6 @@ class InMemoryMessagingNetwork( _sentMessages.onNext(transfer) } - @CordaSerializable private data class InMemoryMessage(override val topicSession: TopicSession, override val data: ByteArray, override val uniqueMessageId: UUID, @@ -287,7 +286,6 @@ class InMemoryMessagingNetwork( override fun toString() = "$topicSession#${String(data)}" } - @CordaSerializable private data class InMemoryReceivedMessage(override val topicSession: TopicSession, override val data: ByteArray, override val platformVersion: Int, From 9cec137a31fd3b3bac014c6b62d4196baad8605d Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Wed, 11 Oct 2017 11:17:14 +0100 Subject: [PATCH 156/185] CORDA-702: Don't whitelist certain non-annotated types (#1864) * Don't whitelist arrays of non-serialisable types for RPC. * Don't whitelist enums which have not been annotated as serialisable. --- .../serialization/CordaClassResolver.kt | 5 ++- .../serialization/CordaClassResolverTests.kt | 40 ++++++++++++++++++- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt index 04c43122ec..97c50dcd67 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt @@ -57,12 +57,13 @@ class CordaClassResolver(serializationContext: SerializationContext) : DefaultCl private fun checkClass(type: Class<*>): Registration? { // If call path has disabled whitelisting (see [CordaKryo.register]), just return without checking. if (!whitelistEnabled) return null - // Allow primitives, abstracts and interfaces - if (type.isPrimitive || type == Any::class.java || isAbstract(type.modifiers) || type == String::class.java) return null // If array, recurse on element type if (type.isArray) return checkClass(type.componentType) // Specialised enum entry, so just resolve the parent Enum type since cannot annotate the specialised entry. if (!type.isEnum && Enum::class.java.isAssignableFrom(type)) return checkClass(type.superclass) + // Allow primitives, abstracts and interfaces. Note that we can also create abstract Enum types, + // but we don't want to whitelist those here. + if (type.isPrimitive || type == Any::class.java || type == String::class.java || (!type.isEnum && isAbstract(type.modifiers))) return null // It's safe to have the Class already, since Kryo loads it with initialisation off. // If we use a whitelist with blacklisting capabilities, whitelist.hasListed(type) may throw an IllegalStateException if input class is blacklisted. // Thus, blacklisting precedes annotation checking. diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt index 74a3d574ae..e684ef1f29 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt @@ -34,6 +34,23 @@ enum class Foo { abstract val value: Int } +enum class BadFood { + Mud { + override val value = -1 + }; + + abstract val value: Int +} + +@CordaSerializable +enum class Simple { + Easy +} + +enum class BadSimple { + Nasty +} + @CordaSerializable open class Element @@ -106,17 +123,36 @@ class CordaClassResolverTests { @Test fun `Annotation on enum works for specialised entries`() { - // TODO: Remove this suppress when we upgrade to kotlin 1.1 or when JetBrain fixes the bug. - @Suppress("UNSUPPORTED_FEATURE") CordaClassResolver(emptyWhitelistContext).getRegistration(Foo.Bar::class.java) } + @Test(expected = KryoException::class) + fun `Unannotated specialised enum does not work`() { + CordaClassResolver(emptyWhitelistContext).getRegistration(BadFood.Mud::class.java) + } + + @Test + fun `Annotation on simple enum works`() { + CordaClassResolver(emptyWhitelistContext).getRegistration(Simple.Easy::class.java) + } + + @Test(expected = KryoException::class) + fun `Unannotated simple enum does not work`() { + CordaClassResolver(emptyWhitelistContext).getRegistration(BadSimple.Nasty::class.java) + } + @Test fun `Annotation on array element works`() { val values = arrayOf(Element()) CordaClassResolver(emptyWhitelistContext).getRegistration(values.javaClass) } + @Test(expected = KryoException::class) + fun `Unannotated array elements do not work`() { + val values = arrayOf(NotSerializable()) + CordaClassResolver(emptyWhitelistContext).getRegistration(values.javaClass) + } + @Test fun `Annotation not needed on abstract class`() { CordaClassResolver(emptyWhitelistContext).getRegistration(AbstractClass::class.java) From 15d29d79824a177cb6acc7ee86f2c3e79ec4ecaf Mon Sep 17 00:00:00 2001 From: Clinton Date: Wed, 11 Oct 2017 12:41:11 +0100 Subject: [PATCH 157/185] Fixed the dozens of artifactory warnings at gradle initialisation. (#1862) * Fixed the dozens of artifactory warnings at gradle initialisation. --- build.gradle | 8 ++++++-- constants.properties | 2 +- .../net/corda/plugins/ProjectPublishExtension.groovy | 7 +++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 31740da52f..ca5904ba95 100644 --- a/build.gradle +++ b/build.gradle @@ -293,12 +293,16 @@ artifactory { publish { contextUrl = 'https://ci-artifactory.corda.r3cev.com/artifactory' repository { - repoKey = 'corda-releases' + repoKey = 'corda-dev' username = 'teamcity' password = System.getenv('CORDA_ARTIFACTORY_PASSWORD') } + defaults { - publications('corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'cordform-common', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver', 'corda-node-driver', 'corda-confidential-identities') + // Root project applies the plugin (for this block) but does not need to be published + if(project != rootProject) { + publications(project.extensions.publish.name()) + } } } } diff --git a/constants.properties b/constants.properties index f0fbe73c08..deebebf5d5 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=2.0.2 +gradlePluginsVersion=2.0.3 kotlinVersion=1.1.50 guavaVersion=21.0 bouncycastleVersion=1.57 diff --git a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/ProjectPublishExtension.groovy b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/ProjectPublishExtension.groovy index ee978bdbb8..97029028e3 100644 --- a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/ProjectPublishExtension.groovy +++ b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/ProjectPublishExtension.groovy @@ -15,6 +15,13 @@ class ProjectPublishExtension { task.setPublishName(name) } + /** + * Get the publishing name for this project. + */ + String name() { + return task.getPublishName() + } + /** * True when we do not want to publish default Java components */ From d0d0f132df43e1a550c154003db52e76580d3489 Mon Sep 17 00:00:00 2001 From: Alberto Arri <30873160+al-r3@users.noreply.github.com> Date: Wed, 11 Oct 2017 12:54:30 +0100 Subject: [PATCH 158/185] Create nodeInfoDirectory in NodeInfoWatcher initialization, and make NodeInfoWatcher logging less verbose and less frequent (#1857) Let NodeInfoWatcher create the directory it wants to poll at startup. Also log failure in creating the directory, but don't log anything if it can be found at poll time. --- .../services/network/NodeInfoWatcherTest.kt | 7 ++--- .../node/services/network/NodeInfoWatcher.kt | 27 +++++++++++++------ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt index 0f102a5e79..97e2deb364 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt @@ -56,11 +56,12 @@ class NodeInfoWatcherTest : NodeBasedTest() { @Test fun `save a NodeInfo`() { - assertEquals(0, folder.root.list().size) + assertEquals(0, folder.root.list().filter { it.matches(nodeInfoFileRegex) }.size) NodeInfoWatcher.saveToFile(folder.root.toPath(), nodeInfo, keyManagementService) - assertEquals(1, folder.root.list().size) - val fileName = folder.root.list()[0] + val nodeInfoFiles = folder.root.list().filter { it.matches(nodeInfoFileRegex) } + assertEquals(1, nodeInfoFiles.size) + val fileName = nodeInfoFiles.first() assertTrue(fileName.matches(nodeInfoFileRegex)) val file = (folder.root.path / fileName).toFile() // Just check that something is written, another tests verifies that the written value can be read back. 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 aefd79c2d1..65ef358c6d 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 @@ -12,6 +12,7 @@ import net.corda.core.utilities.seconds import rx.Observable import rx.Scheduler import rx.schedulers.Schedulers +import java.io.IOException import java.nio.file.Path import java.util.concurrent.TimeUnit import kotlin.streams.toList @@ -32,7 +33,8 @@ class NodeInfoWatcher(private val nodePath: Path, private val scheduler: Scheduler = Schedulers.io()) { private val nodeInfoDirectory = nodePath / CordformNode.NODE_INFO_DIRECTORY - private val pollFrequencyMsec: Long + private val pollFrequencyMsec: Long = maxOf(pollFrequencyMsec, 5.seconds.toMillis()) + private val successfullyProcessedFiles = mutableSetOf() companion object { private val logger = loggerFor() @@ -61,7 +63,13 @@ class NodeInfoWatcher(private val nodePath: Path, } init { - this.pollFrequencyMsec = maxOf(pollFrequencyMsec, 5.seconds.toMillis()) + if (!nodeInfoDirectory.isDirectory()) { + try { + nodeInfoDirectory.createDirectories() + } catch (e: IOException) { + logger.info("Failed to create $nodeInfoDirectory", e) + } + } } /** @@ -71,8 +79,7 @@ class NodeInfoWatcher(private val nodePath: Path, * 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 [NodeInfo]s, there is no guarantee that the same value isn't returned more - * than once. + * @return an [Observable] returning [NodeInfo]s, at most one [NodeInfo] is returned for each processed file. */ fun nodeInfoUpdates(): Observable { return Observable.interval(pollFrequencyMsec, TimeUnit.MILLISECONDS, scheduler) @@ -87,16 +94,20 @@ class NodeInfoWatcher(private val nodePath: Path, */ private fun loadFromDirectory(): List { if (!nodeInfoDirectory.isDirectory()) { - logger.info("$nodeInfoDirectory isn't a Directory, not loading NodeInfo from files") return emptyList() } val result = nodeInfoDirectory.list { paths -> - paths.filter { it.isRegularFile() } - .map { processFile(it) } + paths.filter { it !in successfullyProcessedFiles } + .filter { it.isRegularFile() } + .map { path -> + processFile(path)?.apply { successfullyProcessedFiles.add(path) } + } .toList() .filterNotNull() } - logger.info("Successfully read ${result.size} NodeInfo files.") + if (result.isNotEmpty()) { + logger.info("Successfully read ${result.size} NodeInfo files from disk.") + } return result } From 3fdc69e54199b9eedddc82b597c95c73870cc998 Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Wed, 11 Oct 2017 14:33:20 +0100 Subject: [PATCH 159/185] Fix coin selection with Flow-friendly sleep (#1847) --- .../kotlin/net/corda/core/flows/FlowLogic.kt | 31 ++++ .../corda/core/internal/FlowStateMachine.kt | 5 + docs/source/changelog.rst | 6 + finance/build.gradle | 3 + .../cash/selection/CashSelectionH2Impl.kt | 147 +++++++++--------- .../contracts/asset/CashSelectionH2Test.kt | 40 +++++ .../services/statemachine/FlowIORequest.kt | 6 + .../statemachine/FlowStateMachineImpl.kt | 38 ++--- .../statemachine/StateMachineManager.kt | 6 + .../net/corda/node/InteractiveShellTest.kt | 4 +- 10 files changed, 191 insertions(+), 95 deletions(-) create mode 100644 finance/src/test/kotlin/net/corda/finance/contracts/asset/CashSelectionH2Test.kt diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt index 419c918753..0239c81f20 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt @@ -1,6 +1,7 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable +import co.paralleluniverse.strands.Strand import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate @@ -16,6 +17,8 @@ import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.debug import org.slf4j.Logger +import java.time.Duration +import java.time.Instant /** * A sub-class of [FlowLogic] implements a flow using direct, straight line blocking code. Thus you @@ -43,6 +46,34 @@ abstract class FlowLogic { /** This is where you should log things to. */ val logger: Logger get() = stateMachine.logger + companion object { + /** + * Return the outermost [FlowLogic] instance, or null if not in a flow. + */ + @JvmStatic + val currentTopLevel: FlowLogic<*>? get() = (Strand.currentStrand() as? FlowStateMachine<*>)?.logic + + /** + * If on a flow, suspends the flow and only wakes it up after at least [duration] time has passed. Otherwise, + * just sleep for [duration]. This sleep function is not designed to aid scheduling, for which you should + * consider using [SchedulableState]. It is designed to aid with managing contention for which you have not + * managed via another means. + * + * Warning: long sleeps and in general long running flows are highly discouraged, as there is currently no + * support for flow migration! This method will throw an exception if you attempt to sleep for longer than + * 5 minutes. + */ + @Suspendable + @JvmStatic + @Throws(FlowException::class) + fun sleep(duration: Duration) { + if (duration.compareTo(Duration.ofMinutes(5)) > 0) { + throw FlowException("Attempt to sleep for longer than 5 minutes is not supported. Consider using SchedulableState.") + } + (Strand.currentStrand() as? FlowStateMachine<*>)?.sleepUntil(Instant.now() + duration) ?: Strand.sleep(duration.toMillis()) + } + } + /** * Returns a wrapped [java.util.UUID] object that identifies this state machine run (i.e. subflows have the same * identifier as their parents). diff --git a/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt b/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt index 261af49f02..a2b0e2fd15 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt @@ -10,6 +10,7 @@ import net.corda.core.node.ServiceHub import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.UntrustworthyData import org.slf4j.Logger +import java.time.Instant /** This is an internal interface that is implemented by code in the node module. You should look at [FlowLogic]. */ interface FlowStateMachine { @@ -35,6 +36,9 @@ interface FlowStateMachine { @Suspendable fun waitForLedgerCommit(hash: SecureHash, sessionFlow: FlowLogic<*>): SignedTransaction + @Suspendable + fun sleepUntil(until: Instant) + fun checkFlowPermission(permissionName: String, extraAuditData: Map) fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map) @@ -45,6 +49,7 @@ interface FlowStateMachine { @Suspendable fun persistFlowStackSnapshot(flowClass: Class>) + val logic: FlowLogic val serviceHub: ServiceHub val logger: Logger val id: StateMachineRunId diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 8b17db03ac..0931e57561 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -36,6 +36,12 @@ UNRELEASED * Cordformation node building DSL can have an additional parameter `configFile` with the path to a properties file to be appended to node.conf. +* ``FlowLogic`` now has a static method called ``sleep`` which can be used in certain circumstances to help with resolving + contention over states in flows. This should be used in place of any other sleep primitive since these are not compatible + with flows and their use will be prevented at some point in the future. Pay attention to the warnings and limitations + described in the documentation for this method. This helps resolve a bug in ``Cash`` coin selection. + A new static property `currentTopLevel` returns the top most `FlowLogic` instance, or null if not in a flow. + * ``CordaService`` annotated classes should be upgraded to take a constructor parameter of type ``AppServiceHub`` which allows services to start flows marked with the ``StartableByService`` annotation. For backwards compatability service classes with only ``ServiceHub`` constructors will still work. diff --git a/finance/build.gradle b/finance/build.gradle index c642969785..662e7c9aa5 100644 --- a/finance/build.gradle +++ b/finance/build.gradle @@ -32,6 +32,9 @@ dependencies { testCompile project(':test-utils') testCompile project(path: ':core', configuration: 'testArtifacts') testCompile "junit:junit:$junit_version" + + // AssertJ: for fluent assertions for testing + testCompile "org.assertj:assertj-core:$assertj_version" } configurations { 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 a447827205..8be96121c8 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 @@ -1,12 +1,12 @@ package net.corda.finance.contracts.asset.cash.selection import co.paralleluniverse.fibers.Suspendable -import co.paralleluniverse.strands.Strand import net.corda.core.contracts.Amount import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionState import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FlowLogic import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.ServiceHub @@ -34,8 +34,9 @@ class CashSelectionH2Impl : CashSelection { } // coin selection retry loop counter, sleep (msecs) and lock for selecting states - private val MAX_RETRIES = 5 + private val MAX_RETRIES = 8 private val RETRY_SLEEP = 100 + private val RETRY_CAP = 2000 private val spendLock: ReentrantLock = ReentrantLock() /** @@ -73,78 +74,84 @@ class CashSelectionH2Impl : CashSelection { // 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries) for (retryCount in 1..MAX_RETRIES) { - - spendLock.withLock { - val statement = services.jdbcSession().createStatement() - try { - statement.execute("CALL SET(@t, CAST(0 AS BIGINT));") - - // we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null) - // the softLockReserve update will detect whether we try to lock states locked by others - val selectJoin = """ - SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id - 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 ccs.ccy_code = '${amount.token}' and @t < ${amount.quantity} - AND (vs.lock_id = '$lockId' OR vs.lock_id is null) - """ + - (if (notary != null) - " AND vs.notary_name = '${notary.name}'" else "") + - (if (onlyFromIssuerParties.isNotEmpty()) - " AND ccs.issuer_key IN ($issuerKeysStr)" else "") + - (if (withIssuerRefs.isNotEmpty()) - " AND ccs.issuer_ref IN ($issuerRefsStr)" else "") - - // Retrieve spendable state refs - val rs = statement.executeQuery(selectJoin) + if (!attemptSpend(services, amount, lockId, notary, onlyFromIssuerParties, issuerKeysStr, withIssuerRefs, issuerRefsStr, stateAndRefs)) { + log.warn("Coin selection failed on attempt $retryCount") + // TODO: revisit the back off strategy for contended spending. + if (retryCount != MAX_RETRIES) { stateAndRefs.clear() - log.debug(selectJoin) - var totalPennies = 0L - while (rs.next()) { - val txHash = SecureHash.parse(rs.getString(1)) - val index = rs.getInt(2) - val stateRef = StateRef(txHash, index) - val state = rs.getBytes(3).deserialize>(context = SerializationDefaults.STORAGE_CONTEXT) - val pennies = rs.getLong(4) - totalPennies = rs.getLong(5) - val rowLockId = rs.getString(6) - stateAndRefs.add(StateAndRef(state, stateRef)) - log.trace { "ROW: $rowLockId ($lockId): $stateRef : $pennies ($totalPennies)" } - } - - if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) { - // we should have a minimum number of states to satisfy our selection `amount` criteria - log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs") - - // With the current single threaded state machine available states are guaranteed to lock. - // TODO However, we will have to revisit these methods in the future multi-threaded. - services.vaultService.softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet()) - return stateAndRefs - } - log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}") - // retry as more states may become available - } catch (e: SQLException) { - log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId] - $e. - """) - } catch (e: StatesNotAvailableException) { // Should never happen with single threaded state machine - stateAndRefs.clear() - log.warn(e.message) - // retry only if there are locked states that may become available again (or consumed with change) - } finally { - statement.close() + val durationMillis = (minOf(RETRY_SLEEP.shl(retryCount), RETRY_CAP / 2) * (1.0 + Math.random())).toInt() + FlowLogic.sleep(durationMillis.millis) + } else { + log.warn("Insufficient spendable states identified for $amount") } - } - - log.warn("Coin selection failed on attempt $retryCount") - // TODO: revisit the back off strategy for contended spending. - if (retryCount != MAX_RETRIES) { - Strand.sleep(RETRY_SLEEP * retryCount.toLong()) + } else { + break } } - - log.warn("Insufficient spendable states identified for $amount") return stateAndRefs } + + private fun attemptSpend(services: ServiceHub, amount: Amount, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set, issuerKeysStr: String, withIssuerRefs: Set, issuerRefsStr: String, stateAndRefs: MutableList>): Boolean { + spendLock.withLock { + val statement = services.jdbcSession().createStatement() + try { + statement.execute("CALL SET(@t, CAST(0 AS BIGINT));") + + // we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null) + // the softLockReserve update will detect whether we try to lock states locked by others + val selectJoin = """ + SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id + 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 ccs.ccy_code = '${amount.token}' and @t < ${amount.quantity} + AND (vs.lock_id = '$lockId' OR vs.lock_id is null) + """ + + (if (notary != null) + " AND vs.notary_name = '${notary.name}'" else "") + + (if (onlyFromIssuerParties.isNotEmpty()) + " AND ccs.issuer_key IN ($issuerKeysStr)" else "") + + (if (withIssuerRefs.isNotEmpty()) + " AND ccs.issuer_ref IN ($issuerRefsStr)" else "") + + // Retrieve spendable state refs + val rs = statement.executeQuery(selectJoin) + log.debug(selectJoin) + var totalPennies = 0L + while (rs.next()) { + val txHash = SecureHash.parse(rs.getString(1)) + val index = rs.getInt(2) + val stateRef = StateRef(txHash, index) + val state = rs.getBytes(3).deserialize>(context = SerializationDefaults.STORAGE_CONTEXT) + val pennies = rs.getLong(4) + totalPennies = rs.getLong(5) + val rowLockId = rs.getString(6) + stateAndRefs.add(StateAndRef(state, stateRef)) + log.trace { "ROW: $rowLockId ($lockId): $stateRef : $pennies ($totalPennies)" } + } + + if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) { + // we should have a minimum number of states to satisfy our selection `amount` criteria + log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs") + + // With the current single threaded state machine available states are guaranteed to lock. + // TODO However, we will have to revisit these methods in the future multi-threaded. + services.vaultService.softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet()) + return true + } + log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}") + // retry as more states may become available + } catch (e: SQLException) { + log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId] + $e. + """) + } catch (e: StatesNotAvailableException) { // Should never happen with single threaded state machine + log.warn(e.message) + // retry only if there are locked states that may become available again (or consumed with change) + } finally { + statement.close() + } + } + return false + } } \ No newline at end of file diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashSelectionH2Test.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashSelectionH2Test.kt new file mode 100644 index 0000000000..ef85dead0a --- /dev/null +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashSelectionH2Test.kt @@ -0,0 +1,40 @@ +package net.corda.finance.contracts.asset + +import net.corda.core.utilities.getOrThrow +import net.corda.finance.DOLLARS +import net.corda.finance.flows.CashException +import net.corda.finance.flows.CashPaymentFlow +import net.corda.testing.chooseIdentity +import net.corda.testing.node.MockNetwork +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Test + + +class CashSelectionH2Test { + + @Test + fun `check does not hold connection over retries`() { + val mockNet = MockNetwork(threadPerNode = true) + try { + val notaryNode = mockNet.createNotaryNode() + val bankA = mockNet.createNode(configOverrides = { existingConfig -> + // Tweak connections to be minimal to make this easier (1 results in a hung node during start up, so use 2 connections). + existingConfig.dataSourceProperties.setProperty("maximumPoolSize", "2") + existingConfig + }) + + mockNet.startNodes() + + // Start more cash spends than we have connections. If spend leaks a connection on retry, we will run out of connections. + val flow1 = bankA.services.startFlow(CashPaymentFlow(amount = 100.DOLLARS, anonymous = false, recipient = notaryNode.info.chooseIdentity())) + val flow2 = bankA.services.startFlow(CashPaymentFlow(amount = 100.DOLLARS, anonymous = false, recipient = notaryNode.info.chooseIdentity())) + val flow3 = bankA.services.startFlow(CashPaymentFlow(amount = 100.DOLLARS, anonymous = false, recipient = notaryNode.info.chooseIdentity())) + + assertThatThrownBy { flow1.resultFuture.getOrThrow() }.isInstanceOf(CashException::class.java) + assertThatThrownBy { flow2.resultFuture.getOrThrow() }.isInstanceOf(CashException::class.java) + assertThatThrownBy { flow3.resultFuture.getOrThrow() }.isInstanceOf(CashException::class.java) + } finally { + mockNet.stopNodes() + } + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowIORequest.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowIORequest.kt index cd56d786ad..bd29525072 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowIORequest.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowIORequest.kt @@ -2,6 +2,7 @@ package net.corda.node.services.statemachine import co.paralleluniverse.fibers.Suspendable import net.corda.core.crypto.SecureHash +import java.time.Instant interface FlowIORequest { // This is used to identify where we suspended, in case of message mismatch errors and other things where we @@ -112,4 +113,9 @@ data class WaitForLedgerCommit(val hash: SecureHash, val fiber: FlowStateMachine override fun shouldResume(message: ExistingSessionMessage, session: FlowSessionInternal): Boolean = message is ErrorSessionEnd } +data class Sleep(val until: Instant, val fiber: FlowStateMachineImpl<*>) : FlowIORequest { + @Transient + override val stackTraceInCaseOfProblems: StackSnapshot = StackSnapshot() +} + class StackSnapshot : Throwable("This is a stack trace to help identify the source of the underlying problem") 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 c61ef66ead..019940363b 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,13 +12,9 @@ import net.corda.core.crypto.random63BitValue import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate -import net.corda.core.internal.FlowStateMachine -import net.corda.core.internal.abbreviate +import net.corda.core.internal.* import net.corda.core.internal.concurrent.OpenFuture import net.corda.core.internal.concurrent.openFuture -import net.corda.core.internal.isRegularFile -import net.corda.core.internal.staticField -import net.corda.core.internal.uncheckedCast import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.* import net.corda.node.services.api.FlowAppAuditEvent @@ -32,13 +28,15 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import java.nio.file.Paths import java.sql.SQLException +import java.time.Duration +import java.time.Instant import java.util.* import java.util.concurrent.TimeUnit class FlowPermissionException(message: String) : FlowException(message) class FlowStateMachineImpl(override val id: StateMachineRunId, - val logic: FlowLogic, + override val logic: FlowLogic, scheduler: FiberScheduler, override val flowInitiator: FlowInitiator, // Store the Party rather than the full cert path with PartyAndCertificate @@ -52,23 +50,6 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, * Return the current [FlowStateMachineImpl] or null if executing outside of one. */ fun currentStateMachine(): FlowStateMachineImpl<*>? = Strand.currentStrand() as? FlowStateMachineImpl<*> - - /** - * Provide a mechanism to sleep within a Strand without locking any transactional state - */ - // TODO: inlined due to an intermittent Quasar error (to be fully investigated) - @Suppress("NOTHING_TO_INLINE") - @Suspendable - inline fun sleep(millis: Long) { - if (currentStateMachine() != null) { - val db = DatabaseTransactionManager.dataSource - DatabaseTransactionManager.current().commit() - DatabaseTransactionManager.current().close() - Strand.sleep(millis) - DatabaseTransactionManager.dataSource = db - DatabaseTransactionManager.newTransaction() - } else Strand.sleep(millis) - } } // These fields shouldn't be serialised, so they are marked @Transient. @@ -259,6 +240,13 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, throw IllegalStateException("We were resumed after waiting for $hash but it wasn't found in our local storage") } + // Provide a mechanism to sleep within a Strand without locking any transactional state. + // This checkpoints, since we cannot undo any database writes up to this point. + @Suspendable + override fun sleepUntil(until: Instant) { + suspend(Sleep(until, this)) + } + // TODO Dummy implementation of access to application specific permission controls and audit logging override fun checkFlowPermission(permissionName: String, extraAuditData: Map) { val permissionGranted = true // TODO define permission control service on ServiceHubInternal and actually check authorization. @@ -494,6 +482,10 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, } } + if (exceptionDuringSuspend == null && ioRequest is Sleep) { + // Sleep on the fiber. This will not sleep if it's in the past. + Strand.sleep(Duration.between(Instant.now(), ioRequest.until).toNanos(), TimeUnit.NANOSECONDS) + } createTransaction() // TODO Now that we're throwing outside of the suspend the FlowLogic can catch it. We need Quasar to terminate // the fiber when exceptions occur inside a suspend. diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index 42fd677f65..2215354b07 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -584,6 +584,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, when (ioRequest) { is SendRequest -> processSendRequest(ioRequest) is WaitForLedgerCommit -> processWaitForCommitRequest(ioRequest) + is Sleep -> processSleepRequest(ioRequest) } } @@ -621,6 +622,11 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } } + private fun processSleepRequest(ioRequest: Sleep) { + // Resume the fiber now we have checkpointed, so we can sleep on the Fiber. + resumeFiber(ioRequest.fiber) + } + private fun sendSessionMessage(party: Party, message: SessionMessage, fiber: FlowStateMachineImpl<*>? = null, retryId: Long? = null) { val partyInfo = serviceHub.networkMapCache.getPartyInfo(party) ?: throw IllegalArgumentException("Don't know about party $party") diff --git a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt index f694eec66c..3d4005d6fc 100644 --- a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt +++ b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt @@ -52,7 +52,7 @@ class InteractiveShellTest { private fun check(input: String, expected: String) { var output: DummyFSM? = null InteractiveShell.runFlowFromString({ DummyFSM(it as FlowA).apply { output = this } }, input, FlowA::class.java, om) - assertEquals(expected, output!!.logic.a, input) + assertEquals(expected, output!!.flowA.a, input) } @Test @@ -83,5 +83,5 @@ class InteractiveShellTest { @Test fun party() = check("party: \"${MEGA_CORP.name}\"", MEGA_CORP.name.toString()) - class DummyFSM(val logic: FlowA) : FlowStateMachine by mock() + class DummyFSM(val flowA: FlowA) : FlowStateMachine by mock() } From e232d111ea153da027ccb9b13dc0a3a7b3ab9632 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Wed, 11 Oct 2017 14:46:04 +0100 Subject: [PATCH 160/185] Applies release-V1 vault docs fixes to master. --- docs/source/api-persistence.rst | 2 + docs/source/api-vault-query.rst | 219 +++++++++++++++++++++++--------- 2 files changed, 161 insertions(+), 60 deletions(-) diff --git a/docs/source/api-persistence.rst b/docs/source/api-persistence.rst index 84afad26a5..add8f1b785 100644 --- a/docs/source/api-persistence.rst +++ b/docs/source/api-persistence.rst @@ -7,6 +7,8 @@ API: Persistence ================ +.. contents:: + Corda offers developers the option to expose all or some part of a contract state to an *Object Relational Mapping* (ORM) tool to be persisted in a RDBMS. The purpose of this is to assist *vault* development by effectively indexing persisted contract states held in the vault for the purpose of running queries over them and to allow relational joins diff --git a/docs/source/api-vault-query.rst b/docs/source/api-vault-query.rst index 0b9944948d..a525a8b413 100644 --- a/docs/source/api-vault-query.rst +++ b/docs/source/api-vault-query.rst @@ -1,33 +1,42 @@ API: Vault Query ================ -Corda has been architected from the ground up to encourage usage of industry standard, proven query frameworks and libraries for accessing RDBMS backed transactional stores (including the Vault). +.. contents:: + +Overview +-------- +Corda has been architected from the ground up to encourage usage of industry standard, proven query frameworks and +libraries for accessing RDBMS backed transactional stores (including the Vault). Corda provides a number of flexible query mechanisms for accessing the Vault: - Vault Query API -- using a JDBC session (as described in :ref:`Persistence `) -- custom JPA_/JPQL_ queries -- custom 3rd party Data Access frameworks such as `Spring Data `_ +- Using a JDBC session (as described in :ref:`Persistence `) +- Custom JPA_/JPQL_ queries +- Custom 3rd party Data Access frameworks such as `Spring Data `_ -The majority of query requirements can be satisfied by using the Vault Query API, which is exposed via the ``VaultService`` for use directly by flows: +The majority of query requirements can be satisfied by using the Vault Query API, which is exposed via the +``VaultService`` for use directly by flows: .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/node/services/VaultService.kt :language: kotlin :start-after: DOCSTART VaultQueryAPI :end-before: DOCEND VaultQueryAPI + :dedent: 4 -and via ``CordaRPCOps`` for use by RPC client applications: +And via ``CordaRPCOps`` for use by RPC client applications: .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt :language: kotlin :start-after: DOCSTART VaultQueryByAPI :end-before: DOCEND VaultQueryByAPI + :dedent: 4 .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt :language: kotlin :start-after: DOCSTART VaultTrackByAPI :end-before: DOCEND VaultTrackByAPI + :dedent: 4 Helper methods are also provided with default values for arguments: @@ -35,51 +44,84 @@ Helper methods are also provided with default values for arguments: :language: kotlin :start-after: DOCSTART VaultQueryAPIHelpers :end-before: DOCEND VaultQueryAPIHelpers + :dedent: 4 .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt :language: kotlin :start-after: DOCSTART VaultTrackAPIHelpers :end-before: DOCEND VaultTrackAPIHelpers + :dedent: 4 -The API provides both static (snapshot) and dynamic (snapshot with streaming updates) methods for a defined set of filter criteria. +The API provides both static (snapshot) and dynamic (snapshot with streaming updates) methods for a defined set of +filter criteria: - Use ``queryBy`` to obtain a only current snapshot of data (for a given ``QueryCriteria``) - Use ``trackBy`` to obtain a both a current snapshot and a future stream of updates (for a given ``QueryCriteria``) .. note:: Streaming updates are only filtered based on contract type and state status (UNCONSUMED, CONSUMED, ALL) -Simple pagination (page number and size) and sorting (directional ordering using standard or custom property attributes) is also specifiable. -Defaults are defined for Paging (pageNumber = 1, pageSize = 200) and Sorting (direction = ASC). +Simple pagination (page number and size) and sorting (directional ordering using standard or custom property +attributes) is also specifiable. Defaults are defined for paging (pageNumber = 1, pageSize = 200) and sorting +(direction = ASC). -The ``QueryCriteria`` interface provides a flexible mechanism for specifying different filtering criteria, including and/or composition and a rich set of operators to include: binary logical (AND, OR), comparison (LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL), equality (EQUAL, NOT_EQUAL), likeness (LIKE, NOT_LIKE), nullability (IS_NULL, NOT_NULL), and collection based (IN, NOT_IN). Standard SQL-92 aggregate functions (SUM, AVG, MIN, MAX, COUNT) are also supported. +The ``QueryCriteria`` interface provides a flexible mechanism for specifying different filtering criteria, including +and/or composition and a rich set of operators to include: + +* Binary logical (AND, OR) +* Comparison (LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL) +* Equality (EQUAL, NOT_EQUAL) +* Likeness (LIKE, NOT_LIKE) +* Nullability (IS_NULL, NOT_NULL) +* Collection based (IN, NOT_IN) +* Standard SQL-92 aggregate functions (SUM, AVG, MIN, MAX, COUNT) There are four implementations of this interface which can be chained together to define advanced filters. -1. ``VaultQueryCriteria`` provides filterable criteria on attributes within the Vault states table: status (UNCONSUMED, CONSUMED), state reference(s), contract state type(s), notaries, soft locked states, timestamps (RECORDED, CONSUMED). +1. ``VaultQueryCriteria`` provides filterable criteria on attributes within the Vault states table: status (UNCONSUMED, + CONSUMED), state reference(s), contract state type(s), notaries, soft locked states, timestamps (RECORDED, CONSUMED). - .. note:: Sensible defaults are defined for frequently used attributes (status = UNCONSUMED, always include soft locked states). + .. note:: Sensible defaults are defined for frequently used attributes (status = UNCONSUMED, always include soft + locked states). -2. ``FungibleAssetQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``FungibleAsset`` contract state interface, used to represent assets that are fungible, countable and issued by a specific party (eg. ``Cash.State`` and ``CommodityContract.State`` in the Corda finance module). Filterable attributes include: participants(s), owner(s), quantity, issuer party(s) and issuer reference(s). +2. ``FungibleAssetQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core + ``FungibleAsset`` contract state interface, used to represent assets that are fungible, countable and issued by a + specific party (eg. ``Cash.State`` and ``CommodityContract.State`` in the Corda finance module). Filterable + attributes include: participants(s), owner(s), quantity, issuer party(s) and issuer reference(s). - .. note:: All contract states that extend the ``FungibleAsset`` now automatically persist that interfaces common state attributes to the **vault_fungible_states** table. + .. note:: All contract states that extend the ``FungibleAsset`` now automatically persist that interfaces common + state attributes to the **vault_fungible_states** table. -3. ``LinearStateQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``LinearState`` and ``DealState`` contract state interfaces, used to represent entities that continuously supercede themselves, all of which share the same *linearId* (eg. trade entity states such as the ``IRSState`` defined in the SIMM valuation demo). Filterable attributes include: participant(s), linearId(s), uuid(s), and externalId(s). +3. ``LinearStateQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``LinearState`` + and ``DealState`` contract state interfaces, used to represent entities that continuously supersede themselves, all + of which share the same ``linearId`` (e.g. trade entity states such as the ``IRSState`` defined in the SIMM + valuation demo). Filterable attributes include: participant(s), linearId(s), uuid(s), and externalId(s). - .. note:: All contract states that extend ``LinearState`` or ``DealState`` now automatically persist those interfaces common state attributes to the **vault_linear_states** table. + .. note:: All contract states that extend ``LinearState`` or ``DealState`` now automatically persist those + interfaces common state attributes to the **vault_linear_states** table. -4. ``VaultCustomQueryCriteria`` provides the means to specify one or many arbitrary expressions on attributes defined by a custom contract state that implements its own schema as described in the :doc:`Persistence ` documentation and associated examples. Custom criteria expressions are expressed using one of several type-safe ``CriteriaExpression``: BinaryLogical, Not, ColumnPredicateExpression, AggregateFunctionExpression. The ``ColumnPredicateExpression`` allows for specification arbitrary criteria using the previously enumerated operator types. The ``AggregateFunctionExpression`` allows for the specification of an aggregate function type (sum, avg, max, min, count) with optional grouping and sorting. Furthermore, a rich DSL is provided to enable simple construction of custom criteria using any combination of ``ColumnPredicate``. See the ``Builder`` object in ``QueryCriteriaUtils`` for a complete specification of the DSL. +4. ``VaultCustomQueryCriteria`` provides the means to specify one or many arbitrary expressions on attributes defined + by a custom contract state that implements its own schema as described in the :doc:`Persistence ` + documentation and associated examples. Custom criteria expressions are expressed using one of several type-safe + ``CriteriaExpression``: BinaryLogical, Not, ColumnPredicateExpression, AggregateFunctionExpression. The + ``ColumnPredicateExpression`` allows for specification arbitrary criteria using the previously enumerated operator + types. The ``AggregateFunctionExpression`` allows for the specification of an aggregate function type (sum, avg, + max, min, count) with optional grouping and sorting. Furthermore, a rich DSL is provided to enable simple + construction of custom criteria using any combination of ``ColumnPredicate``. See the ``Builder`` object in + ``QueryCriteriaUtils`` for a complete specification of the DSL. - .. note:: custom contract schemas are automatically registered upon node startup for CorDapps. Please refer to - :doc:`Persistence ` for mechanisms of registering custom schemas for different testing purposes. + .. note:: Custom contract schemas are automatically registered upon node startup for CorDapps. Please refer to + :doc:`Persistence ` for mechanisms of registering custom schemas for different testing + purposes. All ``QueryCriteria`` implementations are composable using ``and`` and ``or`` operators. All ``QueryCriteria`` implementations provide an explicitly specifiable set of common attributes: 1. State status attribute (``Vault.StateStatus``), which defaults to filtering on UNCONSUMED states. - When chaining several criterias using AND / OR, the last value of this attribute will override any previous. -2. Contract state types (``>``), which will contain at minimum one type (by default this will be ``ContractState`` which resolves to all state types). - When chaining several criteria using ``and`` and ``or`` operators, all specified contract state types are combined into a single set. + When chaining several criterias using AND / OR, the last value of this attribute will override any previous +2. Contract state types (``>``), which will contain at minimum one type (by default this + will be ``ContractState`` which resolves to all state types). When chaining several criteria using ``and`` and + ``or`` operators, all specified contract state types are combined into a single set An example of a custom query is illustrated here: @@ -87,20 +129,30 @@ An example of a custom query is illustrated here: :language: kotlin :start-after: DOCSTART VaultQueryExample20 :end-before: DOCEND VaultQueryExample20 + :dedent: 12 -.. note:: Custom contract states that implement the ``Queryable`` interface may now extend common schemas types ``FungiblePersistentState`` or, ``LinearPersistentState``. Previously, all custom contracts extended the root ``PersistentState`` class and defined repeated mappings of ``FungibleAsset`` and ``LinearState`` attributes. See ``SampleCashSchemaV2`` and ``DummyLinearStateSchemaV2`` as examples. +.. note:: Custom contract states that implement the ``Queryable`` interface may now extend common schemas types + ``FungiblePersistentState`` or, ``LinearPersistentState``. Previously, all custom contracts extended the root + ``PersistentState`` class and defined repeated mappings of ``FungibleAsset`` and ``LinearState`` attributes. See + ``SampleCashSchemaV2`` and ``DummyLinearStateSchemaV2`` as examples. Examples of these ``QueryCriteria`` objects are presented below for Kotlin and Java. -.. note:: When specifying the Contract Type as a parameterised type to the QueryCriteria in Kotlin, queries now include all concrete implementations of that type if this is an interface. Previously, it was only possible to query on Concrete types (or the universe of all Contract States). +.. note:: When specifying the ``ContractType`` as a parameterised type to the ``QueryCriteria`` in Kotlin, queries now + include all concrete implementations of that type if this is an interface. Previously, it was only possible to query + on concrete types (or the universe of all ``ContractState``). -The Vault Query API leverages the rich semantics of the underlying JPA Hibernate_ based :doc:`Persistence ` framework adopted by Corda. +The Vault Query API leverages the rich semantics of the underlying JPA Hibernate_ based +:doc:`Persistence ` framework adopted by Corda. .. _Hibernate: https://docs.jboss.org/hibernate/jpa/2.1/api/ -.. note:: Permissioning at the database level will be enforced at a later date to ensure authenticated, role-based, read-only access to underlying Corda tables. +.. note:: Permissioning at the database level will be enforced at a later date to ensure authenticated, role-based, + read-only access to underlying Corda tables. -.. note:: API's now provide ease of use calling semantics from both Java and Kotlin. However, it should be noted that Java custom queries are significantly more verbose due to the use of reflection fields to reference schema attribute types. +.. note:: API's now provide ease of use calling semantics from both Java and Kotlin. However, it should be noted that + Java custom queries are significantly more verbose due to the use of reflection fields to reference schema attribute + types. An example of a custom query in Java is illustrated here: @@ -108,17 +160,24 @@ An example of a custom query in Java is illustrated here: :language: java :start-after: DOCSTART VaultJavaQueryExample3 :end-before: DOCEND VaultJavaQueryExample3 + :dedent: 16 -.. note:: Queries by ``Party`` specify the ``AbstractParty`` which may be concrete or anonymous. In the later case, where an anonymous party does not resolve to an X500Name via the IdentityService, no query results will ever be produced. For performance reasons, queries do not use PublicKey as search criteria. +.. note:: Queries by ``Party`` specify the ``AbstractParty`` which may be concrete or anonymous. In the later case, + 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. Pagination ---------- -The API provides support for paging where large numbers of results are expected (by default, a page size is set to 200 results). -Defining a sensible default page size enables efficient checkpointing within flows, and frees the developer from worrying about pagination where -result sets are expected to be constrained to 200 or fewer entries. Where large result sets are expected (such as using the RPC API for reporting and/or UI display), it is strongly recommended to define a ``PageSpecification`` to correctly process results with efficient memory utilistion. A fail-fast mode is in place to alert API users to the need for pagination where a single query returns more than 200 results and no ``PageSpecification`` -has been supplied. +The API provides support for paging where large numbers of results are expected (by default, a page size is set to 200 +results). Defining a sensible default page size enables efficient checkpointing within flows, and frees the developer +from worrying about pagination where result sets are expected to be constrained to 200 or fewer entries. Where large +result sets are expected (such as using the RPC API for reporting and/or UI display), it is strongly recommended to +define a ``PageSpecification`` to correctly process results with efficient memory utilisation. A fail-fast mode is in +place to alert API users to the need for pagination where a single query returns more than 200 results and no +``PageSpecification`` has been supplied. -.. note:: A pages maximum size ``MAX_PAGE_SIZE`` is defined as ``Int.MAX_VALUE`` and should be used with extreme caution as results returned may exceed your JVM's memory footprint. +.. note:: A pages maximum size ``MAX_PAGE_SIZE`` is defined as ``Int.MAX_VALUE`` and should be used with extreme + caution as results returned may exceed your JVM's memory footprint. Example usage ------------- @@ -126,7 +185,7 @@ Example usage Kotlin ^^^^^^ -**General snapshot queries using** ``VaultQueryCriteria`` +**General snapshot queries using** ``VaultQueryCriteria``: Query for all unconsumed states (simplest query possible): @@ -134,6 +193,7 @@ Query for all unconsumed states (simplest query possible): :language: kotlin :start-after: DOCSTART VaultQueryExample1 :end-before: DOCEND VaultQueryExample1 + :dedent: 12 Query for unconsumed states for some state references: @@ -141,6 +201,7 @@ Query for unconsumed states for some state references: :language: kotlin :start-after: DOCSTART VaultQueryExample2 :end-before: DOCEND VaultQueryExample2 + :dedent: 12 Query for unconsumed states for several contract state types: @@ -148,6 +209,7 @@ Query for unconsumed states for several contract state types: :language: kotlin :start-after: DOCSTART VaultQueryExample3 :end-before: DOCEND VaultQueryExample3 + :dedent: 12 Query for unconsumed states for a given notary: @@ -155,6 +217,7 @@ Query for unconsumed states for a given notary: :language: kotlin :start-after: DOCSTART VaultQueryExample4 :end-before: DOCEND VaultQueryExample4 + :dedent: 12 Query for unconsumed states for a given set of participants: @@ -162,6 +225,7 @@ Query for unconsumed states for a given set of participants: :language: kotlin :start-after: DOCSTART VaultQueryExample5 :end-before: DOCEND VaultQueryExample5 + :dedent: 12 Query for unconsumed states recorded between two time intervals: @@ -169,6 +233,7 @@ Query for unconsumed states recorded between two time intervals: :language: kotlin :start-after: DOCSTART VaultQueryExample6 :end-before: DOCEND VaultQueryExample6 + :dedent: 12 .. note:: This example illustrates usage of a ``Between`` ``ColumnPredicate``. @@ -178,17 +243,21 @@ Query for all states with pagination specification (10 results per page): :language: kotlin :start-after: DOCSTART VaultQueryExample7 :end-before: DOCEND VaultQueryExample7 + :dedent: 12 -.. note:: The result set metadata field `totalStatesAvailable` allows you to further paginate accordingly as demonstrated in the following example. +.. note:: The result set metadata field `totalStatesAvailable` allows you to further paginate accordingly as + demonstrated in the following example. -Query for all states using pagination specification and iterate using `totalStatesAvailable` field until no further pages available: +Query for all states using pagination specification and iterate using `totalStatesAvailable` field until no further +pages available: .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt :language: kotlin :start-after: DOCSTART VaultQueryExamplePaging :end-before: DOCEND VaultQueryExamplePaging + :dedent: 8 -**LinearState and DealState queries using** ``LinearStateQueryCriteria`` +**LinearState and DealState queries using** ``LinearStateQueryCriteria``: Query for unconsumed linear states for given linear ids: @@ -196,6 +265,7 @@ Query for unconsumed linear states for given linear ids: :language: kotlin :start-after: DOCSTART VaultQueryExample8 :end-before: DOCEND VaultQueryExample8 + :dedent: 12 Query for all linear states associated with a linear id: @@ -203,6 +273,7 @@ Query for all linear states associated with a linear id: :language: kotlin :start-after: DOCSTART VaultQueryExample9 :end-before: DOCEND VaultQueryExample9 + :dedent: 12 Query for unconsumed deal states with deals references: @@ -210,6 +281,7 @@ Query for unconsumed deal states with deals references: :language: kotlin :start-after: DOCSTART VaultQueryExample10 :end-before: DOCEND VaultQueryExample10 + :dedent: 12 Query for unconsumed deal states with deals parties: @@ -217,8 +289,9 @@ Query for unconsumed deal states with deals parties: :language: kotlin :start-after: DOCSTART VaultQueryExample11 :end-before: DOCEND VaultQueryExample11 + :dedent: 12 -**FungibleAsset and DealState queries using** ``FungibleAssetQueryCriteria`` +**FungibleAsset and DealState queries using** ``FungibleAssetQueryCriteria``: Query for fungible assets for a given currency: @@ -226,6 +299,7 @@ Query for fungible assets for a given currency: :language: kotlin :start-after: DOCSTART VaultQueryExample12 :end-before: DOCEND VaultQueryExample12 + :dedent: 12 Query for fungible assets for a minimum quantity: @@ -233,19 +307,21 @@ Query for fungible assets for a minimum quantity: :language: kotlin :start-after: DOCSTART VaultQueryExample13 :end-before: DOCEND VaultQueryExample13 + :dedent: 12 .. note:: This example uses the builder DSL. -Query for fungible assets for a specifc issuer party: +Query for fungible assets for a specific issuer party: .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt :language: kotlin :start-after: DOCSTART VaultQueryExample14 :end-before: DOCEND VaultQueryExample14 + :dedent: 12 -**Aggregate Function queries using** ``VaultCustomQueryCriteria`` +**Aggregate Function queries using** ``VaultCustomQueryCriteria``: -.. note:: Query results for aggregate functions are contained in the `otherResults` attribute of a results Page. +.. note:: Query results for aggregate functions are contained in the ``otherResults`` attribute of a results Page. Aggregations on cash using various functions: @@ -253,8 +329,9 @@ Aggregations on cash using various functions: :language: kotlin :start-after: DOCSTART VaultQueryExample21 :end-before: DOCEND VaultQueryExample21 + :dedent: 12 -.. note:: `otherResults` will contain 5 items, one per calculated aggregate function. +.. note:: ``otherResults`` will contain 5 items, one per calculated aggregate function. Aggregations on cash grouped by currency for various functions: @@ -262,8 +339,10 @@ Aggregations on cash grouped by currency for various functions: :language: kotlin :start-after: DOCSTART VaultQueryExample22 :end-before: DOCEND VaultQueryExample22 + :dedent: 12 -.. note:: `otherResults` will contain 24 items, one result per calculated aggregate function per currency (the grouping attribute - currency in this case - is returned per aggregate result). +.. note:: ``otherResults`` will contain 24 items, one result per calculated aggregate function per currency (the + grouping attribute - currency in this case - is returned per aggregate result). Sum aggregation on cash grouped by issuer party and currency and sorted by sum: @@ -271,10 +350,15 @@ Sum aggregation on cash grouped by issuer party and currency and sorted by sum: :language: kotlin :start-after: DOCSTART VaultQueryExample23 :end-before: DOCEND VaultQueryExample23 + :dedent: 12 -.. note:: `otherResults` will contain 12 items sorted from largest summed cash amount to smallest, one result per calculated aggregate function per issuer party and currency (grouping attributes are returned per aggregate result). +.. note:: ``otherResults`` will contain 12 items sorted from largest summed cash amount to smallest, one result per + calculated aggregate function per issuer party and currency (grouping attributes are returned per aggregate result). -**Dynamic queries** (also using ``VaultQueryCriteria``) are an extension to the snapshot queries by returning an additional ``QueryResults`` return type in the form of an ``Observable``. Refer to `ReactiveX Observable `_ for a detailed understanding and usage of this type. +Dynamic queries (also using ``VaultQueryCriteria``) are an extension to the snapshot queries by returning an +additional ``QueryResults`` return type in the form of an ``Observable``. Refer to +`ReactiveX Observable `_ for a detailed understanding and usage of +this type. Track unconsumed cash states: @@ -282,6 +366,7 @@ Track unconsumed cash states: :language: kotlin :start-after: DOCSTART VaultQueryExample15 :end-before: DOCEND VaultQueryExample15 + :dedent: 20 Track unconsumed linear states: @@ -289,8 +374,9 @@ Track unconsumed linear states: :language: kotlin :start-after: DOCSTART VaultQueryExample16 :end-before: DOCEND VaultQueryExample16 + :dedent: 20 -.. note:: This will return both Deal and Linear states. +.. note:: This will return both ``DealState`` and ``LinearState`` states. Track unconsumed deal states: @@ -298,8 +384,9 @@ Track unconsumed deal states: :language: kotlin :start-after: DOCSTART VaultQueryExample17 :end-before: DOCEND VaultQueryExample17 + :dedent: 20 -.. note:: This will return only Deal states. +.. note:: This will return only ``DealState`` states. Java examples ^^^^^^^^^^^^^ @@ -310,6 +397,7 @@ Query for all unconsumed linear states: :language: java :start-after: DOCSTART VaultJavaQueryExample0 :end-before: DOCEND VaultJavaQueryExample0 + :dedent: 12 Query for all consumed cash states: @@ -317,6 +405,7 @@ Query for all consumed cash states: :language: java :start-after: DOCSTART VaultJavaQueryExample1 :end-before: DOCEND VaultJavaQueryExample1 + :dedent: 12 Query for consumed deal states or linear ids, specify a paging specification and sort by unique identifier: @@ -324,8 +413,9 @@ Query for consumed deal states or linear ids, specify a paging specification and :language: java :start-after: DOCSTART VaultJavaQueryExample2 :end-before: DOCEND VaultJavaQueryExample2 + :dedent: 12 -**Aggregate Function queries using** ``VaultCustomQueryCriteria`` +**Aggregate Function queries using** ``VaultCustomQueryCriteria``: Aggregations on cash using various functions: @@ -333,6 +423,7 @@ Aggregations on cash using various functions: :language: java :start-after: DOCSTART VaultJavaQueryExample21 :end-before: DOCEND VaultJavaQueryExample21 + :dedent: 16 Aggregations on cash grouped by currency for various functions: @@ -340,6 +431,7 @@ Aggregations on cash grouped by currency for various functions: :language: java :start-after: DOCSTART VaultJavaQueryExample22 :end-before: DOCEND VaultJavaQueryExample22 + :dedent: 16 Sum aggregation on cash grouped by issuer party and currency and sorted by sum: @@ -347,6 +439,7 @@ Sum aggregation on cash grouped by issuer party and currency and sorted by sum: :language: java :start-after: DOCSTART VaultJavaQueryExample23 :end-before: DOCEND VaultJavaQueryExample23 + :dedent: 16 Track unconsumed cash states: @@ -354,13 +447,16 @@ Track unconsumed cash states: :language: java :start-after: DOCSTART VaultJavaQueryExample4 :end-before: DOCEND VaultJavaQueryExample4 + :dedent: 12 -Track unconsumed deal states or linear states (with snapshot including specification of paging and sorting by unique identifier): +Track unconsumed deal states or linear states (with snapshot including specification of paging and sorting by unique +identifier): .. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java :language: java :start-after: DOCSTART VaultJavaQueryExample4 :end-before: DOCEND VaultJavaQueryExample4 + :dedent: 12 Troubleshooting --------------- @@ -373,24 +469,27 @@ If the results your were expecting do not match actual returned query results we Behavioural notes ----------------- -1. **TrackBy** updates do not take into account the full criteria specification due to different and more restrictive syntax - in `observables `_ filtering (vs full SQL-92 JDBC filtering as used in snapshot views). - Specifically, dynamic updates are filtered by ``contractStateType`` and ``stateType`` (UNCONSUMED, CONSUMED, ALL) only. -2. **QueryBy** and **TrackBy snapshot views** using pagination may return different result sets as each paging request is a - separate SQL query on the underlying database, and it is entirely conceivable that state modifications are taking - place in between and/or in parallel to paging requests. - When using pagination, always check the value of the ``totalStatesAvailable`` (from the ``Vault.Page`` result) and - adjust further paging requests appropriately. +1. ``TrackBy`` updates do not take into account the full criteria specification due to different and more restrictive + syntax in `observables `_ filtering (vs full SQL-92 JDBC filtering as used + in snapshot views). Specifically, dynamic updates are filtered by ``contractStateType`` and ``stateType`` + (UNCONSUMED, CONSUMED, ALL) only +2. ``QueryBy`` and ``TrackBy`` snapshot views using pagination may return different result sets as each paging request + is a separate SQL query on the underlying database, and it is entirely conceivable that state modifications are + taking place in between and/or in parallel to paging requests. When using pagination, always check the value of the + ``totalStatesAvailable`` (from the ``Vault.Page`` result) and adjust further paging requests appropriately. Other use case scenarios ------------------------ -For advanced use cases that require sophisticated pagination, sorting, grouping, and aggregation functions, it is recommended that the CorDapp developer utilise one of the many proven frameworks that ship with this capability out of the box. Namely, implementations of JPQL (JPA Query Language) such as **Hibernate** for advanced SQL access, and **Spring Data** for advanced pagination and ordering constructs. +For advanced use cases that require sophisticated pagination, sorting, grouping, and aggregation functions, it is +recommended that the CorDapp developer utilise one of the many proven frameworks that ship with this capability out of +the box. Namely, implementations of JPQL (JPA Query Language) such as Hibernate for advanced SQL access, and +Spring Data for advanced pagination and ordering constructs. The Corda Tutorials provide examples satisfying these additional Use Cases: - 1. Template / Tutorial CorDapp service using Vault API Custom Query to access attributes of IOU State - 2. Template / Tutorial CorDapp service query extension executing Named Queries via JPQL_ + 1. Example CorDapp service using Vault API Custom Query to access attributes of IOU State + 2. Example CorDapp service query extension executing Named Queries via JPQL_ 3. `Advanced pagination `_ queries using Spring Data JPA_ .. _JPQL: http://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#hql From 899f7f9d0d45ebbfaacb779cec372324dd512f1e Mon Sep 17 00:00:00 2001 From: Matthew Nesbit Date: Mon, 9 Oct 2017 15:04:00 +0100 Subject: [PATCH 161/185] Change the serialization/deserialization code of SessionMessage data to add more validation. Address PR comments As pointed out by Shams the SessionInit must be well formed at this point. --- .../serialization/ListsSerializationTest.kt | 14 ++++++++----- .../serialization/MapsSerializationTest.kt | 7 +++++-- .../serialization/SetsSerializationTest.kt | 13 ++++++++---- .../statemachine/FlowStateMachineImpl.kt | 7 +++++-- .../services/statemachine/SessionMessage.kt | 20 ++++++++++++++----- .../statemachine/StateMachineManager.kt | 20 ++++++++----------- .../statemachine/FlowFrameworkTests.kt | 4 ++-- 7 files changed, 53 insertions(+), 32 deletions(-) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt index ede793b2be..d9d0f58e0c 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt @@ -11,11 +11,12 @@ import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.amqpSpecific import net.corda.testing.kryoSpecific import org.assertj.core.api.Assertions -import org.junit.Assert.* +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals import org.junit.Test import java.io.ByteArrayOutputStream import java.io.NotSerializableException -import java.nio.charset.StandardCharsets.* +import java.nio.charset.StandardCharsets.US_ASCII import java.util.* class ListsSerializationTest : TestDependencyInjectionBase() { @@ -40,16 +41,19 @@ class ListsSerializationTest : TestDependencyInjectionBase() { @Test fun `check list can be serialized as part of SessionData`() { run { - val sessionData = SessionData(123, listOf(1)) + val sessionData = SessionData(123, listOf(1).serialize()) assertEqualAfterRoundTripSerialization(sessionData) + assertEquals(listOf(1), sessionData.payload.deserialize()) } run { - val sessionData = SessionData(123, listOf(1, 2)) + val sessionData = SessionData(123, listOf(1, 2).serialize()) assertEqualAfterRoundTripSerialization(sessionData) + assertEquals(listOf(1, 2), sessionData.payload.deserialize()) } run { - val sessionData = SessionData(123, emptyList()) + val sessionData = SessionData(123, emptyList().serialize()) assertEqualAfterRoundTripSerialization(sessionData) + assertEquals(emptyList(), sessionData.payload.deserialize()) } } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/MapsSerializationTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/MapsSerializationTest.kt index 9788885420..4e9f598eab 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/MapsSerializationTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/MapsSerializationTest.kt @@ -3,17 +3,19 @@ package net.corda.nodeapi.internal.serialization import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.util.DefaultClassResolver import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.node.services.statemachine.SessionData import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.amqpSpecific import net.corda.testing.kryoSpecific import org.assertj.core.api.Assertions +import org.bouncycastle.asn1.x500.X500Name import org.junit.Assert.assertArrayEquals import org.junit.Test -import org.bouncycastle.asn1.x500.X500Name import java.io.ByteArrayOutputStream import java.util.* +import kotlin.test.assertEquals class MapsSerializationTest : TestDependencyInjectionBase() { private companion object { @@ -33,8 +35,9 @@ class MapsSerializationTest : TestDependencyInjectionBase() { @Test fun `check list can be serialized as part of SessionData`() { - val sessionData = SessionData(123, smallMap) + val sessionData = SessionData(123, smallMap.serialize()) assertEqualAfterRoundTripSerialization(sessionData) + assertEquals(smallMap, sessionData.payload.deserialize()) } @CordaSerializable diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SetsSerializationTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SetsSerializationTest.kt index 4a652a7521..210a0cd800 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SetsSerializationTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SetsSerializationTest.kt @@ -2,11 +2,13 @@ package net.corda.nodeapi.internal.serialization import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.util.DefaultClassResolver +import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.node.services.statemachine.SessionData import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.kryoSpecific -import org.junit.Assert.* +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals import org.junit.Test import java.io.ByteArrayOutputStream import java.util.* @@ -26,16 +28,19 @@ class SetsSerializationTest : TestDependencyInjectionBase() { @Test fun `check set can be serialized as part of SessionData`() { run { - val sessionData = SessionData(123, setOf(1)) + val sessionData = SessionData(123, setOf(1).serialize()) assertEqualAfterRoundTripSerialization(sessionData) + assertEquals(setOf(1), sessionData.payload.deserialize()) } run { - val sessionData = SessionData(123, setOf(1, 2)) + val sessionData = SessionData(123, setOf(1, 2).serialize()) assertEqualAfterRoundTripSerialization(sessionData) + assertEquals(setOf(1, 2), sessionData.payload.deserialize()) } run { - val sessionData = SessionData(123, emptySet()) + val sessionData = SessionData(123, emptySet().serialize()) assertEqualAfterRoundTripSerialization(sessionData) + assertEquals(emptySet(), sessionData.payload.deserialize()) } } 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 019940363b..89f0bf6dc6 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 @@ -15,6 +15,8 @@ import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.* import net.corda.core.internal.concurrent.OpenFuture import net.corda.core.internal.concurrent.openFuture +import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.* import net.corda.node.services.api.FlowAppAuditEvent @@ -327,7 +329,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, is FlowSessionState.Initiated -> sessionState.peerSessionId else -> throw IllegalStateException("We've somehow held onto a non-initiated session: $session") } - return SessionData(peerSessionId, payload) + return SessionData(peerSessionId, payload.serialize(context = SerializationDefaults.P2P_CONTEXT)) } @Suspendable @@ -389,7 +391,8 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, session.state = FlowSessionState.Initiating(state.otherParty) session.retryable = retryable val (version, initiatingFlowClass) = session.flow.javaClass.flowVersionAndInitiatingClass - val sessionInit = SessionInit(session.ourSessionId, initiatingFlowClass.name, version, session.flow.javaClass.appName, firstPayload) + val payloadBytes = firstPayload?.serialize(context = SerializationDefaults.P2P_CONTEXT) + val sessionInit = SessionInit(session.ourSessionId, initiatingFlowClass.name, version, session.flow.javaClass.appName, payloadBytes) sendInternal(session, sessionInit) if (waitForConfirmation) { session.waitForConfirmation() diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/SessionMessage.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/SessionMessage.kt index fc103e6dca..c321d3768a 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/SessionMessage.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/SessionMessage.kt @@ -5,7 +5,10 @@ import net.corda.core.flows.UnexpectedFlowEndException import net.corda.core.identity.Party import net.corda.core.internal.castIfPossible import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.SerializedBytes import net.corda.core.utilities.UntrustworthyData +import java.io.IOException @CordaSerializable interface SessionMessage @@ -25,7 +28,7 @@ data class SessionInit(val initiatorSessionId: Long, val initiatingFlowClass: String, val flowVersion: Int, val appName: String, - val firstPayload: Any?) : SessionMessage + val firstPayload: SerializedBytes?) : SessionMessage data class SessionConfirm(override val initiatorSessionId: Long, val initiatedSessionId: Long, @@ -34,7 +37,7 @@ data class SessionConfirm(override val initiatorSessionId: Long, data class SessionReject(override val initiatorSessionId: Long, val errorMessage: String) : SessionInitResponse -data class SessionData(override val recipientSessionId: Long, val payload: Any) : ExistingSessionMessage +data class SessionData(override val recipientSessionId: Long, val payload: SerializedBytes) : ExistingSessionMessage data class NormalSessionEnd(override val recipientSessionId: Long) : SessionEnd @@ -42,8 +45,15 @@ data class ErrorSessionEnd(override val recipientSessionId: Long, val errorRespo data class ReceivedSessionMessage(val sender: Party, val message: M) -fun ReceivedSessionMessage.checkPayloadIs(type: Class): UntrustworthyData { - return type.castIfPossible(message.payload)?.let { UntrustworthyData(it) } ?: +fun ReceivedSessionMessage.checkPayloadIs(type: Class): UntrustworthyData { + val payloadData: T = try { + val serializer = SerializationDefaults.SERIALIZATION_FACTORY + serializer.deserialize(message.payload, type, SerializationDefaults.P2P_CONTEXT) + } catch (ex: Exception) { + throw IOException("Payload invalid", ex) + } + return type.castIfPossible(payloadData)?.let { UntrustworthyData(it) } ?: throw UnexpectedFlowEndException("We were expecting a ${type.name} from $sender but we instead got a " + - "${message.payload.javaClass.name} (${message.payload})") + "${payloadData.javaClass.name} (${payloadData})") + } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index 2215354b07..74697821e9 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -15,11 +15,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.random63BitValue import net.corda.core.flows.* import net.corda.core.identity.Party -import net.corda.core.internal.FlowStateMachine -import net.corda.core.internal.ThreadBox -import net.corda.core.internal.bufferUntilSubscribed -import net.corda.core.internal.castIfPossible -import net.corda.core.internal.uncheckedCast +import net.corda.core.internal.* import net.corda.core.messaging.DataFeed import net.corda.core.serialization.SerializationDefaults.CHECKPOINT_CONTEXT import net.corda.core.serialization.SerializationDefaults.SERIALIZATION_FACTORY @@ -290,7 +286,12 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } private fun onSessionMessage(message: ReceivedMessage) { - val sessionMessage = message.data.deserialize() + val sessionMessage = try { + message.data.deserialize() + } catch (ex: Exception) { + logger.error("Received corrupt SessionMessage data from ${message.peer}") + return + } val sender = serviceHub.networkMapCache.getPeerByLegalName(message.peer) if (sender != null) { when (sessionMessage) { @@ -382,12 +383,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, updateCheckpoint(fiber) session to initiatedFlowFactory } catch (e: SessionRejectException) { - // TODO: Handle this more gracefully - try { - logger.warn("${e.logMessage}: $sessionInit") - } catch (e: Throwable) { - logger.warn("Problematic session init message during logging", e) - } + logger.warn("${e.logMessage}: $sessionInit") sendSessionReject(e.rejectMessage) return } catch (e: Exception) { diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index d4391109e3..418ffb5142 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -710,11 +710,11 @@ class FlowFrameworkTests { } private fun sessionInit(clientFlowClass: KClass>, flowVersion: Int = 1, payload: Any? = null): SessionInit { - return SessionInit(0, clientFlowClass.java.name, flowVersion, "", payload) + return SessionInit(0, clientFlowClass.java.name, flowVersion, "", payload?.serialize()) } private fun sessionConfirm(flowVersion: Int = 1) = SessionConfirm(0, 0, flowVersion, "") - private fun sessionData(payload: Any) = SessionData(0, payload) + private fun sessionData(payload: Any) = SessionData(0, payload.serialize()) private val normalEnd = NormalSessionEnd(0) private fun erroredEnd(errorResponse: FlowException? = null) = ErrorSessionEnd(0, errorResponse) From de391dc9f006ce658e714dd1ccd592ab37bb3d21 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Wed, 11 Oct 2017 16:15:24 +0100 Subject: [PATCH 162/185] Adds a length method to calculate a time-window's length. --- .../net/corda/core/contracts/TimeWindow.kt | 11 ++++++++++ .../corda/core/contracts/TimeWindowTest.kt | 22 ++++++++++++++----- docs/source/changelog.rst | 3 +++ 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/contracts/TimeWindow.kt b/core/src/main/kotlin/net/corda/core/contracts/TimeWindow.kt index c8c650257d..8126292138 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TimeWindow.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TimeWindow.kt @@ -64,6 +64,17 @@ abstract class TimeWindow { */ abstract val midpoint: Instant? + /** + * Returns the duration between [fromTime] and [untilTime] if both are non-null. Otherwise returns null. + */ + val length: Duration? get() { + return if (fromTime == null || untilTime == null) { + null + } else { + Duration.between(fromTime, untilTime) + } + } + /** Returns true iff the given [instant] is within the time interval of this [TimeWindow]. */ abstract operator fun contains(instant: Instant): Boolean diff --git a/core/src/test/kotlin/net/corda/core/contracts/TimeWindowTest.kt b/core/src/test/kotlin/net/corda/core/contracts/TimeWindowTest.kt index 453b01eb65..f0da2499b9 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TimeWindowTest.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TimeWindowTest.kt @@ -1,9 +1,12 @@ package net.corda.core.contracts +import net.corda.core.internal.div +import net.corda.core.internal.times import net.corda.core.utilities.millis import net.corda.core.utilities.minutes import org.assertj.core.api.Assertions.assertThat import org.junit.Test +import java.time.Duration import java.time.Instant import java.time.LocalDate import java.time.ZoneOffset.UTC @@ -17,6 +20,7 @@ class TimeWindowTest { assertThat(timeWindow.fromTime).isEqualTo(now) assertThat(timeWindow.untilTime).isNull() assertThat(timeWindow.midpoint).isNull() + assertThat(timeWindow.length).isNull() assertThat(timeWindow.contains(now - 1.millis)).isFalse() assertThat(timeWindow.contains(now)).isTrue() assertThat(timeWindow.contains(now + 1.millis)).isTrue() @@ -28,6 +32,7 @@ class TimeWindowTest { assertThat(timeWindow.fromTime).isNull() assertThat(timeWindow.untilTime).isEqualTo(now) assertThat(timeWindow.midpoint).isNull() + assertThat(timeWindow.length).isNull() assertThat(timeWindow.contains(now - 1.millis)).isTrue() assertThat(timeWindow.contains(now)).isFalse() assertThat(timeWindow.contains(now + 1.millis)).isFalse() @@ -42,6 +47,7 @@ class TimeWindowTest { assertThat(timeWindow.fromTime).isEqualTo(fromTime) assertThat(timeWindow.untilTime).isEqualTo(untilTime) assertThat(timeWindow.midpoint).isEqualTo(today.atTime(12, 15).toInstant(UTC)) + assertThat(timeWindow.length).isEqualTo(Duration.between(fromTime, untilTime)) assertThat(timeWindow.contains(fromTime - 1.millis)).isFalse() assertThat(timeWindow.contains(fromTime)).isTrue() assertThat(timeWindow.contains(fromTime + 1.millis)).isTrue() @@ -51,17 +57,21 @@ class TimeWindowTest { @Test fun fromStartAndDuration() { - val timeWindow = TimeWindow.fromStartAndDuration(now, 10.minutes) + val duration = 10.minutes + val timeWindow = TimeWindow.fromStartAndDuration(now, duration) assertThat(timeWindow.fromTime).isEqualTo(now) - assertThat(timeWindow.untilTime).isEqualTo(now + 10.minutes) - assertThat(timeWindow.midpoint).isEqualTo(now + 5.minutes) + assertThat(timeWindow.untilTime).isEqualTo(now + duration) + assertThat(timeWindow.midpoint).isEqualTo(now + duration / 2) + assertThat(timeWindow.length).isEqualTo(duration) } @Test fun withTolerance() { - val timeWindow = TimeWindow.withTolerance(now, 10.minutes) - assertThat(timeWindow.fromTime).isEqualTo(now - 10.minutes) - assertThat(timeWindow.untilTime).isEqualTo(now + 10.minutes) + val tolerance = 10.minutes + val timeWindow = TimeWindow.withTolerance(now, tolerance) + assertThat(timeWindow.fromTime).isEqualTo(now - tolerance) + assertThat(timeWindow.untilTime).isEqualTo(now + tolerance) assertThat(timeWindow.midpoint).isEqualTo(now) + assertThat(timeWindow.length).isEqualTo(tolerance * 2) } } diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 0931e57561..ddc74beb2a 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -46,6 +46,9 @@ UNRELEASED allows services to start flows marked with the ``StartableByService`` annotation. For backwards compatability service classes with only ``ServiceHub`` constructors will still work. +* ``TimeWindow`` now has a ``length`` property that returns the length of the time-window, or ``null`` if the + time-window is open-ended. + .. _changelog_v1: Release 1.0 From ed79db68641d7a90b4ee8d231fa0124b11f7661d Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Wed, 11 Oct 2017 17:47:48 +0100 Subject: [PATCH 163/185] Clean up Crypto (#1866) clean up Crypto comments + PublicKey.toSHA256Bytes() uses .encoded --- .../kotlin/net/corda/core/crypto/Crypto.kt | 20 +++++------ .../net/corda/core/crypto/CryptoUtils.kt | 34 ++++++++++++++----- .../net/corda/core/utilities/EncodingUtils.kt | 21 ++++++++---- 3 files changed, 51 insertions(+), 24 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index 58b68bb176..f2c0a45918 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -2,7 +2,10 @@ package net.corda.core.crypto import net.corda.core.internal.X509EdDSAEngine import net.corda.core.serialization.serialize -import net.i2p.crypto.eddsa.* +import net.i2p.crypto.eddsa.EdDSAEngine +import net.i2p.crypto.eddsa.EdDSAPrivateKey +import net.i2p.crypto.eddsa.EdDSAPublicKey +import net.i2p.crypto.eddsa.EdDSASecurityProvider import net.i2p.crypto.eddsa.math.GroupElement import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable @@ -39,8 +42,6 @@ import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey import org.bouncycastle.pqc.jcajce.spec.SPHINCS256KeyGenParameterSpec import java.math.BigInteger import java.security.* -import java.security.KeyFactory -import java.security.KeyPairGenerator import java.security.spec.InvalidKeySpecException import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.X509EncodedKeySpec @@ -148,7 +149,7 @@ object Crypto { "at the cost of larger key sizes and loss of compatibility." ) - /** Corda composite key type */ + /** Corda composite key type. */ @JvmField val COMPOSITE_KEY = SignatureScheme( 6, @@ -823,7 +824,7 @@ object Crypto { @JvmStatic fun deriveKeyPairFromEntropy(entropy: BigInteger): KeyPair = deriveKeyPairFromEntropy(DEFAULT_SIGNATURE_SCHEME, entropy) - // custom key pair generator from entropy. + // Custom key pair generator from entropy. private fun deriveEdDSAKeyPairFromEntropy(entropy: BigInteger): KeyPair { val params = EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec val bytes = entropy.toByteArray().copyOf(params.curve.field.getb() / 8) // Need to pad the entropy to the valid seed length. @@ -882,7 +883,7 @@ object Crypto { } } - // return true if EdDSA publicKey is point at infinity. + // Return true if EdDSA publicKey is point at infinity. // For EdDSA a custom function is required as it is not supported by the I2P implementation. private fun isEdDSAPointAtInfinity(publicKey: EdDSAPublicKey): Boolean { return publicKey.a.toP3() == (EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3) @@ -894,7 +895,7 @@ object Crypto { return signatureScheme.schemeCodeName in signatureSchemeMap } - // validate a key, by checking its algorithmic params. + // Validate a key, by checking its algorithmic params. private fun validateKey(signatureScheme: SignatureScheme, key: Key): Boolean { return when (key) { is PublicKey -> validatePublicKey(signatureScheme, key) @@ -903,7 +904,7 @@ object Crypto { } } - // check if a public key satisfies algorithm specs (for ECC: key should lie on the curve and not being point-at-infinity). + // Check if a public key satisfies algorithm specs (for ECC: key should lie on the curve and not being point-at-infinity). private fun validatePublicKey(signatureScheme: SignatureScheme, key: PublicKey): Boolean { return when (key) { is BCECPublicKey, is EdDSAPublicKey -> publicKeyOnCurve(signatureScheme, key) @@ -912,7 +913,7 @@ object Crypto { } } - // check if a private key satisfies algorithm specs. + // Check if a private key satisfies algorithm specs. private fun validatePrivateKey(signatureScheme: SignatureScheme, key: PrivateKey): Boolean { return when (key) { is BCECPrivateKey -> key.parameters == signatureScheme.algSpec @@ -924,7 +925,6 @@ object Crypto { /** * Convert a public key to a supported implementation. - * * @param key a public key. * @return a supported implementation of the input public key. * @throws IllegalArgumentException on not supported scheme or if the given key specification 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 32b14cffcd..17bd078862 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -20,9 +20,19 @@ import java.security.* * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ -@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) +@Throws(InvalidKeyException::class, SignatureException::class) fun PrivateKey.sign(bytesToSign: ByteArray): DigitalSignature = DigitalSignature(Crypto.doSign(this, bytesToSign)) +/** + * Utility to simplify the act of signing a byte array and return a [DigitalSignature.WithKey] object. + * Note that there is no check if the public key matches with the signing private key. + * @param bytesToSign the data/message to be signed in [ByteArray] form (usually the Merkle root). + * @return the [DigitalSignature.WithKey] object on the input message [bytesToSign] and [publicKey]. + * @throws IllegalArgumentException if the signature scheme is not supported for this private key. + * @throws InvalidKeyException if the private key is invalid. + * @throws SignatureException if signing is not possible due to malformed data or private key. + */ +@Throws(InvalidKeyException::class, SignatureException::class) fun PrivateKey.sign(bytesToSign: ByteArray, publicKey: PublicKey) = DigitalSignature.WithKey(publicKey, this.sign(bytesToSign).bytes) /** @@ -33,10 +43,13 @@ fun PrivateKey.sign(bytesToSign: ByteArray, publicKey: PublicKey) = DigitalSigna * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ -@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) +@Throws(InvalidKeyException::class, SignatureException::class) fun KeyPair.sign(bytesToSign: ByteArray) = private.sign(bytesToSign, public) +/** Helper function to sign the bytes of [bytesToSign] with a key pair. */ +@Throws(InvalidKeyException::class, SignatureException::class) fun KeyPair.sign(bytesToSign: OpaqueBytes) = sign(bytesToSign.bytes) + /** * Helper function for signing a [SignableData] object. * @param signableData the object to be signed. @@ -56,8 +69,8 @@ fun KeyPair.sign(signableData: SignableData): TransactionSignature = Crypto.doSi * @throws SignatureException if the signature is invalid (i.e. damaged), or does not match the key (incorrect). * @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty. */ -// TODO: SignatureException should be used only for a damaged signature, as per `java.security.Signature.verify()`, -@Throws(SignatureException::class, IllegalArgumentException::class, InvalidKeyException::class) +// TODO: SignatureException should be used only for a damaged signature, as per `java.security.Signature.verify()`. +@Throws(SignatureException::class, InvalidKeyException::class) fun PublicKey.verify(content: ByteArray, signature: DigitalSignature) = Crypto.doVerify(this, signature.bytes, content) /** @@ -70,9 +83,10 @@ fun PublicKey.verify(content: ByteArray, signature: DigitalSignature) = Crypto.d * signature). * @throws SignatureException if the signature is invalid (i.e. damaged). * @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty. + * @throws IllegalStateException if this is a [CompositeKey], because verification of composite key signatures is not supported. * @return whether the signature is correct for this key. */ -@Throws(IllegalStateException::class, SignatureException::class, IllegalArgumentException::class) +@Throws(SignatureException::class, InvalidKeyException::class) fun PublicKey.isValid(content: ByteArray, signature: DigitalSignature): Boolean { if (this is CompositeKey) throw IllegalStateException("Verification of CompositeKey signatures currently not supported.") // TODO CompositeSignature verification. @@ -82,9 +96,12 @@ 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]). */ val PublicKey.keys: Set get() = (this as? CompositeKey)?.leafKeys ?: setOf(this) +/** Return true if [otherKey] fulfils the requirements of this [PublicKey]. */ fun PublicKey.isFulfilledBy(otherKey: PublicKey): Boolean = isFulfilledBy(setOf(otherKey)) +/** Return true if [otherKeys] fulfil the requirements of this [PublicKey]. */ fun PublicKey.isFulfilledBy(otherKeys: Iterable): 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]. */ @@ -98,8 +115,9 @@ fun Iterable.byKeys() = map { it.by }.toSet() // Allow Kotlin destructuring: // val (private, public) = keyPair +/* The [PrivateKey] of this [KeyPair] .*/ operator fun KeyPair.component1(): PrivateKey = this.private - +/* The [PublicKey] of this [KeyPair] .*/ operator fun KeyPair.component2(): PublicKey = this.public /** A simple wrapper that will make it easier to swap out the EC algorithm we use in future. */ @@ -122,7 +140,7 @@ fun entropyToKeyPair(entropy: BigInteger): KeyPair = Crypto.deriveKeyPairFromEnt * if this signatureData algorithm is unable to process the input data provided, etc. * @throws IllegalArgumentException if the signature scheme is not supported for this private key or if any of the clear or signature data is empty. */ -@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) +@Throws(InvalidKeyException::class, SignatureException::class) fun PublicKey.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = Crypto.doVerify(this, signatureData, clearData) /** @@ -135,7 +153,7 @@ fun PublicKey.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = * if this signatureData algorithm is unable to process the input data provided, etc. * @throws IllegalArgumentException if the signature scheme is not supported for this private key or if any of the clear or signature data is empty. */ -@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) +@Throws(InvalidKeyException::class, SignatureException::class) fun KeyPair.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = Crypto.doVerify(this.public, signatureData, clearData) /** diff --git a/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt index 2c1854c897..2fd31c9590 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt @@ -3,9 +3,8 @@ package net.corda.core.utilities import net.corda.core.crypto.Base58 +import net.corda.core.crypto.Crypto import net.corda.core.crypto.sha256 -import net.corda.core.serialization.deserialize -import net.corda.core.serialization.serialize import java.nio.charset.Charset import java.security.PublicKey import java.util.* @@ -15,11 +14,13 @@ import javax.xml.bind.DatatypeConverter // [ByteArray] encoders +/** Convert a byte array to a Base58 encoded [String]. */ fun ByteArray.toBase58(): String = Base58.encode(this) +/** Convert a byte array to a Base64 encoded [String]. */ fun ByteArray.toBase64(): String = Base64.getEncoder().encodeToString(this) -/** Convert a byte array to a hex (base 16) capitalized encoded string.*/ +/** Convert a byte array to a hex (Base16) capitalized encoded [String]. */ fun ByteArray.toHex(): String = DatatypeConverter.printHexBinary(this) @@ -65,7 +66,15 @@ fun String.hexToBase64(): String = hexToByteArray().toBase64() // TODO We use for both CompositeKeys and EdDSAPublicKey custom serializers and deserializers. We need to specify encoding. // TODO: follow the crypto-conditions ASN.1 spec, some changes are needed to be compatible with the condition // structure, e.g. mapping a PublicKey to a condition with the specific feature (ED25519). -fun parsePublicKeyBase58(base58String: String): PublicKey = base58String.base58ToByteArray().deserialize() +/** + * Method to return the [PublicKey] object given its Base58-[String] representation. + * @param base58String the Base58 encoded format of the serialised [PublicKey]. + * @return the resulted [PublicKey] after decoding the [base58String] input and then deserialising to a [PublicKey] object. + */ +fun parsePublicKeyBase58(base58String: String): PublicKey = Crypto.decodePublicKey(base58String.base58ToByteArray()) -fun PublicKey.toBase58String(): String = this.serialize().bytes.toBase58() -fun PublicKey.toSHA256Bytes(): ByteArray = this.serialize().bytes.sha256().bytes // TODO: decide on the format of hashed key (encoded Vs serialised). +/** Return the Base58 representation of the serialised public key. */ +fun PublicKey.toBase58String(): String = this.encoded.toBase58() + +/** Return the bytes of the SHA-256 output for this public key. */ +fun PublicKey.toSHA256Bytes(): ByteArray = this.encoded.sha256().bytes From 327f0ebd7363a66b258f0c82b3b90ee2019dc045 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Wed, 11 Oct 2017 18:26:09 +0100 Subject: [PATCH 164/185] CORDA-654: Migrate test APIs to match identity changes (#1744) Rework identity usage in tests to extract identity from nodes by name, rather than just arbitrarily choosing the first identity. This better models the intended design for production (future work). --- .../confidential/SwapIdentitiesFlowTests.kt | 4 +- .../kotlin/net/corda/core/node/NodeInfo.kt | 6 ++ .../net/corda/core/flows/AttachmentTests.kt | 15 ++--- .../core/flows/CollectSignaturesFlowTests.kt | 23 +++++--- .../net/corda/core/flows/FinalityFlowTests.kt | 42 ++++++++------ .../WorkflowTransactionBuildTutorialTest.kt | 57 +++++++++++-------- .../corda/node/services/NotaryChangeTests.kt | 16 +++--- .../ValidatingNotaryServiceTests.kt | 39 +++++++------ .../corda/bank/BankOfCordaRPCClientTest.kt | 4 +- .../kotlin/net/corda/testing/node/MockNode.kt | 9 +-- .../kotlin/net/corda/testing/TestConstants.kt | 10 +++- .../net/corda/verifier/VerifierTests.kt | 18 +++--- 12 files changed, 142 insertions(+), 101 deletions(-) diff --git a/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt b/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt index 55e9ef9d26..3a0e6602fe 100644 --- a/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt +++ b/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt @@ -19,8 +19,8 @@ class SwapIdentitiesFlowTests { val notaryNode = mockNet.createNotaryNode() val aliceNode = mockNet.createPartyNode(ALICE.name) val bobNode = mockNet.createPartyNode(BOB.name) - val alice: Party = aliceNode.services.myInfo.chooseIdentity() - val bob: Party = bobNode.services.myInfo.chooseIdentity() + val alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME) + val bob = bobNode.services.myInfo.identityFromX500Name(BOB_NAME) // Run the flows val requesterFlow = aliceNode.services.startFlow(SwapIdentitiesFlow(bob)) diff --git a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt index cf3e5aed2e..07e6c550be 100644 --- a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt +++ b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt @@ -1,5 +1,6 @@ package net.corda.core.node +import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.serialization.CordaSerializable @@ -42,4 +43,9 @@ data class NodeInfo(val addresses: List, /** Returns true if [party] is one of the identities of this node, else false. */ fun isLegalIdentity(party: Party): Boolean = party in legalIdentities + + fun identityFromX500Name(name: CordaX500Name): Party { + val identity = legalIdentitiesAndCerts.singleOrNull { it.name == name } ?: throw IllegalArgumentException("Node does not have an identity \"$name\"") + return identity.party + } } diff --git a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt index 6a2bf1ef8b..ab4936d6fc 100644 --- a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt @@ -15,8 +15,8 @@ import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.utilities.DatabaseTransactionManager import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.ALICE +import net.corda.testing.ALICE_NAME import net.corda.testing.BOB -import net.corda.testing.chooseIdentity import net.corda.testing.node.MockNetwork import org.junit.After import org.junit.Before @@ -55,13 +55,13 @@ class AttachmentTests { @Test fun `download and store`() { - mockNet.createNotaryNode() val aliceNode = mockNet.createPartyNode(ALICE.name) val bobNode = mockNet.createPartyNode(BOB.name) // Ensure that registration was successful before progressing any further mockNet.runNetwork() aliceNode.internals.ensureRegistered() + val alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME) aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) bobNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) @@ -73,7 +73,7 @@ class AttachmentTests { // Get node one to run a flow to fetch it and insert it. mockNet.runNetwork() - val bobFlow = bobNode.startAttachmentFlow(setOf(id), aliceNode.info.chooseIdentity()) + val bobFlow = bobNode.startAttachmentFlow(setOf(id), alice) mockNet.runNetwork() assertEquals(0, bobFlow.resultFuture.getOrThrow().fromDisk.size) @@ -87,13 +87,12 @@ class AttachmentTests { // Shut down node zero and ensure node one can still resolve the attachment. aliceNode.dispose() - val response: FetchDataFlow.Result = bobNode.startAttachmentFlow(setOf(id), aliceNode.info.chooseIdentity()).resultFuture.getOrThrow() + val response: FetchDataFlow.Result = bobNode.startAttachmentFlow(setOf(id), alice).resultFuture.getOrThrow() assertEquals(attachment, response.fromDisk[0]) } @Test fun `missing`() { - mockNet.createNotaryNode() val aliceNode = mockNet.createPartyNode(ALICE.name) val bobNode = mockNet.createPartyNode(BOB.name) @@ -107,7 +106,8 @@ class AttachmentTests { // Get node one to fetch a non-existent attachment. val hash = SecureHash.randomSHA256() mockNet.runNetwork() - val bobFlow = bobNode.startAttachmentFlow(setOf(hash), aliceNode.info.chooseIdentity()) + val alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME) + val bobFlow = bobNode.startAttachmentFlow(setOf(hash), alice) mockNet.runNetwork() val e = assertFailsWith { bobFlow.resultFuture.getOrThrow() } assertEquals(hash, e.requested) @@ -130,6 +130,7 @@ class AttachmentTests { // Ensure that registration was successful before progressing any further mockNet.runNetwork() aliceNode.internals.ensureRegistered() + val alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME) aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) bobNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java) @@ -151,7 +152,7 @@ class AttachmentTests { // Get n1 to fetch the attachment. Should receive corrupted bytes. mockNet.runNetwork() - val bobFlow = bobNode.startAttachmentFlow(setOf(id), aliceNode.info.chooseIdentity()) + val bobFlow = bobNode.startAttachmentFlow(setOf(id), alice) mockNet.runNetwork() assertFailsWith { bobFlow.resultFuture.getOrThrow() } } diff --git a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt index 984646750c..d2c2a846ac 100644 --- a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -31,6 +31,9 @@ class CollectSignaturesFlowTests { lateinit var aliceNode: StartedNode lateinit var bobNode: StartedNode lateinit var charlieNode: StartedNode + lateinit var alice: Party + lateinit var bob: Party + lateinit var charlie: Party lateinit var notary: Party @Before @@ -41,8 +44,11 @@ class CollectSignaturesFlowTests { bobNode = mockNet.createPartyNode(BOB.name) charlieNode = mockNet.createPartyNode(CHARLIE.name) mockNet.runNetwork() - notary = notaryNode.services.getDefaultNotary() aliceNode.internals.ensureRegistered() + alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME) + bob = bobNode.services.myInfo.identityFromX500Name(BOB_NAME) + charlie = charlieNode.services.myInfo.identityFromX500Name(CHARLIE_NAME) + notary = notaryNode.services.networkMapCache.getNotary(DUMMY_NOTARY_SERVICE_NAME)!! } @After @@ -143,7 +149,8 @@ class CollectSignaturesFlowTests { @Test fun `successfully collects two signatures`() { val bConfidentialIdentity = bobNode.database.transaction { - bobNode.services.keyManagementService.freshKeyAndCert(bobNode.info.chooseIdentityAndCert(), false) + val bobCert = bobNode.services.myInfo.legalIdentitiesAndCerts.single { it.name == bob.name } + bobNode.services.keyManagementService.freshKeyAndCert(bobCert, false) } aliceNode.database.transaction { // Normally this is handled by TransactionKeyFlow, but here we have to manually let A know about the identity @@ -151,7 +158,7 @@ class CollectSignaturesFlowTests { } registerFlowOnAllNodes(TestFlowTwo.Responder::class) val magicNumber = 1337 - val parties = listOf(aliceNode.info.chooseIdentity(), bConfidentialIdentity.party, charlieNode.info.chooseIdentity()) + val parties = listOf(alice, bConfidentialIdentity.party, charlie) val state = DummyContract.MultiOwnerState(magicNumber, parties) val flow = aliceNode.services.startFlow(TestFlowTwo.Initiator(state)) mockNet.runNetwork() @@ -163,7 +170,7 @@ class CollectSignaturesFlowTests { @Test fun `no need to collect any signatures`() { - val onePartyDummyContract = DummyContract.generateInitial(1337, notary, aliceNode.info.chooseIdentity().ref(1)) + val onePartyDummyContract = DummyContract.generateInitial(1337, notary, alice.ref(1)) val ptx = aliceNode.services.signInitialTransaction(onePartyDummyContract) val flow = aliceNode.services.startFlow(CollectSignaturesFlow(ptx, emptySet())) mockNet.runNetwork() @@ -175,7 +182,7 @@ class CollectSignaturesFlowTests { @Test fun `fails when not signed by initiator`() { - val onePartyDummyContract = DummyContract.generateInitial(1337, notary, aliceNode.info.chooseIdentity().ref(1)) + val onePartyDummyContract = DummyContract.generateInitial(1337, notary, alice.ref(1)) val miniCorpServices = MockServices(cordappPackages, MINI_CORP_KEY) val ptx = miniCorpServices.signInitialTransaction(onePartyDummyContract) val flow = aliceNode.services.startFlow(CollectSignaturesFlow(ptx, emptySet())) @@ -188,9 +195,9 @@ class CollectSignaturesFlowTests { @Test fun `passes with multiple initial signatures`() { val twoPartyDummyContract = DummyContract.generateInitial(1337, notary, - aliceNode.info.chooseIdentity().ref(1), - bobNode.info.chooseIdentity().ref(2), - bobNode.info.chooseIdentity().ref(3)) + alice.ref(1), + bob.ref(2), + bob.ref(3)) val signedByA = aliceNode.services.signInitialTransaction(twoPartyDummyContract) val signedByBoth = bobNode.services.addSignature(signedByA) val flow = aliceNode.services.startFlow(CollectSignaturesFlow(signedByBoth, emptySet())) diff --git a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt index e17da0775b..a68d4542c8 100644 --- a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt @@ -6,7 +6,7 @@ import net.corda.core.utilities.getOrThrow import net.corda.finance.POUNDS import net.corda.finance.contracts.asset.Cash import net.corda.finance.issuedBy -import net.corda.node.internal.StartedNode +import net.corda.node.services.api.ServiceHubInternal import net.corda.testing.* import net.corda.testing.node.MockNetwork import org.junit.After @@ -16,20 +16,26 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith class FinalityFlowTests { - lateinit var mockNet: MockNetwork - lateinit var aliceNode: StartedNode - lateinit var bobNode: StartedNode - lateinit var notary: Party + private lateinit var mockNet: MockNetwork + private lateinit var aliceServices: ServiceHubInternal + private lateinit var bobServices: ServiceHubInternal + private lateinit var alice: Party + private lateinit var bob: Party + private lateinit var notary: Party @Before fun setup() { mockNet = MockNetwork(cordappPackages = listOf("net.corda.finance.contracts.asset")) - mockNet.createNotaryNode() - aliceNode = mockNet.createPartyNode(ALICE.name) - bobNode = mockNet.createPartyNode(BOB.name) + val notaryNode = mockNet.createNotaryNode() + val aliceNode = mockNet.createPartyNode(ALICE_NAME) + val bobNode = mockNet.createPartyNode(BOB_NAME) mockNet.runNetwork() aliceNode.internals.ensureRegistered() - notary = aliceNode.services.getDefaultNotary() + aliceServices = aliceNode.services + bobServices = bobNode.services + alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME) + bob = bobNode.services.myInfo.identityFromX500Name(BOB_NAME) + notary = notaryNode.services.networkMapCache.getNotary(DUMMY_NOTARY_SERVICE_NAME)!! } @After @@ -39,28 +45,28 @@ class FinalityFlowTests { @Test fun `finalise a simple transaction`() { - val amount = 1000.POUNDS.issuedBy(aliceNode.info.chooseIdentity().ref(0)) + val amount = 1000.POUNDS.issuedBy(alice.ref(0)) val builder = TransactionBuilder(notary) - Cash().generateIssue(builder, amount, bobNode.info.chooseIdentity(), notary) - val stx = aliceNode.services.signInitialTransaction(builder) - val flow = aliceNode.services.startFlow(FinalityFlow(stx)) + Cash().generateIssue(builder, amount, bob, notary) + val stx = aliceServices.signInitialTransaction(builder) + val flow = aliceServices.startFlow(FinalityFlow(stx)) mockNet.runNetwork() val notarisedTx = flow.resultFuture.getOrThrow() notarisedTx.verifyRequiredSignatures() - val transactionSeenByB = bobNode.services.database.transaction { - bobNode.services.validatedTransactions.getTransaction(notarisedTx.id) + val transactionSeenByB = bobServices.database.transaction { + bobServices.validatedTransactions.getTransaction(notarisedTx.id) } assertEquals(notarisedTx, transactionSeenByB) } @Test fun `reject a transaction with unknown parties`() { - val amount = 1000.POUNDS.issuedBy(aliceNode.info.chooseIdentity().ref(0)) + val amount = 1000.POUNDS.issuedBy(alice.ref(0)) val fakeIdentity = CHARLIE // Charlie isn't part of this network, so node A won't recognise them val builder = TransactionBuilder(notary) Cash().generateIssue(builder, amount, fakeIdentity, notary) - val stx = aliceNode.services.signInitialTransaction(builder) - val flow = aliceNode.services.startFlow(FinalityFlow(stx)) + val stx = aliceServices.signInitialTransaction(builder) + val flow = aliceServices.startFlow(FinalityFlow(stx)) mockNet.runNetwork() assertFailsWith { flow.resultFuture.getOrThrow() 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 089885d5f5..f7b99ead79 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 @@ -3,14 +3,14 @@ package net.corda.docs import net.corda.core.contracts.LinearState import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.identity.Party import net.corda.core.node.ServiceHub 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.node.internal.StartedNode -import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.chooseIdentity +import net.corda.node.services.api.ServiceHubInternal +import net.corda.testing.* import net.corda.testing.node.MockNetwork import org.junit.After import org.junit.Before @@ -19,8 +19,10 @@ import kotlin.test.assertEquals class WorkflowTransactionBuildTutorialTest { lateinit var mockNet: MockNetwork - lateinit var nodeA: StartedNode - lateinit var nodeB: StartedNode + lateinit var aliceServices: ServiceHubInternal + lateinit var bobServices: ServiceHubInternal + lateinit var alice: Party + lateinit var bob: Party // Helper method to locate the latest Vault version of a LinearState private inline fun ServiceHub.latest(ref: UniqueIdentifier): StateAndRef { @@ -31,10 +33,15 @@ class WorkflowTransactionBuildTutorialTest { @Before fun setup() { mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.docs")) + // While we don't use the notary, we need there to be one on the network mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) - nodeA = mockNet.createPartyNode() - nodeB = mockNet.createPartyNode() - nodeA.internals.registerInitiatedFlow(RecordCompletionFlow::class.java) + val aliceNode = mockNet.createPartyNode(ALICE_NAME) + val bobNode = mockNet.createPartyNode(BOB_NAME) + aliceNode.internals.registerInitiatedFlow(RecordCompletionFlow::class.java) + aliceServices = aliceNode.services + bobServices = bobNode.services + alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME) + bob = bobNode.services.myInfo.identityFromX500Name(BOB_NAME) } @After @@ -45,50 +52,50 @@ class WorkflowTransactionBuildTutorialTest { @Test fun `Run workflow to completion`() { // Setup a vault subscriber to wait for successful upload of the proposal to NodeB - val nodeBVaultUpdate = nodeB.services.vaultService.updates.toFuture() + val nodeBVaultUpdate = bobServices.vaultService.updates.toFuture() // Kick of the proposal flow - val flow1 = nodeA.services.startFlow(SubmitTradeApprovalFlow("1234", nodeB.info.chooseIdentity())) + val flow1 = aliceServices.startFlow(SubmitTradeApprovalFlow("1234", bob)) // Wait for the flow to finish val proposalRef = flow1.resultFuture.getOrThrow() val proposalLinearId = proposalRef.state.data.linearId // Wait for NodeB to include it's copy in the vault nodeBVaultUpdate.get() // Fetch the latest copy of the state from both nodes - val latestFromA = nodeA.database.transaction { - nodeA.services.latest(proposalLinearId) + val latestFromA = aliceServices.database.transaction { + aliceServices.latest(proposalLinearId) } - val latestFromB = nodeB.database.transaction { - nodeB.services.latest(proposalLinearId) + val latestFromB = bobServices.database.transaction { + bobServices.latest(proposalLinearId) } // Confirm the state as as expected assertEquals(WorkflowState.NEW, proposalRef.state.data.state) assertEquals("1234", proposalRef.state.data.tradeId) - assertEquals(nodeA.info.chooseIdentity(), proposalRef.state.data.source) - assertEquals(nodeB.info.chooseIdentity(), proposalRef.state.data.counterparty) + assertEquals(alice, proposalRef.state.data.source) + assertEquals(bob, proposalRef.state.data.counterparty) assertEquals(proposalRef, latestFromA) assertEquals(proposalRef, latestFromB) // Setup a vault subscriber to pause until the final update is in NodeA and NodeB - val nodeAVaultUpdate = nodeA.services.vaultService.updates.toFuture() - val secondNodeBVaultUpdate = nodeB.services.vaultService.updates.toFuture() + val nodeAVaultUpdate = aliceServices.vaultService.updates.toFuture() + val secondNodeBVaultUpdate = bobServices.vaultService.updates.toFuture() // Run the manual completion flow from NodeB - val flow2 = nodeB.services.startFlow(SubmitCompletionFlow(latestFromB.ref, WorkflowState.APPROVED)) + val flow2 = bobServices.startFlow(SubmitCompletionFlow(latestFromB.ref, WorkflowState.APPROVED)) // wait for the flow to end val completedRef = flow2.resultFuture.getOrThrow() // wait for the vault updates to stabilise nodeAVaultUpdate.get() secondNodeBVaultUpdate.get() // Fetch the latest copies from the vault - val finalFromA = nodeA.database.transaction { - nodeA.services.latest(proposalLinearId) + val finalFromA = aliceServices.database.transaction { + aliceServices.latest(proposalLinearId) } - val finalFromB = nodeB.database.transaction { - nodeB.services.latest(proposalLinearId) + val finalFromB = bobServices.database.transaction { + bobServices.latest(proposalLinearId) } // Confirm the state is as expected assertEquals(WorkflowState.APPROVED, completedRef.state.data.state) assertEquals("1234", completedRef.state.data.tradeId) - assertEquals(nodeA.info.chooseIdentity(), completedRef.state.data.source) - assertEquals(nodeB.info.chooseIdentity(), completedRef.state.data.counterparty) + assertEquals(alice, completedRef.state.data.source) + assertEquals(bob, completedRef.state.data.counterparty) assertEquals(completedRef, finalFromA) assertEquals(completedRef, finalFromB) } diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index 957d6ac442..58aea5fac8 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -7,11 +7,13 @@ import net.corda.core.flows.NotaryFlow import net.corda.core.flows.StateReplacementException import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds import net.corda.node.internal.StartedNode +import net.corda.node.services.api.ServiceHubInternal import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.node.MockNetwork @@ -39,11 +41,11 @@ class NotaryChangeTests { oldNotaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) clientNodeA = mockNet.createNode() clientNodeB = mockNet.createNode() - newNotaryNode = mockNet.createNotaryNode() + newNotaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name.copy(organisation = "Dummy Notary 2")) mockNet.runNetwork() // Clear network map registration messages oldNotaryNode.internals.ensureRegistered() - newNotaryParty = newNotaryNode.info.legalIdentities[1] - oldNotaryParty = oldNotaryNode.info.legalIdentities[1] + oldNotaryParty = newNotaryNode.services.networkMapCache.getNotary(DUMMY_NOTARY_SERVICE_NAME)!! + newNotaryParty = newNotaryNode.services.networkMapCache.getNotary(DUMMY_NOTARY_SERVICE_NAME.copy(organisation = "Dummy Notary 2"))!! } @After @@ -211,10 +213,10 @@ fun issueMultiPartyState(nodeA: StartedNode<*>, nodeB: StartedNode<*>, notaryNod return stx.tx.outRef(0) } -fun issueInvalidState(node: StartedNode<*>, notary: Party): StateAndRef { - val tx = DummyContract.generateInitial(Random().nextInt(), notary, node.info.chooseIdentity().ref(0)) +fun issueInvalidState(services: ServiceHub, identity: Party, notary: Party): StateAndRef { + val tx = DummyContract.generateInitial(Random().nextInt(), notary, identity.ref(0)) tx.setTimeWindow(Instant.now(), 30.seconds) - val stx = node.services.signInitialTransaction(tx) - node.services.recordTransactions(stx) + val stx = services.signInitialTransaction(tx) + services.recordTransactions(stx) return stx.tx.outRef(0) } \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt index 740c51be74..a137cc41e9 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt @@ -9,10 +9,11 @@ import net.corda.core.flows.NotaryError import net.corda.core.flows.NotaryException import net.corda.core.flows.NotaryFlow import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow -import net.corda.node.internal.StartedNode +import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.issueInvalidState import net.corda.testing.* import net.corda.testing.contracts.DummyContract @@ -27,18 +28,22 @@ import kotlin.test.assertFailsWith class ValidatingNotaryServiceTests { lateinit var mockNet: MockNetwork - lateinit var notaryNode: StartedNode - lateinit var clientNode: StartedNode + lateinit var notaryServices: ServiceHubInternal + lateinit var aliceServices: ServiceHubInternal lateinit var notary: Party + lateinit var alice: Party @Before fun setup() { mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) - notaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) - clientNode = mockNet.createNode() + val notaryNode = mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) + val aliceNode = mockNet.createNode(legalName = ALICE_NAME) mockNet.runNetwork() // Clear network map registration messages notaryNode.internals.ensureRegistered() - notary = clientNode.services.getDefaultNotary() + notaryServices = notaryNode.services + aliceServices = aliceNode.services + notary = notaryServices.networkMapCache.getNotary(DUMMY_NOTARY_SERVICE_NAME)!! + alice = aliceServices.myInfo.identityFromX500Name(ALICE_NAME) } @After @@ -49,11 +54,11 @@ class ValidatingNotaryServiceTests { @Test fun `should report error for invalid transaction dependency`() { val stx = run { - val inputState = issueInvalidState(clientNode, notary) + val inputState = issueInvalidState(aliceServices, alice, notary) val tx = TransactionBuilder(notary) .addInputState(inputState) - .addCommand(dummyCommand(clientNode.info.chooseIdentity().owningKey)) - clientNode.services.signInitialTransaction(tx) + .addCommand(dummyCommand(alice.owningKey)) + aliceServices.signInitialTransaction(tx) } val future = runClient(stx) @@ -67,11 +72,11 @@ class ValidatingNotaryServiceTests { fun `should report error for missing signatures`() { val expectedMissingKey = MEGA_CORP_KEY.public val stx = run { - val inputState = issueState(clientNode) + val inputState = issueState(aliceServices, alice) val command = Command(DummyContract.Commands.Move(), expectedMissingKey) val tx = TransactionBuilder(notary).withItems(inputState, command) - clientNode.services.signInitialTransaction(tx) + aliceServices.signInitialTransaction(tx) } val ex = assertFailsWith(NotaryException::class) { @@ -87,16 +92,16 @@ class ValidatingNotaryServiceTests { private fun runClient(stx: SignedTransaction): CordaFuture> { val flow = NotaryFlow.Client(stx) - val future = clientNode.services.startFlow(flow).resultFuture + val future = aliceServices.startFlow(flow).resultFuture mockNet.runNetwork() return future } - fun issueState(node: StartedNode<*>): StateAndRef<*> { - val tx = DummyContract.generateInitial(Random().nextInt(), notary, node.info.chooseIdentity().ref(0)) - val signedByNode = node.services.signInitialTransaction(tx) - val stx = notaryNode.services.addSignature(signedByNode, notary.owningKey) - node.services.recordTransactions(stx) + fun issueState(serviceHub: ServiceHub, identity: Party): StateAndRef<*> { + val tx = DummyContract.generateInitial(Random().nextInt(), notary, identity.ref(0)) + val signedByNode = serviceHub.signInitialTransaction(tx) + val stx = notaryServices.addSignature(signedByNode, notary.owningKey) + serviceHub.recordTransactions(stx) return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) } } diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt index d379e3b98a..147d6a65e0 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt @@ -41,12 +41,14 @@ class BankOfCordaRPCClientTest { // Register for Big Corporation Vault updates val vaultUpdatesBigCorp = bigCorpProxy.vaultTrackByCriteria(Cash.State::class.java, criteria).updates + val bigCorporation = bigCorpProxy.wellKnownPartyFromX500Name(BIGCORP_LEGAL_NAME)!! + // Kick-off actual Issuer Flow val anonymous = true val notary = bocProxy.notaryIdentities().first() bocProxy.startFlow(::CashIssueAndPaymentFlow, 1000.DOLLARS, BIG_CORP_PARTY_REF, - nodeBigCorporation.nodeInfo.chooseIdentity(), + bigCorporation, anonymous, notary).returnValue.getOrThrow() diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index 84dc90fe02..ae810b581a 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -44,12 +44,9 @@ import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.node.utilities.CertificateAndKeyPair import net.corda.nodeapi.internal.ServiceInfo -import net.corda.testing.DUMMY_KEY_1 -import net.corda.testing.initialiseTestSerialization +import net.corda.testing.* import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties -import net.corda.testing.resetTestSerialization -import net.corda.testing.testNodeConfiguration import org.apache.activemq.artemis.utils.ReusableLatch import org.slf4j.Logger import java.io.Closeable @@ -393,13 +390,13 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, } @JvmOverloads - fun createNotaryNode(legalName: CordaX500Name? = null, validating: Boolean = true): StartedNode { + fun createNotaryNode(legalName: CordaX500Name = DUMMY_NOTARY.name, validating: Boolean = true): StartedNode { return createNode(legalName = legalName, configOverrides = { whenever(it.notary).thenReturn(NotaryConfig(validating)) }) } - fun createNotaryNode(legalName: CordaX500Name? = null, + fun createNotaryNode(legalName: CordaX500Name = DUMMY_NOTARY.name, validating: Boolean = true, nodeFactory: Factory): StartedNode { return createNode(legalName = legalName, nodeFactory = nodeFactory, configOverrides = { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt index a7f7e11157..9e73764f06 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt @@ -31,6 +31,7 @@ val DUMMY_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(20)) /** Dummy notary identity for tests and simulations */ val DUMMY_NOTARY_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(DUMMY_NOTARY) val DUMMY_NOTARY: Party get() = Party(CordaX500Name(organisation = "Notary Service", locality = "Zurich", country = "CH"), DUMMY_NOTARY_KEY.public) +val DUMMY_NOTARY_SERVICE_NAME: CordaX500Name = DUMMY_NOTARY.name.copy(commonName = "corda.notary.validating") val DUMMY_MAP_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(30)) } /** Dummy network map service identity for tests and simulations */ @@ -51,17 +52,20 @@ val DUMMY_BANK_C: Party get() = Party(CordaX500Name(organisation = "Bank C", loc val ALICE_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(70)) } /** Dummy individual identity for tests and simulations */ val ALICE_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(ALICE) -val ALICE: Party get() = Party(CordaX500Name(organisation = "Alice Corp", locality = "Madrid", country = "ES"), ALICE_KEY.public) +val ALICE_NAME = CordaX500Name(organisation = "Alice Corp", locality = "Madrid", country = "ES") +val ALICE: Party get() = Party(ALICE_NAME, ALICE_KEY.public) val BOB_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(80)) } /** Dummy individual identity for tests and simulations */ val BOB_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(BOB) -val BOB: Party get() = Party(CordaX500Name(organisation = "Bob Plc", locality = "Rome", country = "IT"), BOB_KEY.public) +val BOB_NAME = CordaX500Name(organisation = "Bob Plc", locality = "Rome", country = "IT") +val BOB: Party get() = Party(BOB_NAME, BOB_KEY.public) val CHARLIE_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(90)) } /** Dummy individual identity for tests and simulations */ val CHARLIE_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(CHARLIE) -val CHARLIE: Party get() = Party(CordaX500Name(organisation = "Charlie Ltd", locality = "Athens", country = "GR"), CHARLIE_KEY.public) +val CHARLIE_NAME = CordaX500Name(organisation = "Charlie Ltd", locality = "Athens", country = "GR") +val CHARLIE: Party get() = Party(CHARLIE_NAME, CHARLIE_KEY.public) val DUMMY_REGULATOR_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(100)) } /** Dummy regulator for tests and simulations */ diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt index d12dbc7a41..7bdbecaf17 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt @@ -13,6 +13,9 @@ import net.corda.node.services.config.VerifierType import net.corda.testing.ALICE import net.corda.testing.DUMMY_NOTARY import net.corda.testing.chooseIdentity +import net.corda.node.services.transactions.ValidatingNotaryService +import net.corda.nodeapi.internal.ServiceInfo +import net.corda.testing.* import net.corda.testing.driver.NetworkMapStartStrategy import org.junit.Test import java.util.* @@ -116,14 +119,15 @@ class VerifierTests { ) { val aliceFuture = startNode(providedName = ALICE.name) val notaryFuture = startNotaryNode(DUMMY_NOTARY.name, verifierType = VerifierType.OutOfProcess) - val alice = aliceFuture.get() - val notary = notaryFuture.get() - val notaryIdentity = notary.nodeInfo.legalIdentities[1] - startVerifier(notary) - alice.rpc.startFlow(::CashIssueFlow, 10.DOLLARS, OpaqueBytes.of(0), notaryIdentity).returnValue.get() - notary.waitUntilNumberOfVerifiers(1) + val aliceNode = aliceFuture.get() + val notaryNode = notaryFuture.get() + val alice = notaryNode.rpc.wellKnownPartyFromX500Name(ALICE_NAME)!! + val notary = notaryNode.rpc.notaryPartyFromX500Name(DUMMY_NOTARY_SERVICE_NAME)!! + startVerifier(notaryNode) + aliceNode.rpc.startFlow(::CashIssueFlow, 10.DOLLARS, OpaqueBytes.of(0), notary).returnValue.get() + notaryNode.waitUntilNumberOfVerifiers(1) for (i in 1..10) { - alice.rpc.startFlow(::CashPaymentFlow, 10.DOLLARS, alice.nodeInfo.chooseIdentity()).returnValue.get() + aliceNode.rpc.startFlow(::CashPaymentFlow, 10.DOLLARS, alice).returnValue.get() } } } From d1c51115675d7c7e3edf1f34ff60084b210043fc Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Thu, 12 Oct 2017 11:39:22 +0100 Subject: [PATCH 165/185] =?UTF-8?q?Remove=20the=20two=20header=20lines=20f?= =?UTF-8?q?rom=20the=20diff=20output=20to=20simplify=20the=20+/-=20?= =?UTF-8?q?=E2=80=A6=20(#1875)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove the two header lines from the diff output to simplify the +/- check. * Revert Kt class changes as they alter the public API. --- .ci/check-api-changes.sh | 9 +++++---- .../net/corda/core/serialization/SerializationAPI.kt | 2 -- .../main/kotlin/net/corda/core/utilities/KotlinUtils.kt | 2 -- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.ci/check-api-changes.sh b/.ci/check-api-changes.sh index 7694d995dd..e9ac25cf6d 100755 --- a/.ci/check-api-changes.sh +++ b/.ci/check-api-changes.sh @@ -10,13 +10,14 @@ if [ ! -f $apiCurrent ]; then exit -1 fi -diffContents=`diff -u $apiCurrent $APIHOME/../build/api/api-corda-*.txt` -echo "Diff contents:" +# Remove the two header lines from the diff output. +diffContents=`diff -u $apiCurrent $APIHOME/../build/api/api-corda-*.txt | tail --lines=+3` +echo "Diff contents:" echo "$diffContents" echo # A removed line means that an API was either deleted or modified. -removals=$(echo "$diffContents" | grep "^-\s") +removals=$(echo "$diffContents" | grep "^-") removalCount=`grep -v "^$" < Date: Thu, 12 Oct 2017 13:21:00 +0100 Subject: [PATCH 166/185] Remove broken link. --- docs/source/corda-api.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/corda-api.rst b/docs/source/corda-api.rst index 57863be4b7..8b5dd94053 100644 --- a/docs/source/corda-api.rst +++ b/docs/source/corda-api.rst @@ -74,7 +74,7 @@ The following modules are available but we do not commit to their stability or c * **net.corda.samples.demos.bankofcorda**: simulates the role of an asset issuing authority (eg. central bank for cash) * **net.corda.samples.demos.irs**: demonstrates an Interest Rate Swap agreement between two banks * **net.corda.samples.demos.notary**: a simple demonstration of a node getting multiple transactions notarised by a distributed (Raft or BFT SMaRt) notary -* **net.corda.samples.demos.simmvaluation**: See our [main documentation site](https://docs.corda.net/initial-margin-agreement.html) regarding the SIMM valuation and agreement on a distributed ledger +* **net.corda.samples.demos.simmvaluation**: A demo of SIMM valuation and agreement on a distributed ledger * **net.corda.samples.demos.trader**: demonstrates four nodes, a notary, an issuer of cash (Bank of Corda), and two parties trading with each other, exchanging cash for a commercial paper * **net.corda.node.smoke.test.utils**: test utilities for smoke testing * **net.corda.node.test.common**: common test functionality @@ -90,4 +90,4 @@ The following modules are available but we do not commit to their stability or c .. warning:: Code inside any package in the ``net.corda`` namespace which contains ``.internal`` or in ``net.corda.node`` for internal use only. Future releases will reject any CorDapps that use types from these packages. -.. warning:: The web server module will be removed in future. You should call Corda nodes through RPC from your web server of choice e.g., Spring Boot, Vertx, Undertow. \ No newline at end of file +.. warning:: The web server module will be removed in future. You should call Corda nodes through RPC from your web server of choice e.g., Spring Boot, Vertx, Undertow. From 7b10e92819c29b75bd17582f83630ad24b26beca Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Fri, 13 Oct 2017 10:36:25 +0100 Subject: [PATCH 167/185] Fixed AbstractNode to load custom notary services properly (#1720) * Fixed AbstractNode to load custom notary services properly. Added a custom notary sample. * Prevent multiple custom notaries from being loaded * Throw if more than once custom notary service is loaded --- .../corda/core/node/services/NotaryService.kt | 6 ++- docs/source/corda-configuration-file.rst | 12 ++++-- docs/source/running-the-demos.rst | 22 +++++----- docs/source/tutorial-custom-notary.rst | 23 +++++++---- docs/source/tutorials-index.rst | 1 + .../net/corda/node/internal/AbstractNode.kt | 40 ++++++++++++++----- .../node/services/config/NodeConfiguration.kt | 17 +++++--- .../net/corda/bank/BankOfCordaDriver.kt | 2 +- samples/notary-demo/build.gradle | 7 +++- .../corda/notarydemo/CustomNotaryCordform.kt | 36 +++++++++++++++++ .../corda/notarydemo/MyCustomNotaryService.kt | 27 ++++++++++--- 11 files changed, 148 insertions(+), 45 deletions(-) create mode 100644 samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt rename docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt => samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt (74%) diff --git a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt index 574008d6d8..8241fcd13c 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt @@ -1,5 +1,6 @@ package net.corda.core.node.services +import com.google.common.primitives.Booleans import net.corda.core.contracts.StateRef import net.corda.core.contracts.TimeWindow import net.corda.core.crypto.* @@ -15,12 +16,13 @@ import java.security.PublicKey abstract class NotaryService : SingletonSerializeAsToken() { companion object { const val ID_PREFIX = "corda.notary." - fun constructId(validating: Boolean, raft: Boolean = false, bft: Boolean = false): String { - require(!raft || !bft) + fun constructId(validating: Boolean, raft: Boolean = false, bft: Boolean = false, custom: Boolean = false): String { + require(Booleans.countTrue(raft, bft, custom) <= 1) { "At most one of raft, bft or custom may be true" } return StringBuffer(ID_PREFIX).apply { append(if (validating) "validating" else "simple") if (raft) append(".raft") if (bft) append(".bft") + if (custom) append(".custom") }.toString() } } diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index 8511b2d486..2b2627f56e 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -94,7 +94,7 @@ path to the node's base directory. .. note:: The driver will not automatically create a webserver instance, but the Cordformation will. If this field is present the web server will start. -:notary: Optional config object which if present configures the node to run as a notary. If part of a Raft or BFT SMaRt +:notary: Optional configuration object which if present configures the node to run as a notary. If part of a Raft or BFT SMaRt cluster then specify ``raft`` or ``bftSMaRt`` respectively as described below. If a single node notary then omit both. :validating: Boolean to determine whether the notary is a validating or non-validating one. @@ -108,11 +108,15 @@ path to the node's base directory. members must be active and be able to communicate with the cluster leader for joining. If empty, a new cluster will be bootstrapped. - :bftSMaRt: If part of a distributed BFT SMaRt cluster specify this config object, with the following settings: + :bftSMaRt: If part of a distributed BFT-SMaRt cluster specify this config object, with the following settings: - :replicaId: + :replicaId: The zero-based index of the current replica. All replicas must specify a unique replica id. - :clusterAddresses: + :clusterAddresses: List of all BFT-SMaRt cluster member addresses. + + :custom: If `true`, will load and install a notary service from a CorDapp. See :doc:`tutorial-custom-notary`. + + Only one of ``raft``, ``bftSMaRt`` or ``custom`` configuration values may be specified. :networkMapService: If `null`, or missing the node is declaring itself as the NetworkMapService host. Otherwise this is a config object with the details of the network map service: diff --git a/docs/source/running-the-demos.rst b/docs/source/running-the-demos.rst index f9e78737d1..11ebb794c2 100644 --- a/docs/source/running-the-demos.rst +++ b/docs/source/running-the-demos.rst @@ -35,7 +35,7 @@ To run from the command line in Unix: 2. Run ``./samples/trader-demo/build/nodes/runnodes`` to open up four new terminals with the four nodes 3. Run ``./gradlew samples:trader-demo:runBank`` to instruct the bank node to issue cash and commercial paper to the buyer and seller nodes respectively. 4. Run ``./gradlew samples:trader-demo:runSeller`` to trigger the transaction. If you entered ``flow watch`` - + you can see flows running on both sides of transaction. Additionally you should see final trade information displayed to your terminal. @@ -45,7 +45,7 @@ To run from the command line in Windows: 2. Run ``samples\trader-demo\build\nodes\runnodes`` to open up four new terminals with the four nodes 3. Run ``gradlew samples:trader-demo:runBank`` to instruct the buyer node to request issuance of some cash from the Bank of Corda node 4. Run ``gradlew samples:trader-demo:runSeller`` to trigger the transaction. If you entered ``flow watch`` - + you can see flows running on both sides of transaction. Additionally you should see final trade information displayed to your terminal. @@ -112,8 +112,11 @@ 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 `_ version of the demo will start three distributed notary nodes. -The `BFT SMaRt `_ version of the demo will start four distributed notary nodes. + +* The `Raft `_ version of the demo will start three distributed notary nodes. +* The `BFT SMaRt `_ version of the demo will start four distributed notary nodes. +* The Single version of the demo will start a single-node validating notary service. +* The Custom version of the demo will load and start a custom single-node notary service that is defined the demo CorDapp. 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. @@ -122,9 +125,9 @@ You will notice that successive transactions get signed by different members of To run the Raft version of the demo from the command line in Unix: -1. Run ``./gradlew samples:notary-demo:deployNodes``, which will create all three types of notaries' node directories - with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT`` and ``nodesSingle`` for BFT and - Single notaries). +1. Run ``./gradlew samples:notary-demo:deployNodes``, which will create node directories for all versions of the demo, + with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT``, ``nodesSingle``, and ``nodesCustom`` for + BFT, Single and Custom notaries respectively). 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 @@ -133,8 +136,8 @@ To run the Raft version of the demo from the command line in Unix: To run from the command line in Windows: 1. Run ``gradlew samples:notary-demo:deployNodes``, which will create all three types of notaries' node directories - with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT`` and ``nodesSingle`` for BFT and - Single notaries). + with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT``, ``nodesSingle``, and ``nodesCustom`` for + BFT, Single and Custom notaries respectively). 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 @@ -142,6 +145,7 @@ To run from the command line in Windows: To run the BFT SMaRt notary demo, use ``nodesBFT`` instead of ``nodesRaft`` in the path (you will see messages from notary nodes trying to communicate each other sometime with connection errors, that's normal). For a single notary node, use ``nodesSingle``. +For the custom notary service use ``nodesCustom`. Distributed notary nodes store consumed states in a replicated commit log, which is backed by a H2 database on each node. You can ascertain that the commit log is synchronised across the cluster by accessing and comparing each of the nodes' backing stores diff --git a/docs/source/tutorial-custom-notary.rst b/docs/source/tutorial-custom-notary.rst index cabefdd203..28d5dc1158 100644 --- a/docs/source/tutorial-custom-notary.rst +++ b/docs/source/tutorial-custom-notary.rst @@ -1,17 +1,17 @@ .. highlight:: kotlin -Writing a custom notary service -=============================== +Writing a custom notary service (experimental) +============================================== -.. warning:: Customising a notary service is an advanced feature and not recommended for most use-cases. Currently, +.. warning:: Customising a notary service is still an experimental feature and not recommended for most use-cases. Currently, customising Raft or BFT notaries is not yet fully supported. If you want to write your own Raft notary you will have to implement a custom database connector (or use a separate database for the notary), and use a custom configuration file. Similarly to writing an oracle service, the first step is to create a service class in your CorDapp and annotate it -with ``@CordaService``. The Corda node scans for any class with this annotation and initialises them. The only requirement -is that the class provide a constructor with a single parameter of type ``AppServiceHub``. +with ``@CordaService``. The Corda node scans for any class with this annotation and initialises them. The custom notary +service class should provide a constructor with two parameters of types ``AppServiceHub`` and ``PublicKey``. -.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt +.. literalinclude:: ../../samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt :language: kotlin :start-after: START 1 :end-before: END 1 @@ -20,7 +20,16 @@ The next step is to write a notary service flow. You are free to copy and modify as ``ValidatingNotaryFlow``, ``NonValidatingNotaryFlow``, or implement your own from scratch (following the ``NotaryFlow.Service`` template). Below is an example of a custom flow for a *validating* notary service: -.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt +.. literalinclude:: ../../samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt :language: kotlin :start-after: START 2 :end-before: END 2 + +To enable the service, add the following to the node configuration: + +.. parsed-literal:: + + notary : { + validating : true # Set to false if your service is non-validating + custom : true + } \ No newline at end of file diff --git a/docs/source/tutorials-index.rst b/docs/source/tutorials-index.rst index c0b1a5af14..f14be4385f 100644 --- a/docs/source/tutorials-index.rst +++ b/docs/source/tutorials-index.rst @@ -16,6 +16,7 @@ Tutorials flow-testing running-a-notary oracles + tutorial-custom-notary tutorial-tear-offs tutorial-attachments event-scheduling \ No newline at end of file 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 e08b457c37..9a7e5cbbeb 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -19,14 +19,11 @@ import net.corda.core.internal.concurrent.flatMap import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.toX509CertHolder import net.corda.core.internal.uncheckedCast -import net.corda.core.messaging.CordaRPCOps -import net.corda.core.messaging.RPCOps -import net.corda.core.messaging.SingleMessageRecipient -import net.corda.core.node.* import net.corda.core.messaging.* import net.corda.core.node.AppServiceHub import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub +import net.corda.core.node.StateLoader import net.corda.core.node.services.* import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.schemas.MappedSchema @@ -257,7 +254,8 @@ abstract class AbstractNode(config: NodeConfiguration, private class ServiceInstantiationException(cause: Throwable?) : CordaException("Service Instantiation Error", cause) private fun installCordaServices() { - cordappProvider.cordapps.flatMap { it.services }.forEach { + val loadedServices = cordappProvider.cordapps.flatMap { it.services } + filterServicesToInstall(loadedServices).forEach { try { installCordaService(it) } catch (e: NoSuchMethodException) { @@ -271,6 +269,25 @@ abstract class AbstractNode(config: NodeConfiguration, } } + private fun filterServicesToInstall(loadedServices: List>): List> { + val customNotaryServiceList = loadedServices.filter { isNotaryService(it) } + if (customNotaryServiceList.isNotEmpty()) { + if (configuration.notary?.custom == true) { + require(customNotaryServiceList.size == 1) { + "Attempting to install more than one notary service: ${customNotaryServiceList.joinToString()}" + } + } + else return loadedServices - customNotaryServiceList + } + return loadedServices + } + + /** + * If the [serviceClass] is a notary service, it will only be enable 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 */ @@ -321,14 +338,15 @@ abstract class AbstractNode(config: NodeConfiguration, fun installCordaService(serviceClass: Class): T { serviceClass.requireAnnotation() val service = try { - if (NotaryService::class.java.isAssignableFrom(serviceClass)) { + val serviceContext = AppServiceHubImpl(services) + if (isNotaryService(serviceClass)) { check(myNotaryIdentity != null) { "Trying to install a notary service but no notary identity specified" } - val constructor = serviceClass.getDeclaredConstructor(ServiceHub::class.java, PublicKey::class.java).apply { isAccessible = true } - constructor.newInstance(services, myNotaryIdentity!!.owningKey) + 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 } - val serviceContext = AppServiceHubImpl(services) serviceContext.serviceInstance = extendedServiceConstructor.newInstance(serviceContext) serviceContext.serviceInstance } catch (ex: NoSuchMethodException) { @@ -688,7 +706,9 @@ abstract class AbstractNode(config: NodeConfiguration, // Node's main identity Pair("identity", myLegalName) } else { - val notaryId = notaryConfig.run { NotaryService.constructId(validating, raft != null, bftSMaRt != null) } + val notaryId = notaryConfig.run { + NotaryService.constructId(validating, raft != null, bftSMaRt != null, custom) + } if (notaryConfig.bftSMaRt == null && notaryConfig.raft == null) { // Node's notary identity Pair(notaryId, myLegalName.copy(commonName = notaryId)) 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 a72465ccee..782064bf76 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 @@ -36,9 +36,15 @@ interface NodeConfiguration : NodeSSLConfiguration { val additionalNodeInfoPollingFrequencyMsec: Long } -data class NotaryConfig(val validating: Boolean, val raft: RaftConfig? = null, val bftSMaRt: BFTSMaRtConfiguration? = null) { +data class NotaryConfig(val validating: Boolean, + val raft: RaftConfig? = null, + val bftSMaRt: BFTSMaRtConfiguration? = null, + val custom: Boolean = false +) { init { - require(raft == null || bftSMaRt == null) { "raft and bftSMaRt configs cannot be specified together" } + require(raft == null || bftSMaRt == null || !custom) { + "raft, bftSMaRt, and custom configs cannot be specified together" + } } } @@ -46,9 +52,10 @@ data class RaftConfig(val nodeAddress: NetworkHostAndPort, val clusterAddresses: /** @param exposeRaces for testing only, so its default is not in reference.conf but here. */ data class BFTSMaRtConfiguration constructor(val replicaId: Int, - val clusterAddresses: List, - val debug: Boolean = false, - val exposeRaces: Boolean = false) { + val clusterAddresses: List, + val debug: Boolean = false, + val exposeRaces: Boolean = false +) { init { require(replicaId >= 0) { "replicaId cannot be negative" } } diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt index ac2e890f09..81dbf12166 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt @@ -69,7 +69,7 @@ private class BankOfCordaDriver { val bigCorpUser = User(BIGCORP_USERNAME, "test", permissions = setOf( startFlowPermission())) - startNotaryNode(DUMMY_NOTARY.name, validating = false) + startNotaryNode(DUMMY_NOTARY.name, validating = true) val bankOfCorda = startNode( providedName = BOC.name, rpcUsers = listOf(bankUser)) diff --git a/samples/notary-demo/build.gradle b/samples/notary-demo/build.gradle index a13ab70ad8..4d3f97f065 100644 --- a/samples/notary-demo/build.gradle +++ b/samples/notary-demo/build.gradle @@ -46,13 +46,18 @@ publishing { } } -task deployNodes(dependsOn: ['deployNodesSingle', 'deployNodesRaft', 'deployNodesBFT']) +task deployNodes(dependsOn: ['deployNodesSingle', 'deployNodesRaft', 'deployNodesBFT', 'deployNodesCustom']) task deployNodesSingle(type: Cordform, dependsOn: 'jar') { directory "./build/nodes/nodesSingle" definitionClass = 'net.corda.notarydemo.SingleNotaryCordform' } +task deployNodesCustom(type: Cordform, dependsOn: 'jar') { + directory "./build/nodes/nodesCustom" + definitionClass = 'net.corda.notarydemo.CustomNotaryCordform' +} + task deployNodesRaft(type: Cordform, dependsOn: 'jar') { directory "./build/nodes/nodesRaft" definitionClass = 'net.corda.notarydemo.RaftNotaryCordform' diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt new file mode 100644 index 0000000000..900150da69 --- /dev/null +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt @@ -0,0 +1,36 @@ +package net.corda.notarydemo + +import net.corda.cordform.CordformContext +import net.corda.cordform.CordformDefinition +import net.corda.core.internal.div +import net.corda.node.services.config.NotaryConfig +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.internal.demorun.* + +fun main(args: Array) = CustomNotaryCordform.runNodes() + +object CustomNotaryCordform : CordformDefinition("build" / "notary-demo-nodes") { + init { + node { + name(ALICE.name) + p2pPort(10002) + rpcPort(10003) + rpcUsers(notaryDemoUser) + } + node { + name(BOB.name) + p2pPort(10005) + rpcPort(10006) + } + node { + name(DUMMY_NOTARY.name) + p2pPort(10009) + rpcPort(10010) + notary(NotaryConfig(validating = true, custom = true)) + } + } + + override fun setup(context: CordformContext) {} +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt similarity index 74% rename from docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt rename to samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt index d7331d146c..55adae9ee2 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt @@ -1,4 +1,4 @@ -package net.corda.docs +package net.corda.notarydemo import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.TimeWindow @@ -8,11 +8,17 @@ import net.corda.core.node.AppServiceHub import net.corda.core.node.services.CordaService import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.TrustedAuthorityNotaryService +import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionWithSignatures import net.corda.node.services.transactions.PersistentUniquenessProvider import java.security.PublicKey import java.security.SignatureException +/** + * A custom notary service should provide a constructor that accepts two parameters of types [AppServiceHub] and [PublicKey]. + * + * Note that at present only a single-node notary service can be customised. + */ // START 1 @CordaService class MyCustomValidatingNotaryService(override val services: AppServiceHub, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { @@ -26,6 +32,7 @@ class MyCustomValidatingNotaryService(override val services: AppServiceHub, over } // END 1 +@Suppress("UNUSED_PARAMETER") // START 2 class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidatingNotaryService) : NotaryFlow.Service(otherSide, service) { /** @@ -38,11 +45,15 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false)) val notary = stx.notary checkNotary(notary) - val timeWindow: TimeWindow? = if (stx.isNotaryChangeTransaction()) - null - else - stx.tx.timeWindow - val transactionWithSignatures = stx.resolveTransactionWithSignatures(serviceHub) + var timeWindow: TimeWindow? = null + val transactionWithSignatures = if (stx.isNotaryChangeTransaction()) { + stx.resolveNotaryChangeTransaction(serviceHub) + } else { + val wtx = stx.tx + customVerify(wtx.toLedgerTransaction(serviceHub)) + timeWindow = wtx.timeWindow + stx + } checkSignatures(transactionWithSignatures) return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!) } catch (e: Exception) { @@ -54,6 +65,10 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating } } + private fun customVerify(transaction: LedgerTransaction) { + // Add custom verification logic + } + private fun checkSignatures(tx: TransactionWithSignatures) { try { tx.verifySignaturesExcept(service.notaryIdentityKey) From ce5b7de71801a557bb57602e8af2e5ec8ba63959 Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Fri, 13 Oct 2017 12:15:52 +0100 Subject: [PATCH 168/185] CORDA-530 Unduplicate code (#1791) --- .../net/corda/core/flows/AttachmentTests.kt | 3 +- .../AttachmentSerializationTest.kt | 4 +- .../network/PersistentNetworkMapCache.kt | 132 ++++++++---------- .../persistence/DBCheckpointStorage.kt | 9 +- .../persistence/NodeAttachmentService.kt | 8 +- .../node/services/vault/NodeVaultService.kt | 100 ++++++------- .../node/utilities/AppendOnlyPersistentMap.kt | 16 ++- .../utilities/DatabaseTransactionManager.kt | 1 + .../net/corda/node/utilities/PersistentMap.kt | 22 +-- .../persistence/NodeAttachmentStorageTest.kt | 24 ++-- 10 files changed, 152 insertions(+), 167 deletions(-) diff --git a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt index ab4936d6fc..c9974d5dc3 100644 --- a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt @@ -12,7 +12,6 @@ import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.persistence.NodeAttachmentService -import net.corda.node.utilities.DatabaseTransactionManager import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.ALICE import net.corda.testing.ALICE_NAME @@ -147,7 +146,7 @@ class AttachmentTests { val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = attachment) aliceNode.database.transaction { - DatabaseTransactionManager.current().session.update(corruptAttachment) + session.update(corruptAttachment) } // Get n1 to fetch the attachment. Should receive corrupted bytes. diff --git a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt index 89bc81dad9..f6d1eec6b2 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt @@ -16,7 +16,7 @@ import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.internal.StartedNode import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.persistence.NodeAttachmentService -import net.corda.node.utilities.DatabaseTransactionManager +import net.corda.node.utilities.currentDBSession import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.chooseIdentity import net.corda.testing.node.MockNetwork @@ -54,7 +54,7 @@ private fun StartedNode<*>.hackAttachment(attachmentId: SecureHash, content: Str * @see NodeAttachmentService.importAttachment */ private fun updateAttachment(attachmentId: SecureHash, data: ByteArray) { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val attachment = session.get(NodeAttachmentService.DBAttachment::class.java, attachmentId.toString()) attachment?.let { attachment.content = data 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 1f24e27526..eb45e7a0df 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 @@ -31,7 +31,6 @@ import net.corda.node.services.messaging.sendRequest import net.corda.node.services.network.NetworkMapService.FetchMapResponse import net.corda.node.services.network.NetworkMapService.SubscribeResponse import net.corda.node.utilities.AddOrRemove -import net.corda.node.utilities.DatabaseTransactionManager import net.corda.node.utilities.bufferUntilDatabaseCommit import net.corda.node.utilities.wrapWithDatabaseTransaction import org.hibernate.Session @@ -39,7 +38,6 @@ import rx.Observable import rx.subjects.PublishSubject import java.security.PublicKey import java.security.SignatureException -import java.time.Duration import java.util.* import javax.annotation.concurrent.ThreadSafe import kotlin.collections.HashMap @@ -93,7 +91,7 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) init { loadFromFiles() - serviceHub.database.transaction { loadFromDB() } + serviceHub.database.transaction { loadFromDB(session) } } private fun loadFromFiles() { @@ -102,7 +100,7 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) } override fun getPartyInfo(party: Party): PartyInfo? { - val nodes = serviceHub.database.transaction { queryByIdentityKey(party.owningKey) } + val nodes = serviceHub.database.transaction { queryByIdentityKey(session, party.owningKey) } if (nodes.size == 1 && nodes[0].isLegalIdentity(party)) { return PartyInfo.SingleNode(party, nodes[0].addresses) } @@ -117,9 +115,9 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) } override fun getNodeByLegalName(name: CordaX500Name): NodeInfo? = getNodesByLegalName(name).firstOrNull() - override fun getNodesByLegalName(name: CordaX500Name): List = serviceHub.database.transaction { queryByLegalName(name) } + override fun getNodesByLegalName(name: CordaX500Name): List = serviceHub.database.transaction { queryByLegalName(session, name) } override fun getNodesByLegalIdentityKey(identityKey: PublicKey): List = - serviceHub.database.transaction { queryByIdentityKey(identityKey) } + serviceHub.database.transaction { queryByIdentityKey(session, identityKey) } override fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? { val wellKnownParty = serviceHub.identityService.wellKnownPartyFromAnonymous(party) @@ -128,9 +126,9 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) } } - override fun getNodeByAddress(address: NetworkHostAndPort): NodeInfo? = serviceHub.database.transaction { queryByAddress(address) } + override fun getNodeByAddress(address: NetworkHostAndPort): NodeInfo? = serviceHub.database.transaction { queryByAddress(session, address) } - override fun getPeerCertificateByLegalName(name: CordaX500Name): PartyAndCertificate? = serviceHub.database.transaction { queryIdentityByLegalName(name) } + override fun getPeerCertificateByLegalName(name: CordaX500Name): PartyAndCertificate? = serviceHub.database.transaction { queryIdentityByLegalName(session, name) } override fun track(): DataFeed, MapChange> { synchronized(_changed) { @@ -204,7 +202,7 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) synchronized(_changed) { registeredNodes.remove(node.legalIdentities.first().owningKey) serviceHub.database.transaction { - removeInfoDB(node) + removeInfoDB(session, node) changePublisher.onNext(MapChange.Removed(node)) } } @@ -238,10 +236,8 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) } override val allNodes: List - get () = serviceHub.database.transaction { - createSession { - getAllInfos(it).map { it.toNodeInfo() } - } + get() = serviceHub.database.transaction { + getAllInfos(session).map { it.toNodeInfo() } } private fun processRegistration(reg: NodeRegistration) { @@ -259,10 +255,6 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) // Changes related to NetworkMap redesign // TODO It will be properly merged into network map cache after services removal. - private inline fun createSession(block: (Session) -> T): T { - return DatabaseTransactionManager.current().session.let { block(it) } - } - private fun getAllInfos(session: Session): List { val criteria = session.criteriaBuilder.createQuery(NodeInfoSchemaV1.PersistentNodeInfo::class.java) criteria.select(criteria.from(NodeInfoSchemaV1.PersistentNodeInfo::class.java)) @@ -272,24 +264,22 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) /** * Load NetworkMap data from the database if present. Node can start without having NetworkMapService configured. */ - private fun loadFromDB() { + private fun loadFromDB(session: Session) { logger.info("Loading network map from database...") - createSession { - val result = getAllInfos(it) - for (nodeInfo in result) { - try { - logger.info("Loaded node info: $nodeInfo") - val node = nodeInfo.toNodeInfo() - addNode(node) - _loadDBSuccess = true // This is used in AbstractNode to indicate that node is ready. - } catch (e: Exception) { - logger.warn("Exception parsing network map from the database.", e) - } - } - if (loadDBSuccess) { - _registrationFuture.set(null) // Useful only if we don't have NetworkMapService configured so StateMachineManager can start. + val result = getAllInfos(session) + for (nodeInfo in result) { + try { + logger.info("Loaded node info: $nodeInfo") + val node = nodeInfo.toNodeInfo() + addNode(node) + _loadDBSuccess = true // This is used in AbstractNode to indicate that node is ready. + } catch (e: Exception) { + logger.warn("Exception parsing network map from the database.", e) } } + if (loadDBSuccess) { + _registrationFuture.set(null) // Useful only if we don't have NetworkMapService configured so StateMachineManager can start. + } } private fun updateInfoDB(nodeInfo: NodeInfo) { @@ -313,11 +303,9 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) } } - private fun removeInfoDB(nodeInfo: NodeInfo) { - createSession { - val info = findByIdentityKey(it, nodeInfo.legalIdentitiesAndCerts.first().owningKey).single() - it.remove(info) - } + private fun removeInfoDB(session: Session, nodeInfo: NodeInfo) { + val info = findByIdentityKey(session, nodeInfo.legalIdentitiesAndCerts.first().owningKey).single() + session.remove(info) } private fun findByIdentityKey(session: Session, identityKey: PublicKey): List { @@ -328,48 +316,40 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) return query.resultList } - private fun queryByIdentityKey(identityKey: PublicKey): List { - createSession { - val result = findByIdentityKey(it, identityKey) - return result.map { it.toNodeInfo() } - } + private fun queryByIdentityKey(session: Session, identityKey: PublicKey): List { + val result = findByIdentityKey(session, identityKey) + return result.map { it.toNodeInfo() } } - private fun queryIdentityByLegalName(name: CordaX500Name): PartyAndCertificate? { - createSession { - val query = it.createQuery( - // We do the JOIN here to restrict results to those present in the network map - "SELECT DISTINCT l FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.name = :name", - NodeInfoSchemaV1.DBPartyAndCertificate::class.java) - query.setParameter("name", name.toString()) - val candidates = query.resultList.map { it.toLegalIdentityAndCert() } - // The map is restricted to holding a single identity for any X.500 name, so firstOrNull() is correct here. - return candidates.firstOrNull() - } + private fun queryIdentityByLegalName(session: Session, name: CordaX500Name): PartyAndCertificate? { + val query = session.createQuery( + // We do the JOIN here to restrict results to those present in the network map + "SELECT DISTINCT l FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.name = :name", + NodeInfoSchemaV1.DBPartyAndCertificate::class.java) + query.setParameter("name", name.toString()) + val candidates = query.resultList.map { it.toLegalIdentityAndCert() } + // The map is restricted to holding a single identity for any X.500 name, so firstOrNull() is correct here. + return candidates.firstOrNull() } - private fun queryByLegalName(name: CordaX500Name): List { - createSession { - val query = it.createQuery( - "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.name = :name", - NodeInfoSchemaV1.PersistentNodeInfo::class.java) - query.setParameter("name", name.toString()) - val result = query.resultList - return result.map { it.toNodeInfo() } - } + private fun queryByLegalName(session: Session, name: CordaX500Name): List { + val query = session.createQuery( + "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.name = :name", + NodeInfoSchemaV1.PersistentNodeInfo::class.java) + query.setParameter("name", name.toString()) + val result = query.resultList + return result.map { it.toNodeInfo() } } - private fun queryByAddress(hostAndPort: NetworkHostAndPort): NodeInfo? { - createSession { - val query = it.createQuery( - "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.addresses a WHERE a.pk.host = :host AND a.pk.port = :port", - NodeInfoSchemaV1.PersistentNodeInfo::class.java) - query.setParameter("host", hostAndPort.host) - query.setParameter("port", hostAndPort.port) - val result = query.resultList - return if (result.isEmpty()) null - else result.map { it.toNodeInfo() }.singleOrNull() ?: throw IllegalStateException("More than one node with the same host and port") - } + private fun queryByAddress(session: Session, hostAndPort: NetworkHostAndPort): NodeInfo? { + val query = session.createQuery( + "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.addresses a WHERE a.pk.host = :host AND a.pk.port = :port", + NodeInfoSchemaV1.PersistentNodeInfo::class.java) + query.setParameter("host", hostAndPort.host) + query.setParameter("port", hostAndPort.port) + val result = query.resultList + return if (result.isEmpty()) null + else result.map { it.toNodeInfo() }.singleOrNull() ?: throw IllegalStateException("More than one node with the same host and port") } /** Object Relational Mapping support. */ @@ -388,10 +368,8 @@ open class PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) override fun clearNetworkMapCache() { serviceHub.database.transaction { - createSession { - val result = getAllInfos(it) - for (nodeInfo in result) it.remove(nodeInfo) - } + val result = getAllInfos(session) + for (nodeInfo in result) session.remove(nodeInfo) } } } 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 a82642adf7..b9b5f0bbdc 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 @@ -3,8 +3,8 @@ package net.corda.node.services.persistence import net.corda.core.serialization.SerializedBytes import net.corda.node.services.api.Checkpoint import net.corda.node.services.api.CheckpointStorage -import net.corda.node.utilities.DatabaseTransactionManager import net.corda.node.utilities.NODE_DATABASE_PREFIX +import net.corda.node.utilities.currentDBSession import javax.persistence.Column import javax.persistence.Entity import javax.persistence.Id @@ -28,15 +28,14 @@ class DBCheckpointStorage : CheckpointStorage { ) override fun addCheckpoint(checkpoint: Checkpoint) { - val session = DatabaseTransactionManager.current().session - session.save(DBCheckpoint().apply { + currentDBSession().save(DBCheckpoint().apply { checkpointId = checkpoint.id.toString() this.checkpoint = checkpoint.serializedFiber.bytes }) } override fun removeCheckpoint(checkpoint: Checkpoint) { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder val delete = criteriaBuilder.createCriteriaDelete(DBCheckpoint::class.java) val root = delete.from(DBCheckpoint::class.java) @@ -45,7 +44,7 @@ class DBCheckpointStorage : CheckpointStorage { } override fun forEach(block: (Checkpoint) -> Boolean) { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaQuery = session.criteriaBuilder.createQuery(DBCheckpoint::class.java) val root = criteriaQuery.from(DBCheckpoint::class.java) criteriaQuery.select(root) 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 c864521d7b..c95fe2bf94 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 @@ -13,8 +13,8 @@ import net.corda.core.crypto.SecureHash import net.corda.core.node.services.AttachmentStorage import net.corda.core.serialization.* import net.corda.core.utilities.loggerFor -import net.corda.node.utilities.DatabaseTransactionManager import net.corda.node.utilities.NODE_DATABASE_PREFIX +import net.corda.node.utilities.currentDBSession import java.io.* import java.nio.file.Paths import java.util.jar.JarInputStream @@ -50,7 +50,7 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single private val attachmentCount = metrics.counter("Attachments") init { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder val criteriaQuery = criteriaBuilder.createQuery(Long::class.java) criteriaQuery.select(criteriaBuilder.count(criteriaQuery.from(NodeAttachmentService.DBAttachment::class.java))) @@ -140,7 +140,7 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single } override fun openAttachment(id: SecureHash): Attachment? { - val attachment = DatabaseTransactionManager.current().session.get(NodeAttachmentService.DBAttachment::class.java, id.toString()) + val attachment = currentDBSession().get(NodeAttachmentService.DBAttachment::class.java, id.toString()) attachment?.let { return AttachmentImpl(id, { attachment.content }, checkAttachmentsOnLoad) } @@ -161,7 +161,7 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single checkIsAValidJAR(ByteArrayInputStream(bytes)) val id = SecureHash.SHA256(hs.hash().asBytes()) - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder val criteriaQuery = criteriaBuilder.createQuery(Long::class.java) val attachments = criteriaQuery.from(NodeAttachmentService.DBAttachment::class.java) 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 3b5236d97a..864da2bd50 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 @@ -29,6 +29,7 @@ import net.corda.node.services.persistence.HibernateConfiguration import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.utilities.DatabaseTransactionManager import net.corda.node.utilities.bufferUntilDatabaseCommit +import net.corda.node.utilities.currentDBSession import net.corda.node.utilities.wrapWithDatabaseTransaction import org.hibernate.Session import rx.Observable @@ -38,6 +39,15 @@ import java.time.Clock import java.time.Instant import java.util.* import javax.persistence.Tuple +import javax.persistence.criteria.CriteriaBuilder +import javax.persistence.criteria.CriteriaUpdate +import javax.persistence.criteria.Predicate +import javax.persistence.criteria.Root + +private fun CriteriaBuilder.executeUpdate(session: Session, configure: Root<*>.(CriteriaUpdate<*>) -> Any?) = createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java).let { update -> + update.from(VaultSchemaV1.VaultStates::class.java).run { configure(update) } + session.createQuery(update).executeUpdate() +} /** * Currently, the node vault service is a very simple RDBMS backed implementation. It will change significantly when @@ -73,7 +83,7 @@ class NodeVaultService(private val clock: Clock, private val keyManagementServic val consumedStateRefs = update.consumed.map { it.ref } log.trace { "Removing $consumedStateRefs consumed contract states and adding $producedStateRefs produced contract states to the database." } - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() producedStateRefsMap.forEach { stateAndRef -> val state = VaultSchemaV1.VaultStates( notary = stateAndRef.value.state.notary, @@ -189,7 +199,7 @@ class NodeVaultService(private val clock: Clock, private val keyManagementServic private fun loadStates(refs: Collection): HashSet> { val states = HashSet>() if (refs.isNotEmpty()) { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) @@ -223,11 +233,11 @@ class NodeVaultService(private val clock: Clock, private val keyManagementServic override fun addNoteToTransaction(txnId: SecureHash, noteText: String) { val txnNoteEntity = VaultSchemaV1.VaultTxnNote(txnId.toString(), noteText) - DatabaseTransactionManager.current().session.save(txnNoteEntity) + currentDBSession().save(txnNoteEntity) } override fun getTransactionNotes(txnId: SecureHash): Iterable { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultTxnNote::class.java) val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultTxnNote::class.java) @@ -241,35 +251,34 @@ class NodeVaultService(private val clock: Clock, private val keyManagementServic override fun softLockReserve(lockId: UUID, stateRefs: NonEmptySet) { val softLockTimestamp = clock.instant() try { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder - val criteriaUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java) - val vaultStates = criteriaUpdate.from(VaultSchemaV1.VaultStates::class.java) - val stateStatusPredication = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) - val lockIdPredicate = criteriaBuilder.or(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name).isNull, - criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString())) - val persistentStateRefs = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) } - val compositeKey = vaultStates.get(VaultSchemaV1.VaultStates::stateRef.name) - val stateRefsPredicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs)) - criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) - criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) - criteriaUpdate.where(stateStatusPredication, lockIdPredicate, stateRefsPredicate) - val updatedRows = session.createQuery(criteriaUpdate).executeUpdate() + fun execute(configure: Root<*>.(CriteriaUpdate<*>, Array) -> Any?) = criteriaBuilder.executeUpdate(session) { update -> + val persistentStateRefs = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) } + val compositeKey = get(VaultSchemaV1.VaultStates::stateRef.name) + val stateRefsPredicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs)) + configure(update, arrayOf(stateRefsPredicate)) + } + + val updatedRows = execute { update, commonPredicates -> + val stateStatusPredication = criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) + val lockIdPredicate = criteriaBuilder.or(get(VaultSchemaV1.VaultStates::lockId.name).isNull, + criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString())) + update.set(get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) + update.set(get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) + update.where(stateStatusPredication, lockIdPredicate, *commonPredicates) + } if (updatedRows > 0 && updatedRows == stateRefs.size) { log.trace("Reserving soft lock states for $lockId: $stateRefs") FlowStateMachineImpl.currentStateMachine()?.hasSoftLockedStates = true } else { // revert partial soft locks - val criteriaRevertUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java) - val vaultStatesRevert = criteriaRevertUpdate.from(VaultSchemaV1.VaultStates::class.java) - val lockIdPredicateRevert = criteriaBuilder.equal(vaultStatesRevert.get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) - val lockUpdateTime = criteriaBuilder.equal(vaultStatesRevert.get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) - val persistentStateRefsRevert = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) } - val compositeKeyRevert = vaultStatesRevert.get(VaultSchemaV1.VaultStates::stateRef.name) - val stateRefsPredicateRevert = criteriaBuilder.and(compositeKeyRevert.`in`(persistentStateRefsRevert)) - criteriaRevertUpdate.set(vaultStatesRevert.get(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java)) - criteriaRevertUpdate.where(lockUpdateTime, lockIdPredicateRevert, stateRefsPredicateRevert) - val revertUpdatedRows = session.createQuery(criteriaRevertUpdate).executeUpdate() + val revertUpdatedRows = execute { update, commonPredicates -> + val lockIdPredicate = criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) + val lockUpdateTime = criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) + update.set(get(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java)) + update.where(lockUpdateTime, lockIdPredicate, *commonPredicates) + } if (revertUpdatedRows > 0) { log.trace("Reverting $revertUpdatedRows partially soft locked states for $lockId") } @@ -286,33 +295,30 @@ class NodeVaultService(private val clock: Clock, private val keyManagementServic override fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet?) { val softLockTimestamp = clock.instant() - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder + fun execute(configure: Root<*>.(CriteriaUpdate<*>, Array) -> Any?) = criteriaBuilder.executeUpdate(session) { update -> + val stateStatusPredication = criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) + val lockIdPredicate = criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) + update.set(get(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java)) + update.set(get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) + configure(update, arrayOf(stateStatusPredication, lockIdPredicate)) + } if (stateRefs == null) { - val criteriaUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java) - val vaultStates = criteriaUpdate.from(VaultSchemaV1.VaultStates::class.java) - val stateStatusPredication = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) - val lockIdPredicate = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) - criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java)) - criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) - criteriaUpdate.where(stateStatusPredication, lockIdPredicate) - val update = session.createQuery(criteriaUpdate).executeUpdate() + val update = execute { update, commonPredicates -> + update.where(*commonPredicates) + } if (update > 0) { log.trace("Releasing $update soft locked states for $lockId") } } else { try { - val criteriaUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java) - val vaultStates = criteriaUpdate.from(VaultSchemaV1.VaultStates::class.java) - val stateStatusPredication = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) - val lockIdPredicate = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) - val persistentStateRefs = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) } - val compositeKey = vaultStates.get(VaultSchemaV1.VaultStates::stateRef.name) - val stateRefsPredicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs)) - criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java)) - criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) - criteriaUpdate.where(stateStatusPredication, lockIdPredicate, stateRefsPredicate) - val updatedRows = session.createQuery(criteriaUpdate).executeUpdate() + val updatedRows = execute { update, commonPredicates -> + val persistentStateRefs = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) } + val compositeKey = get(VaultSchemaV1.VaultStates::stateRef.name) + val stateRefsPredicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs)) + update.where(*commonPredicates, stateRefsPredicate) + } if (updatedRows > 0) { log.trace("Releasing $updatedRows soft locked states for $lockId and stateRefs $stateRefs") } 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 62508f0e2b..a90f77d171 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt @@ -40,10 +40,11 @@ class AppendOnlyPersistentMap( * Returns all key/value pairs from the underlying storage. */ fun allPersisted(): Sequence> { - val criteriaQuery = DatabaseTransactionManager.current().session.criteriaBuilder.createQuery(persistentEntityClass) + val session = currentDBSession() + val criteriaQuery = session.criteriaBuilder.createQuery(persistentEntityClass) val root = criteriaQuery.from(persistentEntityClass) criteriaQuery.select(root) - val query = DatabaseTransactionManager.current().session.createQuery(criteriaQuery) + val query = session.createQuery(criteriaQuery) val result = query.resultList return result.map { x -> fromPersistentEntity(x) }.asSequence() } @@ -87,7 +88,7 @@ class AppendOnlyPersistentMap( */ operator fun set(key: K, value: V) = set(key, value, logWarning = false) { k, v -> - DatabaseTransactionManager.current().session.save(toPersistentEntity(k, v)) + currentDBSession().save(toPersistentEntity(k, v)) null } @@ -98,9 +99,10 @@ class AppendOnlyPersistentMap( */ fun addWithDuplicatesAllowed(key: K, value: V, logWarning: Boolean = true): Boolean = set(key, value, logWarning) { k, v -> - val existingEntry = DatabaseTransactionManager.current().session.find(persistentEntityClass, toPersistentEntityKey(k)) + val session = currentDBSession() + val existingEntry = session.find(persistentEntityClass, toPersistentEntityKey(k)) if (existingEntry == null) { - DatabaseTransactionManager.current().session.save(toPersistentEntity(k, v)) + session.save(toPersistentEntity(k, v)) null } else { fromPersistentEntity(existingEntry).second @@ -114,7 +116,7 @@ class AppendOnlyPersistentMap( } private fun loadValue(key: K): V? { - val result = DatabaseTransactionManager.current().session.find(persistentEntityClass, toPersistentEntityKey(key)) + val result = currentDBSession().find(persistentEntityClass, toPersistentEntityKey(key)) return result?.let(fromPersistentEntity)?.second } @@ -125,7 +127,7 @@ class AppendOnlyPersistentMap( * WARNING!! The method is not thread safe. */ fun clear() { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val deleteQuery = session.criteriaBuilder.createCriteriaDelete(persistentEntityClass) deleteQuery.from(persistentEntityClass) session.createQuery(deleteQuery).executeUpdate() diff --git a/node/src/main/kotlin/net/corda/node/utilities/DatabaseTransactionManager.kt b/node/src/main/kotlin/net/corda/node/utilities/DatabaseTransactionManager.kt index 9016112f7b..6c810a7005 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/DatabaseTransactionManager.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/DatabaseTransactionManager.kt @@ -62,6 +62,7 @@ class DatabaseTransaction(isolation: Int, val threadLocal: ThreadLocal() diff --git a/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt b/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt index c24ce3c229..11ace024ef 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt @@ -28,7 +28,7 @@ class PersistentMap( removalListener = ExplicitRemoval(toPersistentEntityKey, persistentEntityClass) ).apply { //preload to allow all() to take data only from the cache (cache is unbound) - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaQuery = session.criteriaBuilder.createQuery(persistentEntityClass) criteriaQuery.select(criteriaQuery.from(persistentEntityClass)) getAll(session.createQuery(criteriaQuery).resultList.map { e -> fromPersistentEntity(e as E).first }.asIterable()) @@ -38,7 +38,7 @@ class PersistentMap( override fun onRemoval(notification: RemovalNotification?) { when (notification?.cause) { RemovalCause.EXPLICIT -> { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val elem = session.find(persistentEntityClass, toPersistentEntityKey(notification.key)) if (elem != null) { session.remove(elem) @@ -101,7 +101,7 @@ class PersistentMap( set(key, value, logWarning = false, store = { k: K, v: V -> - DatabaseTransactionManager.current().session.save(toPersistentEntity(k, v)) + currentDBSession().save(toPersistentEntity(k, v)) null }, replace = { _: K, _: V -> Unit } @@ -115,9 +115,10 @@ class PersistentMap( fun addWithDuplicatesAllowed(key: K, value: V) = set(key, value, store = { k, v -> - val existingEntry = DatabaseTransactionManager.current().session.find(persistentEntityClass, toPersistentEntityKey(k)) + val session = currentDBSession() + val existingEntry = session.find(persistentEntityClass, toPersistentEntityKey(k)) if (existingEntry == null) { - DatabaseTransactionManager.current().session.save(toPersistentEntity(k, v)) + session.save(toPersistentEntity(k, v)) null } else { fromPersistentEntity(existingEntry).second @@ -145,18 +146,19 @@ class PersistentMap( } private fun merge(key: K, value: V): V? { - val existingEntry = DatabaseTransactionManager.current().session.find(persistentEntityClass, toPersistentEntityKey(key)) + val session = currentDBSession() + val existingEntry = session.find(persistentEntityClass, toPersistentEntityKey(key)) return if (existingEntry != null) { - DatabaseTransactionManager.current().session.merge(toPersistentEntity(key, value)) + session.merge(toPersistentEntity(key, value)) fromPersistentEntity(existingEntry).second } else { - DatabaseTransactionManager.current().session.save(toPersistentEntity(key, value)) + session.save(toPersistentEntity(key, value)) null } } private fun loadValue(key: K): V? { - val result = DatabaseTransactionManager.current().session.find(persistentEntityClass, toPersistentEntityKey(key)) + val result = currentDBSession().find(persistentEntityClass, toPersistentEntityKey(key)) return result?.let(fromPersistentEntity)?.second } @@ -256,7 +258,7 @@ class PersistentMap( } fun load() { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaQuery = session.criteriaBuilder.createQuery(persistentEntityClass) criteriaQuery.select(criteriaQuery.from(persistentEntityClass)) cache.getAll(session.createQuery(criteriaQuery).resultList.map { e -> fromPersistentEntity(e as E).first }.asIterable()) diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt index 46e954ecde..d3ee944069 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt @@ -11,7 +11,6 @@ import net.corda.core.internal.write import net.corda.core.internal.writeLines import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.utilities.CordaPersistence -import net.corda.node.utilities.DatabaseTransactionManager import net.corda.node.utilities.configureDatabase import net.corda.testing.LogHelper import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties @@ -98,19 +97,18 @@ class NodeAttachmentStorageTest { @Test fun `corrupt entry throws exception`() { val testJar = makeTestJar() - val id = - database.transaction { - val storage = NodeAttachmentService(MetricRegistry()) - val id = testJar.read { storage.importAttachment(it) } + val id = database.transaction { + val storage = NodeAttachmentService(MetricRegistry()) + val id = testJar.read { storage.importAttachment(it) } - // Corrupt the file in the store. - val bytes = testJar.readAll() - val corruptBytes = "arggghhhh".toByteArray() - System.arraycopy(corruptBytes, 0, bytes, 0, corruptBytes.size) - val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = bytes) - DatabaseTransactionManager.current().session.merge(corruptAttachment) - id - } + // Corrupt the file in the store. + val bytes = testJar.readAll() + val corruptBytes = "arggghhhh".toByteArray() + System.arraycopy(corruptBytes, 0, bytes, 0, corruptBytes.size) + val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = bytes) + session.merge(corruptAttachment) + id + } database.transaction { val storage = NodeAttachmentService(MetricRegistry()) val e = assertFailsWith { From 86ede6f92852ab841f80a57081c2dc58d3f3ab15 Mon Sep 17 00:00:00 2001 From: Clinton Date: Fri, 13 Oct 2017 12:21:48 +0100 Subject: [PATCH 169/185] Upgraded to 2.0-SNAPSHOT since current changes are for 2.0 (#1880) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ca5904ba95..70abd216ca 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { file("$projectDir/constants.properties").withInputStream { constants.load(it) } // Our version: bump this on release. - ext.corda_release_version = "1.1-SNAPSHOT" + ext.corda_release_version = "2.0-SNAPSHOT" // Increment this on any release that changes public APIs anywhere in the Corda platform // TODO This is going to be difficult until we have a clear separation throughout the code of what is public and what is internal ext.corda_platform_version = 1 From 635ad9ac9217853a61879c1d8b628a29ab0bf0af Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Fri, 13 Oct 2017 12:27:42 +0100 Subject: [PATCH 170/185] Ensure that the contents of OpaqueBytes cannot be modified. (#1871) * Ensure that contents of OpaqueBytes cannot be modified. * Update documentation and restore the signature verification check. * Update the API definition. * KDoc fixes. * Update the changelog for v1.1. --- .ci/api-current.txt | 2 +- .../net/corda/core/crypto/SecureHash.kt | 56 +++++++++++++++++-- .../transactions/TransactionWithSignatures.kt | 2 - .../net/corda/core/utilities/ByteArrays.kt | 31 +++++++++- .../TransactionSerializationTests.kt | 8 ++- docs/source/changelog.rst | 3 + 6 files changed, 90 insertions(+), 12 deletions(-) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 6892aeae53..6e8e8df371 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -2923,7 +2923,7 @@ public static final class net.corda.core.utilities.NonEmptySet$iterator$1 extend ## public class net.corda.core.utilities.OpaqueBytes extends net.corda.core.utilities.ByteSequence public (byte[]) - @org.jetbrains.annotations.NotNull public byte[] getBytes() + @org.jetbrains.annotations.NotNull public final byte[] getBytes() public int getOffset() public int getSize() public static final net.corda.core.utilities.OpaqueBytes$Companion Companion diff --git a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt index 16bf533550..6555ac6af7 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt @@ -19,39 +19,87 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) { } } + /** + * Convert the hash value to an uppercase hexadecimal [String]. + */ override fun toString(): String = bytes.toHexString() + /** + * Returns the first [prefixLen] hexadecimal digits of the [SecureHash] value. + * @param prefixLen The number of characters in the prefix. + */ fun prefixChars(prefixLen: Int = 6) = toString().substring(0, prefixLen) + + /** + * Append a second hash value to this hash value, and then compute the SHA-256 hash of the result. + * @param other The hash to append to this one. + */ fun hashConcat(other: SecureHash) = (this.bytes + other.bytes).sha256() // Like static methods in Java, except the 'companion' is a singleton that can have state. companion object { + /** + * Converts a SHA-256 hash value represented as a hexadecimal [String] into a [SecureHash]. + * @param str A sequence of 64 hexadecimal digits that represents a SHA-256 hash value. + * @throws IllegalArgumentException The input string does not contain 64 hexadecimal digits, or it contains incorrectly-encoded characters. + */ @JvmStatic - fun parse(str: String) = str.toUpperCase().parseAsHex().let { - when (it.size) { - 32 -> SHA256(it) - else -> throw IllegalArgumentException("Provided string is ${it.size} bytes not 32 bytes in hex: $str") + fun parse(str: String): SHA256 { + return str.toUpperCase().parseAsHex().let { + when (it.size) { + 32 -> SHA256(it) + else -> throw IllegalArgumentException("Provided string is ${it.size} bytes not 32 bytes in hex: $str") + } } } + /** + * Computes the SHA-256 hash value of the [ByteArray]. + * @param bytes The [ByteArray] to hash. + */ @JvmStatic fun sha256(bytes: ByteArray) = SHA256(MessageDigest.getInstance("SHA-256").digest(bytes)) + /** + * Computes the SHA-256 hash of the [ByteArray], and then computes the SHA-256 hash of the hash. + * @param bytes The [ByteArray] to hash. + */ @JvmStatic fun sha256Twice(bytes: ByteArray) = sha256(sha256(bytes).bytes) + /** + * Computes the SHA-256 hash of the [String]'s UTF-8 byte contents. + * @param str [String] whose UTF-8 contents will be hashed. + */ @JvmStatic fun sha256(str: String) = sha256(str.toByteArray()) + /** + * Generates a random SHA-256 value. + */ @JvmStatic fun randomSHA256() = sha256(newSecureRandom().generateSeed(32)) + /** + * A SHA-256 hash value consisting of 32 0x00 bytes. + */ val zeroHash = SecureHash.SHA256(ByteArray(32, { 0.toByte() })) + + /** + * A SHA-256 hash value consisting of 32 0xFF bytes. + */ val allOnesHash = SecureHash.SHA256(ByteArray(32, { 255.toByte() })) } // In future, maybe SHA3, truncated hashes etc. } +/** + * Compute the SHA-256 hash for the contents of the [ByteArray]. + */ fun ByteArray.sha256(): SecureHash.SHA256 = SecureHash.sha256(this) + +/** + * Compute the SHA-256 hash for the contents of the [OpaqueBytes]. + */ fun OpaqueBytes.sha256(): SecureHash.SHA256 = SecureHash.sha256(this.bytes) diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt index 1909168198..ab25d68064 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt @@ -1,8 +1,6 @@ package net.corda.core.transactions -import net.corda.core.contracts.ContractState import net.corda.core.contracts.NamedByHash -import net.corda.core.contracts.TransactionState import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.isFulfilledBy import net.corda.core.transactions.SignedTransaction.SignaturesMissingException 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 a2b74a11ff..cf8b1f915d 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt @@ -118,8 +118,11 @@ sealed class ByteSequence : Comparable { * In an ideal JVM this would be a value type and be completely overhead free. Project Valhalla is adding such * functionality to Java, but it won't arrive for a few years yet! */ -open class OpaqueBytes(override val bytes: ByteArray) : ByteSequence() { +open class OpaqueBytes(bytes: ByteArray) : ByteSequence() { companion object { + /** + * Create [OpaqueBytes] from a sequence of [Byte] values. + */ @JvmStatic fun of(vararg b: Byte) = OpaqueBytes(byteArrayOf(*b)) } @@ -128,13 +131,35 @@ open class OpaqueBytes(override val bytes: ByteArray) : ByteSequence() { require(bytes.isNotEmpty()) } - override val size: Int get() = bytes.size - override val offset: Int get() = 0 + /** + * The bytes are always cloned so that this object becomes immutable. This has been done + * to prevent tampering with entities such as [SecureHash] and [PrivacySalt], as well as + * preserve the integrity of our hash constants [zeroHash] and [allOnesHash]. + * + * Cloning like this may become a performance issue, depending on whether or not the JIT + * compiler is ever able to optimise away the clone. In which case we may need to revisit + * this later. + */ + override final val bytes: ByteArray = bytes + get() = field.clone() + override val size: Int = bytes.size + override val offset: Int = 0 } +/** + * Copy [size] bytes from this [ByteArray] starting from [offset] into a new [ByteArray]. + */ fun ByteArray.sequence(offset: Int = 0, size: Int = this.size) = ByteSequence.of(this, offset, size) +/** + * Converts this [ByteArray] into a [String] of hexadecimal digits. + */ fun ByteArray.toHexString(): String = DatatypeConverter.printHexBinary(this) + +/** + * Converts this [String] of hexadecimal digits into a [ByteArray]. + * @throws IllegalArgumentException if the [String] contains incorrectly-encoded characters. + */ fun String.parseAsHex(): ByteArray = DatatypeConverter.parseHexBinary(this) /** diff --git a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt index 88ef89b662..6a3324e6dd 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt @@ -12,11 +12,12 @@ import org.junit.Before import org.junit.Test import java.security.SignatureException import java.util.* +import kotlin.reflect.jvm.javaField import kotlin.test.assertEquals import kotlin.test.assertFailsWith class TransactionSerializationTests : TestDependencyInjectionBase() { - val TEST_CASH_PROGRAM_ID = "net.corda.core.serialization.TransactionSerializationTests\$TestCash" + private val TEST_CASH_PROGRAM_ID = "net.corda.core.serialization.TransactionSerializationTests\$TestCash" class TestCash : Contract { override fun verify(tx: LedgerTransaction) { @@ -65,7 +66,10 @@ class TransactionSerializationTests : TestDependencyInjectionBase() { stx.verifyRequiredSignatures() // Corrupt the data and ensure the signature catches the problem. - stx.id.bytes[5] = stx.id.bytes[5].inc() + val bytesField = stx.id::bytes.javaField?.apply { setAccessible(true) } + val bytes = bytesField?.get(stx.id) as ByteArray + bytes[5] = bytes[5].inc() + assertFailsWith(SignatureException::class) { stx.verifyRequiredSignatures() } diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index ddc74beb2a..0ff4e44495 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,6 +6,9 @@ from the previous milestone release. UNRELEASED ---------- +* ``OpaqueBytes.bytes`` now returns a clone of its underlying ``ByteArray``, and has been redeclared as ``final``. + This is a minor change to the public API, but is required to ensure that classes like ``SecureHash`` are immutable. + * ``FlowLogic`` now exposes a series of function called ``receiveAll(...)`` allowing to join ``receive(...)`` instructions. * The ``Cordformation`` gradle plugin has been split into ``cordformation`` and ``cordapp``. The former builds and From b8538d5eb786debffc67b44fc9241684b01e8705 Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Fri, 13 Oct 2017 14:43:33 +0100 Subject: [PATCH 171/185] Fix file resource for non-unix filesystems. --- .../net/corda/signing/configuration/ConfigurationTest.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/signing-server/src/test/kotlin/net/corda/signing/configuration/ConfigurationTest.kt b/signing-server/src/test/kotlin/net/corda/signing/configuration/ConfigurationTest.kt index 57b9ee613b..514715037e 100644 --- a/signing-server/src/test/kotlin/net/corda/signing/configuration/ConfigurationTest.kt +++ b/signing-server/src/test/kotlin/net/corda/signing/configuration/ConfigurationTest.kt @@ -3,12 +3,13 @@ package net.corda.signing.configuration import com.typesafe.config.ConfigException import net.corda.signing.authentication.AuthMode import org.junit.Test +import java.io.File import kotlin.test.assertEquals import kotlin.test.assertFailsWith class ConfigurationTest { - private val validConfigPath = javaClass.getResource("/signing_service.conf").path - private val invalidConfigPath = javaClass.getResource("/signing_service_fail.conf").path + private val validConfigPath = File(javaClass.getResource("/signing_service.conf").path).absolutePath + private val invalidConfigPath = File(javaClass.getResource("/signing_service_fail.conf").path).absolutePath @Test fun `authMode is parsed correctly`() { From 26803616964b0ed9651957748685d6bd0f1cf7ef Mon Sep 17 00:00:00 2001 From: Clinton Date: Fri, 13 Oct 2017 15:01:24 +0100 Subject: [PATCH 172/185] Renamed "plugins" dir to "cordapps" (#1644) * Renamed plugins dir to cordapps dir while maintaining backwards compatibility with the plugins dir. Bumped gradle plugins to 2.0.4 --- .../rpc/StandaloneCordaRPCJavaClientTest.java | 6 +- .../kotlin/rpc/StandaloneCordaRPClientTest.kt | 4 +- constants.properties | 2 +- docs/source/azure-vm.rst | 6 +- docs/source/changelog.rst | 2 + docs/source/cordapp-build-systems.rst | 6 +- docs/source/demobench.rst | 8 +-- docs/source/deploying-a-node.rst | 2 +- docs/source/hello-world-introduction.rst | 2 +- docs/source/hello-world-running.rst | 2 +- .../key-concepts-contract-constraints.rst | 2 +- docs/source/key-concepts-node.rst | 10 +-- docs/source/node-services.rst | 2 +- docs/source/running-a-node.rst | 2 +- docs/source/tutorial-cordapp.rst | 4 +- .../main/groovy/net/corda/plugins/Node.groovy | 16 ++--- .../node/services/AttachmentLoadingTests.kt | 2 +- node/src/main/java/CordaCaplet.java | 23 +++---- .../node/internal/cordapp/CordappLoader.kt | 24 +++++--- .../kotlin/net/corda/node/CordappSmokeTest.kt | 9 +-- tools/demobench/build.gradle | 6 +- .../net/corda/demobench/explorer/Explorer.kt | 9 +-- .../model/{HasPlugins.kt => HasCordapps.kt} | 4 +- .../corda/demobench/model/InstallFactory.kt | 4 +- .../net/corda/demobench/model/NodeConfig.kt | 9 +-- .../corda/demobench/model/NodeController.kt | 12 ++-- .../demobench/plugin/CordappController.kt | 61 +++++++++++++++++++ .../demobench/plugin/PluginController.kt | 58 ------------------ .../demobench/profile/ProfileController.kt | 37 ++++++----- .../corda/demobench/model/NodeConfigTest.kt | 1 + .../src/main/java/ExplorerCaplet.java | 2 +- 31 files changed, 176 insertions(+), 161 deletions(-) rename tools/demobench/src/main/kotlin/net/corda/demobench/model/{HasPlugins.kt => HasCordapps.kt} (56%) create mode 100644 tools/demobench/src/main/kotlin/net/corda/demobench/plugin/CordappController.kt delete mode 100644 tools/demobench/src/main/kotlin/net/corda/demobench/plugin/PluginController.kt diff --git a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java index 70ae26e1b6..31daccc9c2 100644 --- a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java +++ b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java @@ -74,9 +74,9 @@ public class StandaloneCordaRPCJavaClientTest { } private void copyFinanceCordapp() { - Path pluginsDir = (factory.baseDirectory(notaryConfig).resolve("plugins")); + Path cordappsDir = (factory.baseDirectory(notaryConfig).resolve("cordapps")); try { - Files.createDirectories(pluginsDir); + Files.createDirectories(cordappsDir); } catch (IOException ex) { fail("Failed to create directories"); } @@ -84,7 +84,7 @@ public class StandaloneCordaRPCJavaClientTest { paths.forEach(file -> { if (file.toString().contains("corda-finance")) { try { - Files.copy(file, pluginsDir.resolve(file.getFileName())); + Files.copy(file, cordappsDir.resolve(file.getFileName())); } catch (IOException ex) { fail("Failed to copy finance jar"); } diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt index 1d487f762b..d6d305f542 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt @@ -89,12 +89,12 @@ class StandaloneCordaRPClientTest { } private fun copyFinanceCordapp() { - val pluginsDir = (factory.baseDirectory(notaryConfig) / "plugins").createDirectories() + val cordappsDir = (factory.baseDirectory(notaryConfig) / "cordapps").createDirectories() // Find the finance jar file for the smoke tests of this module val financeJar = Paths.get("build", "resources", "smokeTest").list { it.filter { "corda-finance" in it.toString() }.toList().single() } - financeJar.copyToDirectory(pluginsDir) + financeJar.copyToDirectory(cordappsDir) } @Test diff --git a/constants.properties b/constants.properties index deebebf5d5..48f942803c 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=2.0.3 +gradlePluginsVersion=2.0.4 kotlinVersion=1.1.50 guavaVersion=21.0 bouncycastleVersion=1.57 diff --git a/docs/source/azure-vm.rst b/docs/source/azure-vm.rst index bc5ae24fa4..7f94f0fdcb 100644 --- a/docs/source/azure-vm.rst +++ b/docs/source/azure-vm.rst @@ -97,7 +97,7 @@ Loading the Yo! CordDapp on your Corda nodes lets you send simple Yo! messages t * **Loading the Yo! CorDapp onto your nodes** -The nodes you will use to send and receive Yo messages require the Yo! CorDapp jar file to be saved to their plugins directory. +The nodes you will use to send and receive Yo messages require the Yo! CorDapp jar file to be saved to their cordapps directory. Connect to one of your Corda nodes (make sure this is not the Notary node) using an SSH client of your choice (e.g. Putty) and log into the virtual machine using the public IP address and your SSH key or username / password combination you defined in Step 1 of the Azure build process. Type the following command: @@ -105,14 +105,14 @@ For Corda nodes running release M10 .. sourcecode:: shell - cd /opt/corda/plugins + cd /opt/corda/cordapps wget http://downloads.corda.net/cordapps/net/corda/yo/0.10.1/yo-0.10.1.jar For Corda nodes running release M11 .. sourcecode:: shell - cd /opt/corda/plugins + cd /opt/corda/cordapps wget http://downloads.corda.net/cordapps/net/corda/yo/0.11.0/yo-0.11.0.jar Now restart Corda and the Corda webserver using the following commands or restart your Corda VM from the Azure portal: diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 0ff4e44495..e3be980710 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -11,6 +11,8 @@ UNRELEASED * ``FlowLogic`` now exposes a series of function called ``receiveAll(...)`` allowing to join ``receive(...)`` instructions. +* Renamed "plugins" directory on nodes to "cordapps" + * The ``Cordformation`` gradle plugin has been split into ``cordformation`` and ``cordapp``. The former builds and deploys nodes for development and testing, the latter turns a project into a cordapp project that generates JARs in the standard CorDapp format. diff --git a/docs/source/cordapp-build-systems.rst b/docs/source/cordapp-build-systems.rst index 016d741bf2..e20065838f 100644 --- a/docs/source/cordapp-build-systems.rst +++ b/docs/source/cordapp-build-systems.rst @@ -78,11 +78,11 @@ For further information about managing dependencies, see Installing CorDapps ------------------- -At runtime, nodes will load any plugins present in their ``plugins`` folder. Therefore in order to install a cordapp to -a node the cordapp JAR must be added to the ``/plugins/`` folder, where ``node_dir`` is the folder in which the +At runtime, nodes will load any CorDapp JARs present in their ``cordapps`` folder. Therefore in order to install a CorDapp to +a node the CorDapp JAR must be added to the ``/cordapps/`` folder, where ``node_dir`` is the folder in which the node's JAR and configuration files are stored). -The ``deployNodes`` gradle task, if correctly configured, will automatically place your cordapp JAR as well as any +The ``deployNodes`` gradle task, if correctly configured, will automatically place your CorDapp JAR as well as any dependent cordapp JARs specified into the directory automatically. Example diff --git a/docs/source/demobench.rst b/docs/source/demobench.rst index 5382547301..374f12234a 100644 --- a/docs/source/demobench.rst +++ b/docs/source/demobench.rst @@ -37,13 +37,13 @@ Profiles notary/ node.conf - plugins/ + cordapps/ banka/ node.conf - plugins/ + cordapps/ bankb/ node.conf - plugins/ + cordapps/ example-cordapp.jar ... @@ -133,7 +133,7 @@ current working directory of the JVM): corda-webserver.jar explorer/ node-explorer.jar - plugins/ + cordapps/ bank-of-corda.jar .. diff --git a/docs/source/deploying-a-node.rst b/docs/source/deploying-a-node.rst index 4125947fd8..74f8bdac65 100644 --- a/docs/source/deploying-a-node.rst +++ b/docs/source/deploying-a-node.rst @@ -84,7 +84,7 @@ run all the nodes at once. Each node in the ``nodes`` folder has the following s . nodeName ├── corda.jar // The Corda runtime ├── node.conf // The node's configuration - ├── plugins // Any installed CorDapps + ├── cordapps // Any installed CorDapps └── additional-node-infos // Directory containing all the addresses and certificates of the other nodes. .. note:: During the build process each node generates a NodeInfo file which is written in its own root directory, diff --git a/docs/source/hello-world-introduction.rst b/docs/source/hello-world-introduction.rst index 9760d91a6d..25bb4e189d 100644 --- a/docs/source/hello-world-introduction.rst +++ b/docs/source/hello-world-introduction.rst @@ -5,7 +5,7 @@ By this point, :doc:`your dev environment should be set up `, yo :doc:`your first CorDapp `, and you're familiar with Corda's :doc:`key concepts `. What comes next? -If you're a developer, the next step is to write your own CorDapp. Each CorDapp takes the form of a plugin that is +If you're a developer, the next step is to write your own CorDapp. Each CorDapp takes the form of a JAR that is installed on one or more Corda nodes, and gives them the ability to conduct some new process - anything from issuing a debt instrument to making a restaurant booking. diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index b5bf916e18..72c573b789 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -82,7 +82,7 @@ the three node folders. Each node folder has the following structure: |____dependencies |____node.conf // The node's configuration file |____additional-node-infos/ // Directory containing all the other nodes' addresses and identities - |____plugins + |____cordapps |____java/kotlin-source-0.1.jar // Our IOU CorDapp Let's start the nodes by running the following commands from the root of the project: diff --git a/docs/source/key-concepts-contract-constraints.rst b/docs/source/key-concepts-contract-constraints.rst index 39840924fd..edd4c6694d 100644 --- a/docs/source/key-concepts-contract-constraints.rst +++ b/docs/source/key-concepts-contract-constraints.rst @@ -146,4 +146,4 @@ The driver takes a parameter called ``extraCordappPackagesToScan`` which is a li Full Nodes ********** -When testing against full nodes simply place your CorDapp into the plugins directory of the node. +When testing against full nodes simply place your CorDapp into the cordapps directory of the node. diff --git a/docs/source/key-concepts-node.rst b/docs/source/key-concepts-node.rst index 537d0aebc4..624f5308af 100644 --- a/docs/source/key-concepts-node.rst +++ b/docs/source/key-concepts-node.rst @@ -36,7 +36,7 @@ The core elements of the architecture are: * A network interface for interacting with other nodes * An RPC interface for interacting with the node's owner * A service hub for allowing the node's flows to call upon the node's other services -* A plugin registry for extending the node by installing CorDapps +* A cordapp interface and provider for extending the node by installing CorDapps Persistence layer ----------------- @@ -68,11 +68,11 @@ updates. The key services provided are: * Information about the node itself * The current time, as tracked by the node -The plugin registry -------------------- -The plugin registry is where new CorDapps are installed to extend the behavior of the node. +The CorDapp provider +-------------------- +The CorDapp provider is where new CorDapps are installed to extend the behavior of the node. -The node also has several plugins installed by default to handle common tasks such as: +The node also has several CorDapps installed by default to handle common tasks such as: * Retrieving transactions and attachments from counterparties * Upgrading contracts diff --git a/docs/source/node-services.rst b/docs/source/node-services.rst index 3ab92255b6..d62edd2548 100644 --- a/docs/source/node-services.rst +++ b/docs/source/node-services.rst @@ -320,7 +320,7 @@ does this by tracking update notifications from the ``TransactionStorage`` service and processing relevant updates to delete consumed states and insert new states. The resulting update is then persisted to the database. The ``VaultService`` then exposes query and -event notification APIs to flows and CorDapp plugins to allow them +event notification APIs to flows and CorDapp services to allow them to respond to updates, or query for states meeting various conditions to begin the formation of new transactions consuming them. The equivalent services are also forwarded to RPC clients, so that they may show diff --git a/docs/source/running-a-node.rst b/docs/source/running-a-node.rst index b87378bd29..4373e93a3e 100644 --- a/docs/source/running-a-node.rst +++ b/docs/source/running-a-node.rst @@ -10,7 +10,7 @@ already installed. You run each node by navigating to ```` in a termin java -jar corda.jar -.. warning:: If your working directory is not ```` your plugins and configuration will not be used. +.. warning:: If your working directory is not ```` your cordapps and configuration will not be used. The configuration file and workspace paths can be overridden on the command line. For example: diff --git a/docs/source/tutorial-cordapp.rst b/docs/source/tutorial-cordapp.rst index c61e20f4be..7073a67af3 100644 --- a/docs/source/tutorial-cordapp.rst +++ b/docs/source/tutorial-cordapp.rst @@ -210,9 +210,9 @@ Building the example CorDapp . nodeName ├── corda.jar ├── node.conf - └── plugins + └── cordapps - ``corda.jar`` is the Corda runtime, ``plugins`` contains our node's CorDapps, and the node's configuration is + ``corda.jar`` is the Corda runtime, ``cordapps`` contains our node's CorDapps, and the node's configuration is given by ``node.conf`` Running the example CorDapp diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy index f6963541d4..8f6dcea295 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy @@ -17,7 +17,7 @@ class Node extends CordformNode { static final String WEBJAR_NAME = 'corda-webserver.jar' /** - * Set the list of CorDapps to install to the plugins directory. Each cordapp is a fully qualified Maven + * Set the list of CorDapps to install to the cordapps directory. Each cordapp is a fully qualified Maven * dependency name, eg: com.example:product-name:0.1 * * @note Your app will be installed by default and does not need to be included here. @@ -104,7 +104,7 @@ class Node extends CordformNode { if (config.hasPath("webAddress")) { installWebserverJar() } - installBuiltPlugin() + installBuiltCordapp() installCordapps() installConfig() appendOptionalConfig() @@ -157,23 +157,23 @@ class Node extends CordformNode { /** * Installs this project's cordapp to this directory. */ - private void installBuiltPlugin() { - def pluginsDir = new File(nodeDir, "plugins") + private void installBuiltCordapp() { + def cordappsDir = new File(nodeDir, "cordapps") project.copy { from project.jar - into pluginsDir + into cordappsDir } } /** - * Installs other cordapps to this node's plugins directory. + * Installs other cordapps to this node's cordapps directory. */ private void installCordapps() { - def pluginsDir = new File(nodeDir, "plugins") + def cordappsDir = new File(nodeDir, "cordapps") def cordapps = getCordappList() project.copy { from cordapps - into pluginsDir + into cordappsDir } } 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 3c15d0aec5..3ea5527202 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 @@ -69,7 +69,7 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() { private fun DriverDSLExposedInterface.installIsolatedCordappTo(nodeName: CordaX500Name) { // Copy the app jar to the first node. The second won't have it. - val path = (baseDirectory(nodeName.toString()) / "plugins").createDirectories() / "isolated.jar" + val path = (baseDirectory(nodeName.toString()) / "cordapps").createDirectories() / "isolated.jar" logger.info("Installing isolated jar to $path") isolatedJAR.openStream().buffered().use { input -> Files.newOutputStream(path).buffered().use { output -> diff --git a/node/src/main/java/CordaCaplet.java b/node/src/main/java/CordaCaplet.java index 83e76ae2ba..fa39580fa7 100644 --- a/node/src/main/java/CordaCaplet.java +++ b/node/src/main/java/CordaCaplet.java @@ -24,26 +24,27 @@ public class CordaCaplet extends Capsule { // defined as public static final fields on the Capsule class, therefore referential equality is safe. if (ATTR_APP_CLASS_PATH == attr) { T cp = super.attribute(attr); - return (T) augmentClasspath((List) cp, "plugins"); + + (new File("cordapps")).mkdir(); + augmentClasspath((List) cp, "cordapps"); + augmentClasspath((List) cp, "plugins"); + return cp; } return super.attribute(attr); } // TODO: Make directory configurable via the capsule manifest. // TODO: Add working directory variable to capsules string replacement variables. - private List augmentClasspath(List classpath, String dirName) { + private void augmentClasspath(List classpath, String dirName) { File dir = new File(dirName); - if (!dir.exists()) { - dir.mkdir(); - } - - File[] files = dir.listFiles(); - for (File file : files) { - if (file.isFile() && isJAR(file)) { - classpath.add(file.toPath().toAbsolutePath()); + if (dir.exists()) { + File[] files = dir.listFiles(); + for (File file : files) { + if (file.isFile() && isJAR(file)) { + classpath.add(file.toPath().toAbsolutePath()); + } } } - return classpath; } @Override diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt index 7cdba9ca71..c481bfcc36 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt @@ -54,22 +54,30 @@ class CordappLoader private constructor(private val cordappJarPaths: List) companion object { private val logger = loggerFor() + /** + * Default cordapp dir name + */ + val CORDAPPS_DIR_NAME = "cordapps" + /** * Creates a default CordappLoader intended to be used in non-dev or non-test environments. * - * @param baseDir The directory that this node is running in. Will use this to resolve the plugins directory + * @param baseDir The directory that this node is running in. Will use this to resolve the cordapps directory * for classpath scanning. */ - fun createDefault(baseDir: Path) = CordappLoader(getCordappsInDirectory(getPluginsPath(baseDir))) + fun createDefault(baseDir: Path) = CordappLoader(getCordappsInDirectory(getCordappsPath(baseDir))) /** * Create a dev mode CordappLoader for test environments that creates and loads cordapps from the classpath - * and plugins directory. This is intended mostly for use by the driver. + * and cordapps directory. This is intended mostly for use by the driver. + * + * @param baseDir See [createDefault.baseDir] + * @param testPackages See [createWithTestPackages.testPackages] */ @VisibleForTesting fun createDefaultWithTestPackages(configuration: NodeConfiguration, testPackages: List): CordappLoader { check(configuration.devMode) { "Package scanning can only occur in dev mode" } - return CordappLoader(getCordappsInDirectory(getPluginsPath(configuration.baseDirectory)) + testPackages.flatMap(this::createScanPackage)) + return CordappLoader(getCordappsInDirectory(getCordappsPath(configuration.baseDirectory)) + testPackages.flatMap(this::createScanPackage)) } /** @@ -91,7 +99,7 @@ class CordappLoader private constructor(private val cordappJarPaths: List) @VisibleForTesting fun createDevMode(scanJars: List) = CordappLoader(scanJars) - private fun getPluginsPath(baseDir: Path): Path = baseDir / "plugins" + private fun getCordappsPath(baseDir: Path): Path = baseDir / CORDAPPS_DIR_NAME private fun createScanPackage(scanPackage: String): List { val resource = scanPackage.replace('.', '/') @@ -135,11 +143,11 @@ class CordappLoader private constructor(private val cordappJarPaths: List) return generatedCordapps[path]!! } - private fun getCordappsInDirectory(pluginsDir: Path): List { - return if (!pluginsDir.exists()) { + private fun getCordappsInDirectory(cordappsDir: Path): List { + return if (!cordappsDir.exists()) { emptyList() } else { - pluginsDir.list { + cordappsDir.list { it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.map { it.toUri().toURL() }.toList() } } diff --git a/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt b/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt index dbb62d3bac..82c48bf82e 100644 --- a/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt +++ b/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt @@ -11,6 +11,7 @@ import net.corda.core.internal.list import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap +import net.corda.node.internal.cordapp.CordappLoader import net.corda.nodeapi.User import net.corda.smoketesting.NodeConfig import net.corda.smoketesting.NodeProcess @@ -39,12 +40,12 @@ class CordappSmokeTest { @Test fun `FlowContent appName returns the filename of the CorDapp jar`() { - val pluginsDir = (factory.baseDirectory(aliceConfig) / "plugins").createDirectories() + val cordappsDir = (factory.baseDirectory(aliceConfig) / CordappLoader.CORDAPPS_DIR_NAME).createDirectories() // Find the jar file for the smoke tests of this module val selfCordapp = Paths.get("build", "libs").list { it.filter { "-smokeTests" in it.toString() }.toList().single() } - selfCordapp.copyToDirectory(pluginsDir) + selfCordapp.copyToDirectory(cordappsDir) factory.create(aliceConfig).use { alice -> alice.connect().use { connectionToAlice -> @@ -59,8 +60,8 @@ class CordappSmokeTest { } @Test - fun `empty plugins directory`() { - (factory.baseDirectory(aliceConfig) / "plugins").createDirectories() + fun `empty cordapps directory`() { + (factory.baseDirectory(aliceConfig) / CordappLoader.CORDAPPS_DIR_NAME).createDirectories() factory.create(aliceConfig).close() } diff --git a/tools/demobench/build.gradle b/tools/demobench/build.gradle index c2d1142cc7..c3626bd72f 100644 --- a/tools/demobench/build.gradle +++ b/tools/demobench/build.gradle @@ -122,12 +122,12 @@ distributions { } from(project(':finance').tasks.jar) { rename 'corda-finance-(.*)', 'corda-finance.jar' - into 'plugins' + into 'cordapps' fileMode = 0444 } from(project(':samples:bank-of-corda-demo').jar) { rename 'bank-of-corda-demo-(.*)', 'bank-of-corda.jar' - into 'plugins' + into 'cordapps' fileMode = 0444 } } @@ -201,7 +201,7 @@ task javapackage(dependsOn: distZip) { fileset(dir: dist_source, type: 'data') { include(name: 'corda/*.jar') - include(name: 'plugins/*.jar') + include(name: 'cordapps/*.jar') include(name: 'explorer/*.jar') } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt index 165388b2b0..286ad1e4c6 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt @@ -5,6 +5,7 @@ import net.corda.core.internal.div import net.corda.core.internal.list import net.corda.core.utilities.loggerFor import net.corda.demobench.model.JVMConfig +import net.corda.demobench.model.NodeConfig import net.corda.demobench.model.NodeConfigWrapper import net.corda.demobench.readErrorLines import tornadofx.* @@ -82,11 +83,11 @@ class Explorer internal constructor(private val explorerController: ExplorerCont // Note: does not copy dependencies because we should soon be making all apps fat jars and dependencies implicit. // // TODO: Remove this code when serialisation has been upgraded. - val pluginsDir = config.explorerDir / "plugins" - pluginsDir.createDirectories() - config.pluginDir.list { + val cordappsDir = config.explorerDir / NodeConfig.cordappDirName + cordappsDir.createDirectories() + config.cordappsDir.list { it.forEachOrdered { path -> - val destPath = pluginsDir / path.fileName.toString() + val destPath = cordappsDir / path.fileName.toString() try { // Try making a symlink to make things faster and use less disk space. Files.createSymbolicLink(destPath, path) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/HasPlugins.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/HasCordapps.kt similarity index 56% rename from tools/demobench/src/main/kotlin/net/corda/demobench/model/HasPlugins.kt rename to tools/demobench/src/main/kotlin/net/corda/demobench/model/HasCordapps.kt index 52a388b7a7..de0a2607b5 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/HasPlugins.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/HasCordapps.kt @@ -2,6 +2,6 @@ package net.corda.demobench.model import java.nio.file.Path -interface HasPlugins { - val pluginDir: Path +interface HasCordapps { + val cordappsDir: Path } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt index f34ebfef6d..d2dba4a687 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt @@ -37,9 +37,9 @@ class InstallFactory : Controller() { * Wraps the configuration information for a Node * which isn't ready to be instantiated yet. */ -class InstallConfig internal constructor(val baseDir: Path, private val config: NodeConfigWrapper) : HasPlugins { +class InstallConfig internal constructor(val baseDir: Path, private val config: NodeConfigWrapper) : HasCordapps { val key = config.key - override val pluginDir: Path = baseDir / "plugins" + override val cordappsDir: Path = baseDir / "cordapps" fun deleteBaseDir(): Boolean = baseDir.toFile().deleteRecursively() fun installTo(installDir: Path) = config.copy(baseDir = installDir) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt index e536b3bfb4..504fdfab79 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt @@ -31,6 +31,7 @@ data class NodeConfig( companion object { val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false) val defaultUser = user("guest") + val cordappDirName = "cordapps" } @Suppress("unused") @@ -56,18 +57,18 @@ data class NotaryService(val validating: Boolean) : ExtraService { } // TODO Think of a better name -data class NodeConfigWrapper(val baseDir: Path, val nodeConfig: NodeConfig) : HasPlugins { +data class NodeConfigWrapper(val baseDir: Path, val nodeConfig: NodeConfig) : HasCordapps { val key: String = nodeConfig.myLegalName.organisation.toKey() val nodeDir: Path = baseDir / key val explorerDir: Path = baseDir / "$key-explorer" - override val pluginDir: Path = nodeDir / "plugins" + override val cordappsDir: Path = nodeDir / NodeConfig.cordappDirName var state: NodeState = NodeState.STARTING fun install(cordapps: Collection) { if (cordapps.isEmpty()) return - pluginDir.createDirectories() + cordappsDir.createDirectories() for (cordapp in cordapps) { - cordapp.copyToDirectory(pluginDir, StandardCopyOption.REPLACE_EXISTING) + cordapp.copyToDirectory(cordappsDir, StandardCopyOption.REPLACE_EXISTING) } } } 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 4e7b6a6cc7..e13f23d3bb 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 @@ -7,7 +7,7 @@ import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.internal.noneOrSingle import net.corda.core.utilities.NetworkHostAndPort -import net.corda.demobench.plugin.PluginController +import net.corda.demobench.plugin.CordappController import net.corda.demobench.pty.R3Pty import tornadofx.* import java.io.IOException @@ -27,7 +27,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { } private val jvm by inject() - private val pluginController by inject() + private val cordappController by inject() private var baseDir: Path = baseDirFor(ManagementFactory.getRuntimeMXBean().startTime) private val cordaPath: Path = jvm.applicationDir.resolve("corda").resolve("corda.jar") @@ -112,7 +112,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { config.nodeDir.createDirectories() // Install any built-in plugins into the working directory. - pluginController.populate(config) + cordappController.populate(config) // Write this node's configuration file into its working directory. val confFile = config.nodeDir / "node.conf" @@ -164,9 +164,9 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { fun install(config: InstallConfig): NodeConfigWrapper { val installed = config.installTo(baseDir) - pluginController.userPluginsFor(config).forEach { - installed.pluginDir.createDirectories() - val plugin = it.copyToDirectory(installed.pluginDir) + cordappController.useCordappsFor(config).forEach { + installed.cordappsDir.createDirectories() + val plugin = it.copyToDirectory(installed.cordappsDir) log.info("Installed: $plugin") } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/CordappController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/CordappController.kt new file mode 100644 index 0000000000..765824f4e8 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/CordappController.kt @@ -0,0 +1,61 @@ +package net.corda.demobench.plugin + +import net.corda.core.internal.copyToDirectory +import net.corda.core.internal.createDirectories +import net.corda.core.internal.exists +import net.corda.demobench.model.HasCordapps +import net.corda.demobench.model.JVMConfig +import net.corda.demobench.model.NodeConfig +import net.corda.demobench.model.NodeConfigWrapper +import tornadofx.* +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption +import java.util.stream.Stream + +class CordappController : Controller() { + + private val jvm by inject() + private val cordappDir: Path = jvm.applicationDir.resolve(NodeConfig.cordappDirName) + private val bankOfCorda: Path = cordappDir.resolve("bank-of-corda.jar") + private val finance: Path = cordappDir.resolve("corda-finance.jar") + + /** + * Install any built-in cordapps that this node requires. + */ + @Throws(IOException::class) + fun populate(config: NodeConfigWrapper) { + if (!config.cordappsDir.exists()) { + config.cordappsDir.createDirectories() + } + if (finance.exists()) { + finance.copyToDirectory(config.cordappsDir, StandardCopyOption.REPLACE_EXISTING) + log.info("Installed 'Finance' cordapp") + } + // Nodes cannot issue cash unless they contain the "Bank of Corda" cordapp. + if (config.nodeConfig.issuableCurrencies.isNotEmpty() && bankOfCorda.exists()) { + bankOfCorda.copyToDirectory(config.cordappsDir, StandardCopyOption.REPLACE_EXISTING) + log.info("Installed 'Bank of Corda' cordapp") + } + } + + /** + * Generates a stream of a node's non-built-in cordapps. + */ + @Throws(IOException::class) + fun useCordappsFor(config: HasCordapps): Stream = walkCordapps(config.cordappsDir) + .filter { !bankOfCorda.endsWith(it.fileName) } + .filter { !finance.endsWith(it.fileName) } + + private fun walkCordapps(cordappsDir: Path): Stream { + return if (Files.isDirectory(cordappsDir)) + Files.walk(cordappsDir, 1).filter(Path::isCordapp) + else + Stream.empty() + } + +} + +fun Path.isCordapp(): Boolean = Files.isReadable(this) && this.fileName.toString().endsWith(".jar") +fun Path.inCordappsDir(): Boolean = (this.parent != null) && this.parent.endsWith("cordapps/") diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/PluginController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/PluginController.kt deleted file mode 100644 index ac25d72c55..0000000000 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/PluginController.kt +++ /dev/null @@ -1,58 +0,0 @@ -package net.corda.demobench.plugin - -import net.corda.core.internal.copyToDirectory -import net.corda.core.internal.createDirectories -import net.corda.core.internal.exists -import net.corda.demobench.model.HasPlugins -import net.corda.demobench.model.JVMConfig -import net.corda.demobench.model.NodeConfigWrapper -import tornadofx.* -import java.io.IOException -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.StandardCopyOption -import java.util.stream.Stream - -class PluginController : Controller() { - - private val jvm by inject() - private val pluginDir: Path = jvm.applicationDir.resolve("plugins") - private val bankOfCorda: Path = pluginDir.resolve("bank-of-corda.jar") - private val finance: Path = pluginDir.resolve("corda-finance.jar") - - /** - * Install any built-in plugins that this node requires. - */ - @Throws(IOException::class) - fun populate(config: NodeConfigWrapper) { - config.pluginDir.createDirectories() - if (finance.exists()) { - finance.copyToDirectory(config.pluginDir, StandardCopyOption.REPLACE_EXISTING) - log.info("Installed 'Finance' plugin") - } - // Nodes cannot issue cash unless they contain the "Bank of Corda" plugin. - if (config.nodeConfig.issuableCurrencies.isNotEmpty() && bankOfCorda.exists()) { - bankOfCorda.copyToDirectory(config.pluginDir, StandardCopyOption.REPLACE_EXISTING) - log.info("Installed 'Bank of Corda' plugin") - } - } - - /** - * Generates a stream of a node's non-built-in plugins. - */ - @Throws(IOException::class) - fun userPluginsFor(config: HasPlugins): Stream = walkPlugins(config.pluginDir) - .filter { !bankOfCorda.endsWith(it.fileName) } - .filter { !finance.endsWith(it.fileName) } - - private fun walkPlugins(pluginDir: Path): Stream { - return if (Files.isDirectory(pluginDir)) - Files.walk(pluginDir, 1).filter(Path::isPlugin) - else - Stream.empty() - } - -} - -fun Path.isPlugin(): Boolean = Files.isReadable(this) && this.fileName.toString().endsWith(".jar") -fun Path.inPluginsDir(): Boolean = (this.parent != null) && this.parent.endsWith("plugins/") diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt index c963a1c9ae..9f8a380b38 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt @@ -6,13 +6,10 @@ import javafx.stage.FileChooser import javafx.stage.FileChooser.ExtensionFilter import net.corda.core.internal.createDirectories import net.corda.core.internal.div -import net.corda.demobench.model.InstallConfig -import net.corda.demobench.model.InstallFactory -import net.corda.demobench.model.JVMConfig -import net.corda.demobench.model.NodeController -import net.corda.demobench.plugin.PluginController -import net.corda.demobench.plugin.inPluginsDir -import net.corda.demobench.plugin.isPlugin +import net.corda.demobench.model.* +import net.corda.demobench.plugin.CordappController +import net.corda.demobench.plugin.inCordappsDir +import net.corda.demobench.plugin.isCordapp import tornadofx.* import java.io.File import java.io.IOException @@ -31,7 +28,7 @@ class ProfileController : Controller() { private val jvm by inject() private val baseDir: Path = jvm.dataHome private val nodeController by inject() - private val pluginController by inject() + private val cordappController by inject() private val installFactory by inject() private val chooser = FileChooser() @@ -64,11 +61,11 @@ class ProfileController : Controller() { val file = Files.write(nodeDir / "node.conf", config.nodeConfig.toText().toByteArray(UTF_8)) log.info("Wrote: $file") - // Write all of the non-built-in plugins. - val pluginDir = Files.createDirectory(nodeDir.resolve("plugins")) - pluginController.userPluginsFor(config).forEach { - val plugin = Files.copy(it, pluginDir.resolve(it.fileName.toString())) - log.info("Wrote: $plugin") + // Write all of the non-built-in cordapps. + val cordappDir = Files.createDirectory(nodeDir.resolve(NodeConfig.cordappDirName)) + cordappController.useCordappsFor(config).forEach { + val cordapp = Files.copy(it, cordappDir.resolve(it.fileName.toString())) + log.info("Wrote: $cordapp") } } } @@ -120,16 +117,16 @@ class ProfileController : Controller() { // Now extract all of the plugins from the ZIP file, // and copy them to a temporary location. StreamSupport.stream(fs.rootDirectories.spliterator(), false) - .flatMap { Files.find(it, 3, BiPredicate { p, attr -> p.inPluginsDir() && p.isPlugin() && attr.isRegularFile }) } - .forEach { plugin -> - val config = nodeIndex[plugin.getName(0).toString()] ?: return@forEach + .flatMap { Files.find(it, 3, BiPredicate { p, attr -> p.inCordappsDir() && p.isCordapp() && attr.isRegularFile }) } + .forEach { cordapp -> + val config = nodeIndex[cordapp.getName(0).toString()] ?: return@forEach try { - val pluginDir = Files.createDirectories(config.pluginDir) - Files.copy(plugin, pluginDir.resolve(plugin.fileName.toString())) - log.info("Loaded: $plugin") + val cordappDir = Files.createDirectories(config.cordappsDir) + Files.copy(cordapp, cordappDir.resolve(cordapp.fileName.toString())) + log.info("Loaded: $cordapp") } catch (e: Exception) { - log.log(Level.SEVERE, "Failed to extract '$plugin': ${e.message}", e) + log.log(Level.SEVERE, "Failed to extract '$cordapp': ${e.message}", e) configs.forEach { c -> c.deleteBaseDir() } throw e } diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt index e8fb14dff4..0bf02c1ca8 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt @@ -5,6 +5,7 @@ import com.typesafe.config.ConfigValueFactory import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.internal.NetworkMapInfo +import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.services.config.FullNodeConfiguration import net.corda.nodeapi.User import net.corda.nodeapi.config.parseAs diff --git a/tools/explorer/src/main/java/ExplorerCaplet.java b/tools/explorer/src/main/java/ExplorerCaplet.java index 7ab25d09b5..fdeb2821fb 100644 --- a/tools/explorer/src/main/java/ExplorerCaplet.java +++ b/tools/explorer/src/main/java/ExplorerCaplet.java @@ -19,7 +19,7 @@ public class ExplorerCaplet extends Capsule { // defined as public static final fields on the Capsule class, therefore referential equality is safe. if (ATTR_APP_CLASS_PATH == attr) { T cp = super.attribute(attr); - List classpath = augmentClasspath((List) cp, "plugins"); + List classpath = augmentClasspath((List) cp, "cordapps"); return (T) augmentClasspath(classpath, "dependencies"); } return super.attribute(attr); From be235673e1f6274093c47157803a9b01388fdf7a Mon Sep 17 00:00:00 2001 From: josecoll Date: Fri, 13 Oct 2017 16:26:55 +0100 Subject: [PATCH 173/185] H2 coin selection uses Prepared Statement to protect against SQL injection (#1872) * Updated H2 coin selection code to use Prepared Statement to protect against SQL Injection attacks. * Clean-up deprecations and warnings. * Revert correct indentation. * Revert logging back to debug for SQL display. * Fix broken tests. --- .../cash/selection/CashSelectionH2Impl.kt | 64 +++++++++++-------- .../finance/contracts/asset/CashTests.kt | 51 ++++++++------- 2 files changed, 62 insertions(+), 53 deletions(-) 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 8be96121c8..82bc6ad763 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 @@ -61,20 +61,10 @@ class CashSelectionH2Impl : CashSelection { lockId: UUID, withIssuerRefs: Set): List> { - val issuerKeysStr = onlyFromIssuerParties.fold("") { left, right -> left + "('${right.owningKey.toBase58String()}')," }.dropLast(1) - val issuerRefsStr = withIssuerRefs.fold("") { left, right -> left + "('${right.bytes.toHexString()}')," }.dropLast(1) - val stateAndRefs = mutableListOf>() - // We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins: - // 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the - // running total of such an accumulator - // 2) H2 uses session variables to perform this accumulator function: - // http://www.h2database.com/html/functions.html#set - // 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries) - for (retryCount in 1..MAX_RETRIES) { - if (!attemptSpend(services, amount, lockId, notary, onlyFromIssuerParties, issuerKeysStr, withIssuerRefs, issuerRefsStr, stateAndRefs)) { + if (!attemptSpend(services, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs, stateAndRefs)) { log.warn("Coin selection failed on attempt $retryCount") // TODO: revisit the back off strategy for contended spending. if (retryCount != MAX_RETRIES) { @@ -91,32 +81,52 @@ class CashSelectionH2Impl : CashSelection { return stateAndRefs } - private fun attemptSpend(services: ServiceHub, amount: Amount, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set, issuerKeysStr: String, withIssuerRefs: Set, issuerRefsStr: String, stateAndRefs: MutableList>): Boolean { + // We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins: + // 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the + // running total of such an accumulator + // 2) H2 uses session variables to perform this accumulator function: + // http://www.h2database.com/html/functions.html#set + // 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries) + + private fun attemptSpend(services: ServiceHub, amount: Amount, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set, withIssuerRefs: Set, stateAndRefs: MutableList>): Boolean { + val connection = services.jdbcSession() spendLock.withLock { - val statement = services.jdbcSession().createStatement() + val statement = connection.createStatement() try { statement.execute("CALL SET(@t, CAST(0 AS BIGINT));") - // we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null) - // the softLockReserve update will detect whether we try to lock states locked by others val selectJoin = """ - SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id - 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 ccs.ccy_code = '${amount.token}' and @t < ${amount.quantity} - AND (vs.lock_id = '$lockId' OR vs.lock_id is null) - """ + + SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id + 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 ccs.ccy_code = ? and @t < ? + AND (vs.lock_id = ? OR vs.lock_id is null) + """ + (if (notary != null) - " AND vs.notary_name = '${notary.name}'" else "") + + " AND vs.notary_name = ?" else "") + (if (onlyFromIssuerParties.isNotEmpty()) - " AND ccs.issuer_key IN ($issuerKeysStr)" else "") + + " AND ccs.issuer_key IN (?)" else "") + (if (withIssuerRefs.isNotEmpty()) - " AND ccs.issuer_ref IN ($issuerRefsStr)" else "") + " AND ccs.issuer_ref IN (?)" else "") + + // Use prepared statement for protection against SQL Injection (http://www.h2database.com/html/advanced.html#sql_injection) + val psSelectJoin = connection.prepareStatement(selectJoin) + var pIndex = 0 + psSelectJoin.setString(++pIndex, amount.token.currencyCode) + psSelectJoin.setLong(++pIndex, amount.quantity) + psSelectJoin.setString(++pIndex, lockId.toString()) + if (notary != null) + psSelectJoin.setString(++pIndex, notary.name.toString()) + if (onlyFromIssuerParties.isNotEmpty()) + psSelectJoin.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toBase58String() as Any}.toTypedArray() ) + if (withIssuerRefs.isNotEmpty()) + psSelectJoin.setObject(++pIndex, withIssuerRefs.map { it.bytes.toHexString() as Any }.toTypedArray()) + log.debug { psSelectJoin.toString() } // Retrieve spendable state refs - val rs = statement.executeQuery(selectJoin) - log.debug(selectJoin) + val rs = psSelectJoin.executeQuery() + stateAndRefs.clear() var totalPennies = 0L while (rs.next()) { val txHash = SecureHash.parse(rs.getString(1)) diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt index 537f347bd6..df14ca6843 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt @@ -3,9 +3,7 @@ package net.corda.finance.contracts.asset import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.AnonymousParty -import net.corda.core.identity.Party +import net.corda.core.identity.* import net.corda.core.node.ServiceHub import net.corda.core.node.services.VaultService import net.corda.core.node.services.queryBy @@ -32,25 +30,25 @@ import java.util.* import kotlin.test.* class CashTests : TestDependencyInjectionBase() { - val defaultRef = OpaqueBytes(ByteArray(1, { 1 })) - val defaultIssuer = MEGA_CORP.ref(defaultRef) - val inState = Cash.State( + private val defaultRef = OpaqueBytes(ByteArray(1, { 1 })) + private val defaultIssuer = MEGA_CORP.ref(defaultRef) + private val inState = Cash.State( amount = 1000.DOLLARS `issued by` defaultIssuer, owner = AnonymousParty(ALICE_PUBKEY) ) // Input state held by the issuer - val issuerInState = inState.copy(owner = defaultIssuer.party) - val outState = issuerInState.copy(owner = AnonymousParty(BOB_PUBKEY)) + private val issuerInState = inState.copy(owner = defaultIssuer.party) + private val outState = issuerInState.copy(owner = AnonymousParty(BOB_PUBKEY)) - fun Cash.State.editDepositRef(ref: Byte) = copy( + private fun Cash.State.editDepositRef(ref: Byte) = copy( amount = Amount(amount.quantity, token = amount.token.copy(amount.token.issuer.copy(reference = OpaqueBytes.of(ref)))) ) - lateinit var miniCorpServices: MockServices - lateinit var megaCorpServices: MockServices + private lateinit var miniCorpServices: MockServices + private lateinit var megaCorpServices: MockServices val vault: VaultService get() = miniCorpServices.vaultService lateinit var database: CordaPersistence - lateinit var vaultStatesUnconsumed: List> + private lateinit var vaultStatesUnconsumed: List> @Before fun setUp() { @@ -475,19 +473,20 @@ class CashTests : TestDependencyInjectionBase() { // // Spend tx generation - val OUR_KEY: KeyPair by lazy { generateKeyPair() } - val OUR_IDENTITY_1: AbstractParty get() = AnonymousParty(OUR_KEY.public) + private val OUR_KEY: KeyPair by lazy { generateKeyPair() } + private val OUR_IDENTITY_1: AbstractParty get() = AnonymousParty(OUR_KEY.public) + private val OUR_IDENTITY_AND_CERT = getTestPartyAndCertificate(CordaX500Name(organisation = "Me", locality = "London", country = "GB"), OUR_KEY.public) - val THEIR_IDENTITY_1 = AnonymousParty(MINI_CORP_PUBKEY) - val THEIR_IDENTITY_2 = AnonymousParty(CHARLIE_PUBKEY) + private val THEIR_IDENTITY_1 = AnonymousParty(MINI_CORP_PUBKEY) + private val THEIR_IDENTITY_2 = AnonymousParty(CHARLIE_PUBKEY) - fun makeCash(amount: Amount, issuer: AbstractParty, depositRef: Byte = 1) = + private fun makeCash(amount: Amount, issuer: AbstractParty, depositRef: Byte = 1) = StateAndRef( - TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), OUR_IDENTITY_1), Cash.PROGRAM_ID, DUMMY_NOTARY), + TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), OUR_IDENTITY_1), Cash.PROGRAM_ID, DUMMY_NOTARY), StateRef(SecureHash.randomSHA256(), Random().nextInt(32)) ) - val WALLET = listOf( + private val WALLET = listOf( makeCash(100.DOLLARS, MEGA_CORP), makeCash(400.DOLLARS, MEGA_CORP), makeCash(80.DOLLARS, MINI_CORP), @@ -507,7 +506,7 @@ class CashTests : TestDependencyInjectionBase() { private fun makeSpend(amount: Amount, dest: AbstractParty): WireTransaction { val tx = TransactionBuilder(DUMMY_NOTARY) database.transaction { - Cash.generateSpend(miniCorpServices, tx, amount, dest) + Cash.generateSpend(miniCorpServices, tx, amount, OUR_IDENTITY_AND_CERT, dest) } return tx.toWireTransaction(miniCorpServices) } @@ -541,7 +540,7 @@ class CashTests : TestDependencyInjectionBase() { assertEquals(1, expectedInputs.size) val inputState = expectedInputs.single() val actualChange = wtx.outputs.single().data as Cash.State - val expectedChangeAmount = (inputState.state.data as Cash.State).amount.quantity - 50.DOLLARS.quantity + val expectedChangeAmount = inputState.state.data.amount.quantity - 50.DOLLARS.quantity val expectedChange = WALLET[0].state.data.copy(amount = WALLET[0].state.data.amount.copy(quantity = expectedChangeAmount), owner = actualChange.owner) assertEquals(expectedChange, wtx.getOutput(0)) } @@ -588,9 +587,9 @@ class CashTests : TestDependencyInjectionBase() { @Test fun generateExitWithEmptyVault() { initialiseTestSerialization() - assertFailsWith { + assertFailsWith { val tx = TransactionBuilder(DUMMY_NOTARY) - Cash().generateExit(tx, Amount(100, Issued(CHARLIE.ref(1), GBP)), emptyList()) + Cash().generateExit(tx, Amount(100, Issued(CHARLIE.ref(1), GBP)), emptyList(), OUR_IDENTITY_1) } } @@ -615,7 +614,7 @@ class CashTests : TestDependencyInjectionBase() { database.transaction { val tx = TransactionBuilder(DUMMY_NOTARY) - Cash.generateSpend(miniCorpServices, tx, 80.DOLLARS, ALICE, setOf(MINI_CORP)) + Cash.generateSpend(miniCorpServices, tx, 80.DOLLARS, OUR_IDENTITY_AND_CERT, ALICE, setOf(MINI_CORP)) assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0]) } @@ -631,13 +630,13 @@ class CashTests : TestDependencyInjectionBase() { database.transaction { val vaultState = vaultStatesUnconsumed.elementAt(0) val changeAmount = 90.DOLLARS `issued by` defaultIssuer - val likelyChangeState = wtx.outputs.map(TransactionState<*>::data).filter { state -> + val likelyChangeState = wtx.outputs.map(TransactionState<*>::data).single { state -> if (state is Cash.State) { state.amount == changeAmount } else { false } - }.single() + } val changeOwner = (likelyChangeState as Cash.State).owner assertEquals(1, miniCorpServices.keyManagementService.filterMyKeys(setOf(changeOwner.owningKey)).toList().size) assertEquals(vaultState.ref, wtx.inputs[0]) From 8549a77a7624bb4a35acc34b5ec39fdac293b311 Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Fri, 13 Oct 2017 16:31:44 +0100 Subject: [PATCH 174/185] Use system tmp dir instead of hardcoding unix path --- .../src/test/kotlin/com/r3/enclaves/txverify/EnclaveletTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/verify-enclave/src/test/kotlin/com/r3/enclaves/txverify/EnclaveletTest.kt b/verify-enclave/src/test/kotlin/com/r3/enclaves/txverify/EnclaveletTest.kt index 329eb22b8e..0aa04f5c29 100644 --- a/verify-enclave/src/test/kotlin/com/r3/enclaves/txverify/EnclaveletTest.kt +++ b/verify-enclave/src/test/kotlin/com/r3/enclaves/txverify/EnclaveletTest.kt @@ -44,7 +44,7 @@ class EnclaveletTest { val cashContract = MockContractAttachment(interpreter.services.cordappProvider.getContractAttachmentID(Cash.PROGRAM_ID)!!, Cash.PROGRAM_ID) val req = TransactionVerificationRequest(wtx3.serialize(), arrayOf(wtx1.serialize(), wtx2.serialize()), arrayOf(cashContract.serialize().bytes)) val serialized = req.serialize() - Files.write(Paths.get("/tmp/req"), serialized.bytes) + Files.write(Paths.get(System.getProperty("java.io.tmpdir"), "req"), serialized.bytes) verifyInEnclave(serialized.bytes) } } From 99b509cb689be3a961ca181c4035da6ef4851b81 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Mon, 16 Oct 2017 09:16:40 +0100 Subject: [PATCH 175/185] Attempting to make VerifierTests more stable (#1883) * Attempting to make VerifierTests more stable Main point: Alice node will always be faster to locate Alice by name than NotaryNode. Especially given that it may take some time for Alice node to get reflected on the Notary's NetworkMap. Also additional logging which might be helpful during debug and IntelliJ changed project structure reflection. * Introduce a way for nodes to reliably learn about each other existence in the NetworkMap Plus minor refactoring of the Driver code. --- .idea/compiler.xml | 2 ++ .../kotlin/net/corda/testing/driver/Driver.kt | 36 +++++++++++++------ .../net/corda/verifier/VerifierTests.kt | 12 ++++--- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 5df761b918..0c996385a3 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -60,6 +60,8 @@ + + diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index 0a8cf4ecbe..ed9ca09e6d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -67,6 +67,10 @@ import kotlin.concurrent.thread private val log: Logger = loggerFor() +private val DEFAULT_POLL_INTERVAL = 500.millis + +private const val DEFAULT_WARN_COUNT = 120 + /** * This is the interface that's exposed to DSL users. */ @@ -102,13 +106,12 @@ interface DriverDSLExposedInterface : CordformContext { validating: Boolean = true): CordaFuture /** - * Helper function for starting a [node] with custom parameters from Java. + * Helper function for starting a [Node] with custom parameters from Java. * - * @param defaultParameters The default parameters for the driver. - * @param dsl The dsl itself. + * @param parameters The default parameters for the driver. * @return The value returned in the [dsl] closure. */ - fun startNode(parameters: NodeParameters): CordaFuture { + fun startNode(parameters: NodeParameters): CordaFuture { return startNode(defaultParameters = parameters) } @@ -165,16 +168,25 @@ interface DriverDSLExposedInterface : CordformContext { * @param check The function being polled. * @return A future that completes with the non-null value [check] has returned. */ - fun pollUntilNonNull(pollName: String, pollInterval: Duration = 500.millis, warnCount: Int = 120, check: () -> A?): CordaFuture + fun pollUntilNonNull(pollName: String, pollInterval: Duration = DEFAULT_POLL_INTERVAL, warnCount: Int = DEFAULT_WARN_COUNT, check: () -> A?): CordaFuture /** * Polls the given function until it returns true. * @see pollUntilNonNull */ - fun pollUntilTrue(pollName: String, pollInterval: Duration = 500.millis, warnCount: Int = 120, check: () -> Boolean): CordaFuture { + fun pollUntilTrue(pollName: String, pollInterval: Duration = DEFAULT_POLL_INTERVAL, warnCount: Int = DEFAULT_WARN_COUNT, check: () -> Boolean): CordaFuture { return pollUntilNonNull(pollName, pollInterval, warnCount) { if (check()) Unit else null } } + /** + * Polls until a given node knows about presence of another node via its own NetworkMap + */ + fun NodeHandle.pollUntilKnowsAbout(another: NodeHandle, pollInterval: Duration = DEFAULT_POLL_INTERVAL, warnCount: Int = DEFAULT_WARN_COUNT): CordaFuture { + return pollUntilTrue("${nodeInfo.legalIdentities} knows about ${another.nodeInfo.legalIdentities}", pollInterval, warnCount) { + another.nodeInfo in rpc.networkMapSnapshot() + } + } + val shutdownManager: ShutdownManager } @@ -345,7 +357,7 @@ fun driver( /** * Helper function for starting a [driver] with custom parameters from Java. * - * @param defaultParameters The default parameters for the driver. + * @param parameters The default parameters for the driver. * @param dsl The dsl itself. * @return The value returned in the [dsl] closure. */ @@ -706,7 +718,7 @@ class DriverDSL( "webAddress" to webAddress.toString(), "networkMapService" to networkMapServiceConfigLookup(name), "useTestClock" to useTestClock, - "rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers.map { it.toMap() }, + "rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers.map { it.toConfig().root().unwrapped() }, "verifierType" to verifierType.name ) + customOverrides ) @@ -883,14 +895,16 @@ class DriverDSL( } establishRpc(nodeConfiguration, processDeathFuture).flatMap { rpc -> // Call waitUntilNetworkReady in background in case RPC is failing over: - val networkMapFuture = executorService.fork { + val forked = executorService.fork { rpc.waitUntilNetworkReady() - }.flatMap { it } + } + val networkMapFuture = forked.flatMap { it } firstOf(processDeathFuture, networkMapFuture) { if (it == processDeathFuture) { throw ListenProcessDeathException(nodeConfiguration.p2pAddress, process) } processDeathFuture.cancel(false) + log.info("Node handle is ready. NodeInfo: ${rpc.nodeInfo()}, WebAddress: ${webAddress}") NodeHandle.OutOfProcess(rpc.nodeInfo(), rpc, nodeConfiguration, webAddress, debugPort, process) } } @@ -905,7 +919,7 @@ class DriverDSL( } companion object { - private val defaultRpcUserList = listOf(User("default", "default", setOf("ALL")).toMap()) + private val defaultRpcUserList = listOf(User("default", "default", setOf("ALL")).toConfig().root().unwrapped()) private val names = arrayOf( ALICE.name, diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt index 7bdbecaf17..05bfc06673 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt @@ -6,20 +6,19 @@ import net.corda.core.messaging.startFlow import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow import net.corda.finance.DOLLARS import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.config.VerifierType import net.corda.testing.ALICE import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.chooseIdentity -import net.corda.node.services.transactions.ValidatingNotaryService -import net.corda.nodeapi.internal.ServiceInfo import net.corda.testing.* import net.corda.testing.driver.NetworkMapStartStrategy import org.junit.Test import java.util.* import java.util.concurrent.atomic.AtomicInteger +import kotlin.test.assertNotNull class VerifierTests { private fun generateTransactions(number: Int): List { @@ -121,13 +120,16 @@ class VerifierTests { val notaryFuture = startNotaryNode(DUMMY_NOTARY.name, verifierType = VerifierType.OutOfProcess) val aliceNode = aliceFuture.get() val notaryNode = notaryFuture.get() - val alice = notaryNode.rpc.wellKnownPartyFromX500Name(ALICE_NAME)!! + val alice = aliceNode.rpc.wellKnownPartyFromX500Name(ALICE_NAME)!! val notary = notaryNode.rpc.notaryPartyFromX500Name(DUMMY_NOTARY_SERVICE_NAME)!! startVerifier(notaryNode) + notaryNode.pollUntilKnowsAbout(aliceNode).getOrThrow() + aliceNode.pollUntilKnowsAbout(notaryNode).getOrThrow() aliceNode.rpc.startFlow(::CashIssueFlow, 10.DOLLARS, OpaqueBytes.of(0), notary).returnValue.get() notaryNode.waitUntilNumberOfVerifiers(1) for (i in 1..10) { - aliceNode.rpc.startFlow(::CashPaymentFlow, 10.DOLLARS, alice).returnValue.get() + val cashFlowResult = aliceNode.rpc.startFlow(::CashPaymentFlow, 10.DOLLARS, alice).returnValue.get() + assertNotNull(cashFlowResult) } } } From d3d87c2497d51386b0e7634b338157db6b5cb464 Mon Sep 17 00:00:00 2001 From: Alberto Arri <30873160+al-r3@users.noreply.github.com> Date: Mon, 16 Oct 2017 09:42:13 +0100 Subject: [PATCH 176/185] [CORDA-442] DemoBench copies around the NodeInfos for running nodes. (#1796) [CORDA-442] DemoBench copies around the NodeInfos for running nodes. --- tools/demobench/build.gradle | 2 + .../corda/demobench/model/NodeController.kt | 7 + .../demobench/model/NodeInfoFilesCopier.kt | 133 +++++++++++++++ .../model/NodeInfoFilesCopierTest.kt | 156 ++++++++++++++++++ 4 files changed, 298 insertions(+) create mode 100644 tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeInfoFilesCopier.kt create mode 100644 tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeInfoFilesCopierTest.kt diff --git a/tools/demobench/build.gradle b/tools/demobench/build.gradle index c3626bd72f..c0f21ad796 100644 --- a/tools/demobench/build.gradle +++ b/tools/demobench/build.gradle @@ -46,6 +46,8 @@ dependencies { // Controls FX: more java FX components http://fxexperience.com/controlsfx/ compile "org.controlsfx:controlsfx:$controlsfx_version" + compile "net.corda.plugins:cordform-common:$gradle_plugins_version" + compile project(':client:rpc') compile project(':finance') 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 e13f23d3bb..ddd8010bb4 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 @@ -28,6 +28,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { private val jvm by inject() private val cordappController by inject() + private val nodeInfoFilesCopier by inject() private var baseDir: Path = baseDirFor(ManagementFactory.getRuntimeMXBean().startTime) private val cordaPath: Path = jvm.applicationDir.resolve("corda").resolve("corda.jar") @@ -86,12 +87,16 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { log.info("Network map provided by: ${nodeConfig.myLegalName}") } + nodeInfoFilesCopier.addConfig(wrapper) + return wrapper } fun dispose(config: NodeConfigWrapper) { config.state = NodeState.DEAD + nodeInfoFilesCopier.removeConfig(config) + if (config.nodeConfig.isNetworkMap) { log.warning("Network map service (Node '${config.nodeConfig.myLegalName}') has exited.") } @@ -138,6 +143,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { // Wipe out any knowledge of previous nodes. networkMapConfig = null nodes.clear() + nodeInfoFilesCopier.reset() } /** @@ -147,6 +153,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { if (nodes.putIfAbsent(config.key, config) != null) { return false } + nodeInfoFilesCopier.addConfig(config) updatePort(config.nodeConfig) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeInfoFilesCopier.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeInfoFilesCopier.kt new file mode 100644 index 0000000000..ed69e38b93 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeInfoFilesCopier.kt @@ -0,0 +1,133 @@ +package net.corda.demobench.model + +import net.corda.cordform.CordformNode +import net.corda.core.internal.createDirectories +import net.corda.core.internal.isRegularFile +import net.corda.core.internal.list +import rx.Observable +import rx.Scheduler +import rx.schedulers.Schedulers +import tornadofx.* +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption.COPY_ATTRIBUTES +import java.nio.file.StandardCopyOption.REPLACE_EXISTING +import java.nio.file.attribute.BasicFileAttributes +import java.nio.file.attribute.FileTime +import java.util.concurrent.TimeUnit +import java.util.logging.Level + +/** + * Utility class which copies nodeInfo files across a set of running nodes. + * + * This class will create paths that it needs to poll and to where it needs to copy files in case those + * don't exist yet. + */ +class NodeInfoFilesCopier(scheduler: Scheduler = Schedulers.io()): Controller() { + + private val nodeDataMap = mutableMapOf() + + init { + Observable.interval(5, TimeUnit.SECONDS, scheduler) + .subscribe { poll() } + } + + /** + * @param nodeConfig the configuration to be added. + * Add a [NodeConfig] for a node which is about to be started. + * Its nodeInfo file will be copied to other nodes' additional-node-infos directory, and conversely, + * other nodes' nodeInfo files will be copied to this node additional-node-infos directory. + */ + @Synchronized + fun addConfig(nodeConfig: NodeConfigWrapper) { + val newNodeFile = NodeData(nodeConfig.nodeDir) + nodeDataMap[nodeConfig.nodeDir] = newNodeFile + + for (previouslySeenFile in allPreviouslySeenFiles()) { + copy(previouslySeenFile, newNodeFile.destination.resolve(previouslySeenFile.fileName)) + } + log.info("Now watching: ${nodeConfig.nodeDir}") + } + + /** + * @param nodeConfig the configuration to be removed. + * Remove the configuration of a node which is about to be stopped or already stopped. + * No files written by that node will be copied to other nodes, nor files from other nodes will be copied to this + * one. + */ + @Synchronized + fun removeConfig(nodeConfig: NodeConfigWrapper) { + nodeDataMap.remove(nodeConfig.nodeDir) ?: return + log.info("Stopped watching: ${nodeConfig.nodeDir}") + } + + @Synchronized + fun reset() { + nodeDataMap.clear() + } + + private fun allPreviouslySeenFiles() = nodeDataMap.values.map { it.previouslySeenFiles.keys }.flatten() + + @Synchronized + private fun poll() { + for (nodeData in nodeDataMap.values) { + nodeData.nodeDir.list { paths -> + paths.filter { it.isRegularFile() } + .filter { it.fileName.toString().startsWith("nodeInfo-") } + .forEach { path -> processPath(nodeData, path) } + } + } + } + + // Takes a path under nodeData config dir and decides whether the file represented by that path needs to + // be copied. + private fun processPath(nodeData: NodeData, path: Path) { + val newTimestamp = Files.readAttributes(path, BasicFileAttributes::class.java).lastModifiedTime() + val previousTimestamp = nodeData.previouslySeenFiles.put(path, newTimestamp) ?: FileTime.fromMillis(-1) + if (newTimestamp > previousTimestamp) { + for (destination in nodeDataMap.values.filter { it.nodeDir != nodeData.nodeDir }.map { it.destination }) { + val fullDestinationPath = destination.resolve(path.fileName) + copy(path, fullDestinationPath) + } + } + } + + private fun copy(source: Path, destination: Path) { + val tempDestination = try { + Files.createTempFile(destination.parent, ".", null) + } catch (exception: IOException) { + log.log(Level.WARNING, "Couldn't create a temporary file to copy $source", exception) + throw exception + } + try { + // First copy the file to a temporary file within the appropriate directory. + Files.copy(source, tempDestination, COPY_ATTRIBUTES, REPLACE_EXISTING) + } catch (exception: IOException) { + log.log(Level.WARNING, "Couldn't copy $source to $tempDestination.", exception) + Files.delete(tempDestination) + throw exception + } + try { + // Then rename it to the desired name. This way the file 'appears' on the filesystem as an atomic operation. + Files.move(tempDestination, destination, REPLACE_EXISTING) + } catch (exception: IOException) { + log.log(Level.WARNING, "Couldn't move $tempDestination to $destination.", exception) + Files.delete(tempDestination) + throw exception + } + } + + /** + * Convenience holder for all the paths and files relative to a single node. + */ + private class NodeData(val nodeDir: Path) { + val destination: Path = nodeDir.resolve(CordformNode.NODE_INFO_DIRECTORY) + // Map from Path to its lastModifiedTime. + val previouslySeenFiles = mutableMapOf() + + init { + destination.createDirectories() + } + } +} diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeInfoFilesCopierTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeInfoFilesCopierTest.kt new file mode 100644 index 0000000000..557bd7095b --- /dev/null +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeInfoFilesCopierTest.kt @@ -0,0 +1,156 @@ +package net.corda.demobench.model + +import net.corda.cordform.CordformNode +import net.corda.core.identity.CordaX500Name +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.testing.eventually +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import rx.schedulers.TestScheduler +import java.nio.file.Files +import java.nio.file.Path +import java.time.Duration +import java.util.concurrent.TimeUnit +import kotlin.streams.toList +import kotlin.test.assertEquals + +/** + * tests for [NodeInfoFilesCopier] + */ +class NodeInfoFilesCopierTest { + + @Rule @JvmField var folder = TemporaryFolder() + private val rootPath get() = folder.root.toPath() + private val scheduler = TestScheduler() + companion object { + private const val ORGANIZATION = "Organization" + private const val NODE_1_PATH = "node1" + private const val NODE_2_PATH = "node2" + + private val content = "blah".toByteArray(Charsets.UTF_8) + private val GOOD_NODE_INFO_NAME = "nodeInfo-test" + private val GOOD_NODE_INFO_NAME_2 = "nodeInfo-anotherNode" + private val BAD_NODE_INFO_NAME = "something" + private val legalName = CordaX500Name(organisation = ORGANIZATION, locality = "Nowhere", country = "GB") + private val hostAndPort = NetworkHostAndPort("localhost", 1) + } + + private fun nodeDir(nodeBaseDir : String) = rootPath.resolve(nodeBaseDir).resolve(ORGANIZATION.toLowerCase()) + + private val node1Config by lazy { createConfig(NODE_1_PATH) } + private val node2Config by lazy { createConfig(NODE_2_PATH) } + private val node1RootPath by lazy { nodeDir(NODE_1_PATH) } + private val node2RootPath by lazy { nodeDir(NODE_2_PATH) } + private val node1AdditionalNodeInfoPath by lazy { node1RootPath.resolve(CordformNode.NODE_INFO_DIRECTORY) } + private val node2AdditionalNodeInfoPath by lazy { node2RootPath.resolve(CordformNode.NODE_INFO_DIRECTORY) } + + lateinit var nodeInfoFilesCopier: NodeInfoFilesCopier + + @Before + fun setUp() { + nodeInfoFilesCopier = NodeInfoFilesCopier(scheduler) + } + + @Test + fun `files created before a node is started are copied to that node`() { + // Configure the first node. + nodeInfoFilesCopier.addConfig(node1Config) + // Ensure directories are created. + advanceTime() + + // Create 2 files, a nodeInfo and another file in node1 folder. + Files.write(node1RootPath.resolve(GOOD_NODE_INFO_NAME), content) + Files.write(node1RootPath.resolve(BAD_NODE_INFO_NAME), content) + + // Configure the second node. + nodeInfoFilesCopier.addConfig(node2Config) + advanceTime() + + eventually(Duration.ofMinutes(1)) { + // Check only one file is copied. + checkDirectoryContainsSingleFile(node2AdditionalNodeInfoPath, GOOD_NODE_INFO_NAME) + } + } + + @Test + fun `polling of running nodes`() { + // Configure 2 nodes. + nodeInfoFilesCopier.addConfig(node1Config) + nodeInfoFilesCopier.addConfig(node2Config) + advanceTime() + + // Create 2 files, one of which to be copied, in a node root path. + Files.write(node2RootPath.resolve(GOOD_NODE_INFO_NAME), content) + Files.write(node2RootPath.resolve(BAD_NODE_INFO_NAME), content) + advanceTime() + + eventually(Duration.ofMinutes(1)) { + // Check only one file is copied to the other node. + checkDirectoryContainsSingleFile(node1AdditionalNodeInfoPath, GOOD_NODE_INFO_NAME) + } + } + + @Test + fun `remove nodes`() { + // Configure 2 nodes. + nodeInfoFilesCopier.addConfig(node1Config) + nodeInfoFilesCopier.addConfig(node2Config) + advanceTime() + + // Create a file, in node 2 root path. + Files.write(node2RootPath.resolve(GOOD_NODE_INFO_NAME), content) + advanceTime() + + // Remove node 2 + nodeInfoFilesCopier.removeConfig(node2Config) + + // Create another file in node 2 directory. + Files.write(node2RootPath.resolve(GOOD_NODE_INFO_NAME_2), content) + advanceTime() + + eventually(Duration.ofMinutes(1)) { + // Check only one file is copied to the other node. + checkDirectoryContainsSingleFile(node1AdditionalNodeInfoPath, GOOD_NODE_INFO_NAME) + } + } + + @Test + fun `clear`() { + // Configure 2 nodes. + nodeInfoFilesCopier.addConfig(node1Config) + nodeInfoFilesCopier.addConfig(node2Config) + advanceTime() + + nodeInfoFilesCopier.reset() + + advanceTime() + Files.write(node2RootPath.resolve(GOOD_NODE_INFO_NAME_2), content) + + // Give some time to the filesystem to report the change. + Thread.sleep(100) + assertEquals(0, Files.list(node1AdditionalNodeInfoPath).toList().size) + } + + private fun advanceTime() { + scheduler.advanceTimeBy(1, TimeUnit.HOURS) + } + + private fun checkDirectoryContainsSingleFile(path: Path, filename: String) { + assertEquals(1, Files.list(path).toList().size) + val onlyFileName = Files.list(path).toList().first().fileName.toString() + assertEquals(filename, onlyFileName) + } + + private fun createConfig(relativePath: String) = + NodeConfigWrapper(rootPath.resolve(relativePath), + NodeConfig(myLegalName = legalName, + p2pAddress = hostAndPort, + rpcAddress = hostAndPort, + webAddress = hostAndPort, + h2port = -1, + notary = null, + networkMapService = null, + rpcUsers = listOf())) +} \ No newline at end of file From 38cf4a489eb21edf3a1800cb3924b57f46f9e89a Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Mon, 16 Oct 2017 11:35:29 +0100 Subject: [PATCH 177/185] CORDA-676 Eager cordapp schemas (#1839) * Retire customSchemas. * Key cordapp-to-hash map by url as native equality too strict. --- .../client/rpc/CordaRPCJavaClientTest.java | 3 +- .../corda/client/rpc/CordaRPCClientTest.kt | 4 +- .../net/corda/core/internal/InternalUtils.kt | 3 + .../corda/core/internal/InternalUtilsTest.kt | 1 + .../net/corda/docs/CustomVaultQueryTest.kt | 6 +- .../docs/FxTransactionBuildTutorialTest.kt | 5 +- .../internal/AttachmentsClassLoaderTests.kt | 3 +- .../node/services/AttachmentLoadingTests.kt | 2 +- .../net/corda/node/internal/AbstractNode.kt | 40 ++++----- .../kotlin/net/corda/node/internal/Node.kt | 3 +- .../net/corda/node/internal/NodeStartup.kt | 2 +- .../node/internal/cordapp/CordappLoader.kt | 82 +++++++++++-------- .../internal/cordapp/CordappProviderImpl.kt | 26 ++---- .../cordapp/CordappProviderInternal.kt | 8 ++ .../corda/node/services/api/SchemaService.kt | 6 -- .../node/services/api/ServiceHubInternal.kt | 3 +- .../persistence/HibernateConfiguration.kt | 22 ++--- .../node/services/schema/HibernateObserver.kt | 2 +- .../node/services/schema/NodeSchemaService.kt | 20 ++--- .../node/services/vault/NodeVaultService.kt | 12 +-- .../corda/node/utilities/CordaPersistence.kt | 11 +-- .../services/vault/VaultQueryJavaTests.java | 6 +- .../net/corda/node/InteractiveShellTest.kt | 2 +- .../cordapp/CordappProviderImplTests.kt | 16 ++-- .../events/NodeSchedulerServiceTest.kt | 4 +- .../messaging/ArtemisMessagingTests.kt | 2 +- .../persistence/DBCheckpointStorageTests.kt | 2 +- .../persistence/DBTransactionStorageTests.kt | 2 +- .../persistence/HibernateConfigurationTest.kt | 25 +++--- .../persistence/NodeAttachmentStorageTest.kt | 2 +- .../services/schema/HibernateObserverTests.kt | 4 +- .../services/schema/NodeSchemaServiceTest.kt | 17 +++- .../DistributedImmutableMapTests.kt | 2 +- .../PersistentUniquenessProviderTests.kt | 2 +- .../services/vault/NodeVaultServiceTest.kt | 4 +- .../node/services/vault/VaultQueryTests.kt | 15 ++-- .../node/services/vault/VaultWithCashTest.kt | 4 +- .../corda/node/utilities/ObservablesTests.kt | 2 +- .../corda/irs/api/NodeInterestRatesTest.kt | 2 +- .../net/corda/netmap/simulation/Simulation.kt | 7 -- .../net/corda/traderdemo/TraderDemoTest.kt | 8 +- .../node/testing/MockServiceHubInternal.kt | 9 +- .../net/corda/testing/node/MockServices.kt | 22 +++-- .../net/corda/testing/node/SimpleNode.kt | 2 +- .../kotlin/net/corda/testing/LogHelper.kt | 5 +- .../corda/testing/node/MockCordappProvider.kt | 3 +- 46 files changed, 197 insertions(+), 236 deletions(-) create mode 100644 node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderInternal.kt 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 714b0d4316..4c0245462d 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 @@ -32,7 +32,7 @@ import static net.corda.testing.TestConstants.getALICE; public class CordaRPCJavaClientTest extends NodeBasedTest { public CordaRPCJavaClientTest() { - super(Collections.singletonList("net.corda.finance.contracts")); + super(Arrays.asList("net.corda.finance.contracts", CashSchemaV1.class.getPackage().getName())); } private List perms = Arrays.asList(startFlowPermission(CashPaymentFlow.class), startFlowPermission(CashIssueFlow.class)); @@ -53,7 +53,6 @@ public class CordaRPCJavaClientTest extends NodeBasedTest { public void setUp() throws ExecutionException, InterruptedException { CordaFuture> nodeFuture = startNotaryNode(getALICE().getName(), singletonList(rpcUser), true); node = nodeFuture.get(); - node.getInternals().registerCustomSchemas(Collections.singleton(CashSchemaV1.INSTANCE)); client = new CordaRPCClient(requireNonNull(node.getInternals().getConfiguration().getRpcAddress())); } diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt index be6f05715a..277769354c 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt @@ -2,6 +2,7 @@ package net.corda.client.rpc import net.corda.core.crypto.random63BitValue import net.corda.core.flows.FlowInitiator +import net.corda.core.internal.packageName import net.corda.core.messaging.FlowProgressHandle import net.corda.core.messaging.StateMachineUpdate import net.corda.core.messaging.startFlow @@ -32,7 +33,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance.contracts")) { +class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance.contracts", CashSchemaV1::class.packageName)) { private val rpcUser = User("user1", "test", permissions = setOf( startFlowPermission(), startFlowPermission() @@ -48,7 +49,6 @@ class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance.contracts")) @Before fun setUp() { node = startNotaryNode(ALICE.name, rpcUsers = listOf(rpcUser)).getOrThrow() - node.internals.registerCustomSchemas(setOf(CashSchemaV1)) client = CordaRPCClient(node.internals.configuration.rpcAddress!!) } 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 5f26ef2e48..2f101b869f 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -300,3 +300,6 @@ fun TransactionBuilder.toWireTransaction(services: ServicesForResolution, serial * @suppress */ fun TransactionBuilder.toLedgerTransaction(services: ServiceHub, serializationContext: SerializationContext) = toLedgerTransactionWithContext(services, serializationContext) + +/** Convenience method to get the package name of a class literal. */ +val KClass<*>.packageName get() = java.`package`.name diff --git a/core/src/test/kotlin/net/corda/core/internal/InternalUtilsTest.kt b/core/src/test/kotlin/net/corda/core/internal/InternalUtilsTest.kt index b2f31384db..0a2fb69f26 100644 --- a/core/src/test/kotlin/net/corda/core/internal/InternalUtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/InternalUtilsTest.kt @@ -61,6 +61,7 @@ class InternalUtilsTest { assertArrayEquals(intArrayOf(1, 2, 3, 4), (1 until 5).stream().toArray()) assertArrayEquals(intArrayOf(1, 3), (1..4 step 2).stream().toArray()) assertArrayEquals(intArrayOf(1, 3), (1..3 step 2).stream().toArray()) + @Suppress("EmptyRange") // It's supposed to be empty. assertArrayEquals(intArrayOf(), (1..0).stream().toArray()) assertArrayEquals(intArrayOf(1, 0), (1 downTo 0).stream().toArray()) assertArrayEquals(intArrayOf(3, 1), (3 downTo 0 step 2).stream().toArray()) 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 847675db13..1ec43e7271 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 @@ -2,6 +2,7 @@ package net.corda.docs import net.corda.core.contracts.Amount import net.corda.core.identity.Party +import net.corda.core.internal.packageName import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.finance.* @@ -26,15 +27,12 @@ class CustomVaultQueryTest { @Before fun setup() { - mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance.contracts.asset")) + mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName)) mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) nodeA = mockNet.createPartyNode() nodeB = mockNet.createPartyNode() - nodeA.internals.registerInitiatedFlow(TopupIssuerFlow.TopupIssuer::class.java) nodeA.internals.installCordaService(CustomVaultQuery.Service::class.java) - nodeA.internals.registerCustomSchemas(setOf(CashSchemaV1)) - nodeB.internals.registerCustomSchemas(setOf(CashSchemaV1)) notary = nodeA.services.getDefaultNotary() } 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 97ddbc598c..7a5193c0b0 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 @@ -1,6 +1,7 @@ package net.corda.docs import net.corda.core.identity.Party +import net.corda.core.internal.packageName import net.corda.core.toFuture import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow @@ -24,12 +25,10 @@ class FxTransactionBuildTutorialTest { @Before fun setup() { - mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance.contracts.asset")) + mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName)) mockNet.createNotaryNode(legalName = DUMMY_NOTARY.name) nodeA = mockNet.createPartyNode() nodeB = mockNet.createPartyNode() - nodeA.internals.registerCustomSchemas(setOf(CashSchemaV1)) - nodeB.internals.registerCustomSchemas(setOf(CashSchemaV1)) nodeB.internals.registerInitiatedFlow(ForeignExchangeRemoteFlow::class.java) notary = nodeA.services.getDefaultNotary() } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt index bf4138379d..5fd6b46f21 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt @@ -55,8 +55,7 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() { class DummyServiceHub : MockServices() { override val cordappProvider: CordappProviderImpl - = CordappProviderImpl(CordappLoader.createDevMode(listOf(ISOLATED_CONTRACTS_JAR_PATH))).start(attachments) - + = CordappProviderImpl(CordappLoader.createDevMode(listOf(ISOLATED_CONTRACTS_JAR_PATH)), attachments) private val cordapp get() = cordappProvider.cordapps.first() val attachmentId get() = cordappProvider.getCordappAttachmentId(cordapp)!! val appContext get() = cordappProvider.getAppContext(cordapp) 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 3ea5527202..9d4abec634 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 @@ -37,7 +37,7 @@ import kotlin.test.assertFailsWith class AttachmentLoadingTests : TestDependencyInjectionBase() { private class Services : MockServices() { - private val provider = CordappProviderImpl(CordappLoader.createDevMode(listOf(isolatedJAR))).start(attachments) + private val provider = CordappProviderImpl(CordappLoader.createDevMode(listOf(isolatedJAR)), attachments) private val cordapp get() = provider.cordapps.first() val attachmentId get() = provider.getCordappAttachmentId(cordapp)!! val appContext get() = provider.getAppContext(cordapp) 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 9a7e5cbbeb..16829873aa 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -7,7 +7,6 @@ import net.corda.confidential.SwapIdentitiesFlow import net.corda.confidential.SwapIdentitiesHandler import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture -import net.corda.core.cordapp.CordappProvider import net.corda.core.flows.* import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party @@ -26,7 +25,6 @@ import net.corda.core.node.ServiceHub import net.corda.core.node.StateLoader import net.corda.core.node.services.* import net.corda.core.node.services.NetworkMapCache.MapChange -import net.corda.core.schemas.MappedSchema import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken @@ -37,6 +35,7 @@ import net.corda.node.VersionInfo import net.corda.node.internal.classloading.requireAnnotation import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl +import net.corda.node.internal.cordapp.CordappProviderInternal import net.corda.node.services.ContractUpgradeHandler import net.corda.node.services.FinalityHandler import net.corda.node.services.NotaryChangeHandler @@ -148,7 +147,6 @@ abstract class AbstractNode(config: NodeConfiguration, protected lateinit var network: MessagingService protected val runOnStop = ArrayList<() -> Any?>() protected lateinit var database: CordaPersistence - lateinit var cordappProvider: CordappProviderImpl protected val _nodeReadyFuture = openFuture() /** Completes once the node has successfully registered with the network map service * or has loaded network map data from local database */ @@ -161,7 +159,7 @@ abstract class AbstractNode(config: NodeConfiguration, } open val serializationWhitelists: List by lazy { - cordappProvider.cordapps.flatMap { it.serializationWhitelists } + cordappLoader.cordapps.flatMap { it.serializationWhitelists } } /** Set to non-null once [start] has been successfully called. */ @@ -185,11 +183,12 @@ abstract class AbstractNode(config: NodeConfiguration, validateKeystore() } + private fun makeSchemaService() = NodeSchemaService(cordappLoader) open fun generateNodeInfo() { check(started == null) { "Node has already been started" } initCertificate() log.info("Generating nodeInfo ...") - val schemaService = NodeSchemaService() + val schemaService = makeSchemaService() initialiseDatabasePersistence(schemaService) { makeServices(schemaService) saveOwnNodeInfo() @@ -200,7 +199,7 @@ abstract class AbstractNode(config: NodeConfiguration, check(started == null) { "Node has already been started" } initCertificate() log.info("Node starting up ...") - val schemaService = NodeSchemaService() + val schemaService = makeSchemaService() // Do all of this in a database transaction so anything that might need a connection has one. val startedImpl = initialiseDatabasePersistence(schemaService) { val tokenizableServices = makeServices(schemaService) @@ -231,8 +230,7 @@ abstract class AbstractNode(config: NodeConfiguration, installCordaServices() registerCordappFlows() - _services.rpcFlows += cordappProvider.cordapps.flatMap { it.rpcFlows } - registerCustomSchemas(cordappProvider.cordapps.flatMap { it.customSchemas }.toSet()) + _services.rpcFlows += cordappLoader.cordapps.flatMap { it.rpcFlows } FlowLogicRefFactoryImpl.classloader = cordappLoader.appClassLoader runOnStop += network::stop @@ -254,7 +252,7 @@ abstract class AbstractNode(config: NodeConfiguration, private class ServiceInstantiationException(cause: Throwable?) : CordaException("Service Instantiation Error", cause) private fun installCordaServices() { - val loadedServices = cordappProvider.cordapps.flatMap { it.services } + val loadedServices = cordappLoader.cordapps.flatMap { it.services } filterServicesToInstall(loadedServices).forEach { try { installCordaService(it) @@ -374,7 +372,7 @@ abstract class AbstractNode(config: NodeConfiguration, } private fun registerCordappFlows() { - cordappProvider.cordapps.flatMap { it.initiatedFlows } + cordappLoader.cordapps.flatMap { it.initiatedFlows } .forEach { try { registerInitiatedFlowInternal(it, track = false) @@ -471,11 +469,11 @@ abstract class AbstractNode(config: NodeConfiguration, */ private fun makeServices(schemaService: SchemaService): MutableList { checkpointStorage = DBCheckpointStorage() - cordappProvider = CordappProviderImpl(cordappLoader) val transactionStorage = makeTransactionStorage() - _services = ServiceHubInternalImpl(schemaService, transactionStorage, StateLoaderImpl(transactionStorage)) - attachments = NodeAttachmentService(services.monitoringService.metrics) - cordappProvider.start(attachments) + val metrics = MetricRegistry() + attachments = NodeAttachmentService(metrics) + val cordappProvider = CordappProviderImpl(cordappLoader, attachments) + _services = ServiceHubInternalImpl(schemaService, transactionStorage, StateLoaderImpl(transactionStorage), MonitoringService(metrics), cordappProvider) legalIdentity = obtainIdentity(notaryConfig = null) network = makeMessagingService(legalIdentity) info = makeInfo(legalIdentity) @@ -541,7 +539,7 @@ abstract class AbstractNode(config: NodeConfiguration, protected open fun initialiseDatabasePersistence(schemaService: SchemaService, insideTransaction: () -> T): T { val props = configuration.dataSourceProperties if (props.isNotEmpty()) { - this.database = configureDatabase(props, configuration.database, schemaService, { _services.identityService }) + this.database = configureDatabase(props, configuration.database, { _services.identityService }, schemaService) // Now log the vendor string as this will also cause a connection to be tested eagerly. database.transaction { log.info("Connected to ${database.dataSource.connection.metaData.databaseProductName} database.") @@ -764,12 +762,13 @@ abstract class AbstractNode(config: NodeConfiguration, private inner class ServiceHubInternalImpl( override val schemaService: SchemaService, override val validatedTransactions: WritableTransactionStorage, - private val stateLoader: StateLoader + private val stateLoader: StateLoader, + override val monitoringService: MonitoringService, + override val cordappProvider: CordappProviderInternal ) : SingletonSerializeAsToken(), ServiceHubInternal, StateLoader by stateLoader { override val rpcFlows = ArrayList>>() override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage() override val auditService = DummyAuditService() - override val monitoringService = MonitoringService(MetricRegistry()) override val transactionVerifierService by lazy { makeTransactionVerifierService() } override val networkMapCache by lazy { PersistentNetworkMapCache(this) } override val vaultService by lazy { NodeVaultService(platformClock, keyManagementService, stateLoader, this@AbstractNode.database.hibernateConfig) } @@ -794,8 +793,6 @@ abstract class AbstractNode(config: NodeConfiguration, override val myInfo: NodeInfo get() = info override val database: CordaPersistence get() = this@AbstractNode.database override val configuration: NodeConfiguration get() = this@AbstractNode.configuration - override val cordappProvider: CordappProvider = this@AbstractNode.cordappProvider - override fun cordaService(type: Class): T { require(type.isAnnotationPresent(CordaService::class.java)) { "${type.name} is not a Corda service" } return cordappServices.getInstance(type) ?: throw IllegalArgumentException("Corda service ${type.name} does not exist") @@ -817,9 +814,4 @@ abstract class AbstractNode(config: NodeConfiguration, override fun jdbcSession(): Connection = database.createSession() } - - fun registerCustomSchemas(schemas: Set) { - database.hibernateConfig.schemaService.registerCustomSchemas(schemas) - } - } 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 7257eea773..e76f226adc 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -62,7 +62,7 @@ import kotlin.system.exitProcess * * @param configuration This is typically loaded from a TypeSafe HOCON configuration file. */ -open class Node(override val configuration: FullNodeConfiguration, +open class Node(configuration: FullNodeConfiguration, versionInfo: VersionInfo, val initialiseSerialization: Boolean = true, cordappLoader: CordappLoader = makeCordappLoader(configuration) @@ -99,6 +99,7 @@ open class Node(override val configuration: FullNodeConfiguration, } override val log: Logger get() = logger + override val configuration get() = super.configuration as FullNodeConfiguration // Necessary to avoid init order NPE. override val networkMapAddress: NetworkMapAddress? get() = configuration.networkMapService?.address?.let(::NetworkMapAddress) override fun makeTransactionVerifierService() = (network as NodeMessagingClient).verifierService 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 67a06aa2e9..1776b64271 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -95,7 +95,7 @@ open class NodeStartup(val args: Array) { return } val startedNode = node.start() - Node.printBasicNodeInfo("Loaded CorDapps", startedNode.internals.cordappProvider.cordapps.joinToString { it.name }) + Node.printBasicNodeInfo("Loaded CorDapps", startedNode.services.cordappProvider.cordapps.joinToString { it.name }) startedNode.internals.nodeReadyFuture.thenMatch({ val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0 val name = startedNode.info.legalIdentitiesAndCerts.first().name.organisation diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt index c481bfcc36..5154c592b1 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt @@ -38,10 +38,10 @@ import kotlin.streams.toList * * @property cordappJarPaths The classpath of cordapp JARs */ -class CordappLoader private constructor(private val cordappJarPaths: List) { +class CordappLoader private constructor(private val cordappJarPaths: List) { val cordapps: List by lazy { loadCordapps() + coreCordapp } - internal val appClassLoader: ClassLoader = URLClassLoader(cordappJarPaths.toTypedArray(), javaClass.classLoader) + internal val appClassLoader: ClassLoader = URLClassLoader(cordappJarPaths.stream().map { it.url }.toTypedArray(), javaClass.classLoader) init { if (cordappJarPaths.isEmpty()) { @@ -97,20 +97,22 @@ class CordappLoader private constructor(private val cordappJarPaths: List) * @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection */ @VisibleForTesting - fun createDevMode(scanJars: List) = CordappLoader(scanJars) + fun createDevMode(scanJars: List) = CordappLoader(scanJars.map { RestrictedURL(it, null) }) private fun getCordappsPath(baseDir: Path): Path = baseDir / CORDAPPS_DIR_NAME - private fun createScanPackage(scanPackage: String): List { + private fun createScanPackage(scanPackage: String): List { val resource = scanPackage.replace('.', '/') return this::class.java.classLoader.getResources(resource) .asSequence() .map { path -> if (path.protocol == "jar") { - (path.openConnection() as JarURLConnection).jarFileURL.toURI() + // When running tests from gradle this may be a corda module jar, so restrict to scanPackage: + RestrictedURL((path.openConnection() as JarURLConnection).jarFileURL, scanPackage) } else { - createDevCordappJar(scanPackage, path, resource) - }.toURL() + // No need to restrict as createDevCordappJar has already done that: + RestrictedURL(createDevCordappJar(scanPackage, path, resource).toURL(), null) + } } .toList() } @@ -143,12 +145,12 @@ class CordappLoader private constructor(private val cordappJarPaths: List) return generatedCordapps[path]!! } - private fun getCordappsInDirectory(cordappsDir: Path): List { + private fun getCordappsInDirectory(cordappsDir: Path): List { return if (!cordappsDir.exists()) { - emptyList() + emptyList() } else { cordappsDir.list { - it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.map { it.toUri().toURL() }.toList() + it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.map { RestrictedURL(it.toUri().toURL(), null) }.toList() } } } @@ -187,15 +189,15 @@ class CordappLoader private constructor(private val cordappJarPaths: List) findServices(scanResult), findPlugins(it), findCustomSchemas(scanResult), - it) + it.url) } } - private fun findServices(scanResult: ScanResult): List> { + private fun findServices(scanResult: RestrictedScanResult): List> { return scanResult.getClassesWithAnnotation(SerializeAsToken::class, CordaService::class) } - private fun findInitiatedFlows(scanResult: ScanResult): List>> { + private fun findInitiatedFlows(scanResult: RestrictedScanResult): List>> { return scanResult.getClassesWithAnnotation(FlowLogic::class, InitiatedBy::class) // First group by the initiating flow class in case there are multiple mappings .groupBy { it.requireAnnotation().value.java } @@ -214,35 +216,35 @@ class CordappLoader private constructor(private val cordappJarPaths: List) return Modifier.isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || Modifier.isStatic(modifiers)) } - private fun findRPCFlows(scanResult: ScanResult): List>> { + private fun findRPCFlows(scanResult: RestrictedScanResult): List>> { return scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class).filter { it.isUserInvokable() } } - private fun findServiceFlows(scanResult: ScanResult): List>> { + private fun findServiceFlows(scanResult: RestrictedScanResult): List>> { return scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByService::class) } - private fun findSchedulableFlows(scanResult: ScanResult): List>> { + private fun findSchedulableFlows(scanResult: RestrictedScanResult): List>> { return scanResult.getClassesWithAnnotation(FlowLogic::class, SchedulableFlow::class) } - private fun findContractClassNames(scanResult: ScanResult): List { - return (scanResult.getNamesOfClassesImplementing(Contract::class.java) + scanResult.getNamesOfClassesImplementing(UpgradedContract::class.java)).distinct() + private fun findContractClassNames(scanResult: RestrictedScanResult): List { + return (scanResult.getNamesOfClassesImplementing(Contract::class) + scanResult.getNamesOfClassesImplementing(UpgradedContract::class)).distinct() } - private fun findPlugins(cordappJarPath: URL): List { - return ServiceLoader.load(SerializationWhitelist::class.java, URLClassLoader(arrayOf(cordappJarPath), appClassLoader)).toList().filter { - cordappJarPath == it.javaClass.protectionDomain.codeSource.location + private fun findPlugins(cordappJarPath: RestrictedURL): List { + return ServiceLoader.load(SerializationWhitelist::class.java, URLClassLoader(arrayOf(cordappJarPath.url), appClassLoader)).toList().filter { + it.javaClass.protectionDomain.codeSource.location == cordappJarPath.url && it.javaClass.name.startsWith(cordappJarPath.qualifiedNamePrefix) } + DefaultWhitelist // Always add the DefaultWhitelist to the whitelist for an app. } - private fun findCustomSchemas(scanResult: ScanResult): Set { + private fun findCustomSchemas(scanResult: RestrictedScanResult): Set { return scanResult.getClassesWithSuperclass(MappedSchema::class).toSet() } - private fun scanCordapp(cordappJarPath: URL): ScanResult { + private fun scanCordapp(cordappJarPath: RestrictedURL): RestrictedScanResult { logger.info("Scanning CorDapp in $cordappJarPath") - return FastClasspathScanner().addClassLoader(appClassLoader).overrideClasspath(cordappJarPath).scan() + return RestrictedScanResult(FastClasspathScanner().addClassLoader(appClassLoader).overrideClasspath(cordappJarPath.url).scan(), cordappJarPath.qualifiedNamePrefix) } private class FlowTypeHierarchyComparator(val initiatingFlow: Class>) : Comparator>> { @@ -269,16 +271,30 @@ class CordappLoader private constructor(private val cordappJarPaths: List) } } - private fun ScanResult.getClassesWithSuperclass(type: KClass): List { - return getNamesOfSubclassesOf(type.java) - .mapNotNull { loadClass(it, type) } - .filterNot { Modifier.isAbstract(it.modifiers) } - .map { it.kotlin.objectOrNewInstance() } + /** @param rootPackageName only this package and subpackages may be extracted from [url], or null to allow all packages. */ + private class RestrictedURL(val url: URL, rootPackageName: String?) { + val qualifiedNamePrefix = rootPackageName?.let { it + '.' } ?: "" } - private fun ScanResult.getClassesWithAnnotation(type: KClass, annotation: KClass): List> { - return getNamesOfClassesWithAnnotation(annotation.java) - .mapNotNull { loadClass(it, type) } - .filterNot { Modifier.isAbstract(it.modifiers) } + private inner class RestrictedScanResult(private val scanResult: ScanResult, private val qualifiedNamePrefix: String) { + fun getNamesOfClassesImplementing(type: KClass<*>): List { + return scanResult.getNamesOfClassesImplementing(type.java) + .filter { it.startsWith(qualifiedNamePrefix) } + } + + fun getClassesWithSuperclass(type: KClass): List { + return scanResult.getNamesOfSubclassesOf(type.java) + .filter { it.startsWith(qualifiedNamePrefix) } + .mapNotNull { loadClass(it, type) } + .filterNot { Modifier.isAbstract(it.modifiers) } + .map { it.kotlin.objectOrNewInstance() } + } + + fun getClassesWithAnnotation(type: KClass, annotation: KClass): List> { + return scanResult.getNamesOfClassesWithAnnotation(annotation.java) + .filter { it.startsWith(qualifiedNamePrefix) } + .mapNotNull { loadClass(it, type) } + .filterNot { Modifier.isAbstract(it.modifiers) } + } } } 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 967c332fd3..ba305283f2 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 @@ -6,15 +6,14 @@ import net.corda.core.crypto.SecureHash import net.corda.core.node.services.AttachmentStorage import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.CordappContext -import net.corda.core.cordapp.CordappProvider import net.corda.core.node.services.AttachmentId import net.corda.core.serialization.SingletonSerializeAsToken -import java.net.URLClassLoader +import java.net.URL /** * Cordapp provider and store. For querying CorDapps for their attachment and vice versa. */ -open class CordappProviderImpl(private val cordappLoader: CordappLoader) : SingletonSerializeAsToken(), CordappProvider { +open class CordappProviderImpl(private val cordappLoader: CordappLoader, attachmentStorage: AttachmentStorage) : SingletonSerializeAsToken(), CordappProviderInternal { override fun getAppContext(): CordappContext { // TODO: Use better supported APIs in Java 9 Exception().stackTrace.forEach { stackFrame -> @@ -34,28 +33,19 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader) : Singl /** * Current known CorDapps loaded on this node */ - val cordapps get() = cordappLoader.cordapps - private lateinit var cordappAttachments: HashBiMap - - /** - * Should only be called once from the initialisation routine of the node or tests - */ - fun start(attachmentStorage: AttachmentStorage): CordappProviderImpl { - cordappAttachments = HashBiMap.create(loadContractsIntoAttachmentStore(attachmentStorage)) - return this - } - + override val cordapps get() = cordappLoader.cordapps + private val cordappAttachments = HashBiMap.create(loadContractsIntoAttachmentStore(attachmentStorage)) /** * Gets the attachment ID of this CorDapp. Only CorDapps with contracts have an attachment ID * * @param cordapp The cordapp to get the attachment ID * @return An attachment ID if it exists, otherwise nothing */ - fun getCordappAttachmentId(cordapp: Cordapp): SecureHash? = cordappAttachments.inverse().get(cordapp) + fun getCordappAttachmentId(cordapp: Cordapp): SecureHash? = cordappAttachments.inverse().get(cordapp.jarPath) - private fun loadContractsIntoAttachmentStore(attachmentStorage: AttachmentStorage): Map { - val cordappsWithAttachments = cordapps.filter { !it.contractClassNames.isEmpty() } - val attachmentIds = cordappsWithAttachments.map { it.jarPath.openStream().use { attachmentStorage.importAttachment(it) } } + private fun loadContractsIntoAttachmentStore(attachmentStorage: AttachmentStorage): Map { + val cordappsWithAttachments = cordapps.filter { !it.contractClassNames.isEmpty() }.map { it.jarPath } + val attachmentIds = cordappsWithAttachments.map { it.openStream().use { attachmentStorage.importAttachment(it) } } return attachmentIds.zip(cordappsWithAttachments).toMap() } 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 new file mode 100644 index 0000000000..a29d8bab25 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderInternal.kt @@ -0,0 +1,8 @@ +package net.corda.node.internal.cordapp + +import net.corda.core.cordapp.Cordapp +import net.corda.core.cordapp.CordappProvider + +interface CordappProviderInternal : CordappProvider { + val cordapps: List +} diff --git a/node/src/main/kotlin/net/corda/node/services/api/SchemaService.kt b/node/src/main/kotlin/net/corda/node/services/api/SchemaService.kt index 7428a2e90a..4a4d815708 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/SchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/SchemaService.kt @@ -30,11 +30,5 @@ interface SchemaService { * or via custom logic in this service. */ fun generateMappedObject(state: ContractState, schema: MappedSchema): PersistentState - - /** - * Registration mechanism to add custom contract schemas that extend the [MappedSchema] class. - */ - fun registerCustomSchemas(customSchemas: Set) - } //DOCEND SchemaService 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 65bd38d843..5fdb7993a0 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 @@ -21,6 +21,7 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.loggerFor import net.corda.node.internal.InitiatedFlowFactory +import net.corda.node.internal.cordapp.CordappProviderInternal import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl @@ -89,7 +90,7 @@ interface ServiceHubInternal : ServiceHub { val networkService: MessagingService val database: CordaPersistence val configuration: NodeConfiguration - + override val cordappProvider: CordappProviderInternal override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { require(txs.any()) { "No transactions passed in for recording" } val recordedTransactions = txs.filter { validatedTransactions.addTransaction(it) } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/HibernateConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/persistence/HibernateConfiguration.kt index bf6efb6c8c..8fb0023b3d 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/HibernateConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/HibernateConfiguration.kt @@ -33,25 +33,13 @@ class HibernateConfiguration(val schemaService: SchemaService, private val datab private val sessionFactories = ConcurrentHashMap, SessionFactory>() private val transactionIsolationLevel = parserTransactionIsolationLevel(databaseProperties.getProperty("transactionIsolationLevel") ?: "") - - init { - logger.info("Init HibernateConfiguration for schemas: ${schemaService.schemaOptions.keys}") - sessionFactoryForRegisteredSchemas() + val sessionFactoryForRegisteredSchemas = schemaService.schemaOptions.keys.let { + logger.info("Init HibernateConfiguration for schemas: $it") + sessionFactoryForSchemas(it) } - fun sessionFactoryForRegisteredSchemas(): SessionFactory { - return sessionFactoryForSchemas(*schemaService.schemaOptions.keys.toTypedArray()) - } - - fun sessionFactoryForSchema(schema: MappedSchema): SessionFactory { - return sessionFactoryForSchemas(schema) - } - - //vararg to set conversions left to preserve method signature for now - fun sessionFactoryForSchemas(vararg schemas: MappedSchema): SessionFactory { - val schemaSet: Set = schemas.toSet() - return sessionFactories.computeIfAbsent(schemaSet, { makeSessionFactoryForSchemas(schemaSet) }) - } + /** @param key must be immutable, not just read-only. */ + fun sessionFactoryForSchemas(key: Set) = sessionFactories.computeIfAbsent(key, { makeSessionFactoryForSchemas(key) }) private fun makeSessionFactoryForSchemas(schemas: Set): SessionFactory { logger.info("Creating session factory for schemas: $schemas") diff --git a/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt b/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt index 9afc740949..45a3334ef1 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt @@ -38,7 +38,7 @@ class HibernateObserver(vaultUpdates: Observable>, v } fun persistStateWithSchema(state: ContractState, stateRef: StateRef, schema: MappedSchema) { - val sessionFactory = config.sessionFactoryForSchema(schema) + val sessionFactory = config.sessionFactoryForSchemas(setOf(schema)) val session = sessionFactory.withOptions(). connection(DatabaseTransactionManager.current().connection). flushMode(FlushMode.MANUAL). 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 0599616644..8436f9459b 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 @@ -9,6 +9,7 @@ import net.corda.core.schemas.NodeInfoSchemaV1 import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.services.api.SchemaService import net.corda.node.services.events.NodeSchedulerService import net.corda.node.services.identity.PersistentIdentityService @@ -27,13 +28,13 @@ import net.corda.node.services.vault.VaultSchemaV1 /** * Most basic implementation of [SchemaService]. - * + * @param cordappLoader if not null, custom schemas will be extracted from its cordapps. * TODO: support loading schema options from node configuration. * TODO: support configuring what schemas are to be selected for persistence. * TODO: support plugins for schema version upgrading or custom mapping not supported by original [QueryableState]. * TODO: create whitelisted tables when a CorDapp is first installed */ -class NodeSchemaService(customSchemas: Set = emptySet()) : SchemaService, SingletonSerializeAsToken() { +class NodeSchemaService(cordappLoader: CordappLoader?) : SchemaService, SingletonSerializeAsToken() { // Entities for compulsory services object NodeServices @@ -67,9 +68,12 @@ class NodeSchemaService(customSchemas: Set = emptySet()) : SchemaS Pair(NodeInfoSchemaV1, SchemaService.SchemaOptions()), Pair(NodeServicesV1, SchemaService.SchemaOptions())) - override var schemaOptions: Map = requiredSchemas.plus(customSchemas.map { mappedSchema -> - Pair(mappedSchema, SchemaService.SchemaOptions()) - }) + override val schemaOptions: Map = if (cordappLoader == null) { + requiredSchemas + } else { + val customSchemas = cordappLoader.cordapps.flatMap { it.customSchemas }.toSet() + requiredSchemas.plus(customSchemas.map { mappedSchema -> Pair(mappedSchema, SchemaService.SchemaOptions()) }) + } // Currently returns all schemas supported by the state, with no filtering or enrichment. override fun selectSchemas(state: ContractState): Iterable { @@ -92,10 +96,4 @@ class NodeSchemaService(customSchemas: Set = emptySet()) : SchemaS return VaultSchemaV1.VaultFungibleStates(state.owner, state.amount.quantity, state.amount.token.issuer.party, state.amount.token.issuer.reference, state.participants) return (state as QueryableState).generateMappedObject(schema) } - - override fun registerCustomSchemas(_customSchemas: Set) { - schemaOptions = schemaOptions.plus(_customSchemas.map { mappedSchema -> - Pair(mappedSchema, SchemaService.SchemaOptions()) - }) - } } 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 864da2bd50..d5a189b0ee 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 @@ -59,7 +59,7 @@ private fun CriteriaBuilder.executeUpdate(session: Session, configure: Root<*>.( * TODO: keep an audit trail with time stamps of previously unconsumed states "as of" a particular point in time. * TODO: have transaction storage do some caching. */ -class NodeVaultService(private val clock: Clock, private val keyManagementService: KeyManagementService, private val stateLoader: StateLoader, private val hibernateConfig: HibernateConfiguration) : SingletonSerializeAsToken(), VaultServiceInternal { +class NodeVaultService(private val clock: Clock, private val keyManagementService: KeyManagementService, private val stateLoader: StateLoader, hibernateConfig: HibernateConfiguration) : SingletonSerializeAsToken(), VaultServiceInternal { private companion object { val log = loggerFor() @@ -377,9 +377,8 @@ class NodeVaultService(private val clock: Clock, private val keyManagementServic return keysToCheck.any { it in myKeys } } - private var sessionFactory = hibernateConfig.sessionFactoryForRegisteredSchemas() - private var criteriaBuilder = sessionFactory.criteriaBuilder - + private val sessionFactory = hibernateConfig.sessionFactoryForRegisteredSchemas + private val criteriaBuilder = sessionFactory.criteriaBuilder /** * Maintain a list of contract state interfaces to concrete types stored in the vault * for usage in generic queries of type queryBy or queryBy> @@ -406,11 +405,6 @@ class NodeVaultService(private val clock: Clock, private val keyManagementServic @Throws(VaultQueryException::class) override fun _queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class): Vault.Page { log.info("Vault Query for contract type: $contractStateType, criteria: $criteria, pagination: $paging, sorting: $sorting") - - // refresh to include any schemas registered after initial VQ service initialisation - sessionFactory = hibernateConfig.sessionFactoryForRegisteredSchemas() - criteriaBuilder = sessionFactory.criteriaBuilder - // calculate total results where a page specification has been defined var totalStates = -1L if (!paging.isDefault) { diff --git a/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt b/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt index 0bf9885945..b8fc4f333d 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt @@ -6,8 +6,6 @@ import net.corda.core.node.services.IdentityService import net.corda.node.services.api.SchemaService import net.corda.node.services.persistence.HibernateConfiguration import net.corda.node.services.schema.NodeSchemaService -import org.hibernate.SessionFactory - import rx.Observable import rx.Subscriber import rx.subjects.UnicastSubject @@ -32,12 +30,7 @@ class CordaPersistence(var dataSource: HikariDataSource, private val schemaServi HibernateConfiguration(schemaService, databaseProperties, createIdentityService) } } - - val entityManagerFactory: SessionFactory by lazy { - transaction { - hibernateConfig.sessionFactoryForRegisteredSchemas() - } - } + val entityManagerFactory get() = hibernateConfig.sessionFactoryForRegisteredSchemas companion object { fun connect(dataSource: HikariDataSource, schemaService: SchemaService, createIdentityService: () -> IdentityService, databaseProperties: Properties): CordaPersistence { @@ -103,7 +96,7 @@ class CordaPersistence(var dataSource: HikariDataSource, private val schemaServi } } -fun configureDatabase(dataSourceProperties: Properties, databaseProperties: Properties?, schemaService: SchemaService = NodeSchemaService(), createIdentityService: () -> IdentityService): CordaPersistence { +fun configureDatabase(dataSourceProperties: Properties, databaseProperties: Properties?, createIdentityService: () -> IdentityService, schemaService: SchemaService = NodeSchemaService(null)): CordaPersistence { val config = HikariConfig(dataSourceProperties) val dataSource = HikariDataSource(config) val persistence = CordaPersistence.connect(dataSource, schemaService, createIdentityService, databaseProperties ?: Properties()) diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index 0568b45a3e..2ad7697d20 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -8,7 +8,6 @@ import net.corda.core.messaging.*; import net.corda.core.node.services.*; import net.corda.core.node.services.vault.*; import net.corda.core.node.services.vault.QueryCriteria.*; -import net.corda.core.schemas.*; import net.corda.core.utilities.*; import net.corda.finance.contracts.*; import net.corda.finance.contracts.asset.*; @@ -43,14 +42,13 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { @Before public void setUp() { - List cordappPackages = Arrays.asList("net.corda.testing.contracts", "net.corda.finance.contracts.asset"); + List cordappPackages = Arrays.asList("net.corda.testing.contracts", "net.corda.finance.contracts.asset", CashSchemaV1.class.getPackage().getName()); ArrayList keys = new ArrayList<>(); keys.add(getMEGA_CORP_KEY()); keys.add(getDUMMY_NOTARY_KEY()); - Set requiredSchemas = Collections.singleton(CashSchemaV1.INSTANCE); IdentityService identitySvc = makeTestIdentityService(); @SuppressWarnings("unchecked") - Pair databaseAndServices = makeTestDatabaseAndMockServices(requiredSchemas, keys, () -> identitySvc, cordappPackages); + Pair databaseAndServices = makeTestDatabaseAndMockServices(keys, () -> identitySvc, cordappPackages); issuerServices = new MockServices(cordappPackages, getDUMMY_CASH_ISSUER_KEY(), getBOC_KEY()); database = databaseAndServices.getFirst(); services = databaseAndServices.getSecond(); diff --git a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt index 3d4005d6fc..54117732cf 100644 --- a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt +++ b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt @@ -26,7 +26,7 @@ import kotlin.test.assertEquals class InteractiveShellTest { @Before fun setup() { - InteractiveShell.database = configureDatabase(MockServices.makeTestDataSourceProperties(), MockServices.makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) + InteractiveShell.database = configureDatabase(MockServices.makeTestDataSourceProperties(), MockServices.makeTestDatabaseProperties(), ::makeTestIdentityService) } @After 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 877037c387..9f7833f928 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 @@ -1,5 +1,6 @@ package net.corda.node.internal.cordapp +import com.nhaarman.mockito_kotlin.mock import net.corda.core.node.services.AttachmentStorage import net.corda.testing.node.MockAttachmentStorage import org.junit.Assert @@ -22,9 +23,7 @@ class CordappProviderImplTests { @Test fun `isolated jar is loaded into the attachment store`() { val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) - val provider = CordappProviderImpl(loader) - - provider.start(attachmentStore) + val provider = CordappProviderImpl(loader, attachmentStore) val maybeAttachmentId = provider.getCordappAttachmentId(provider.cordapps.first()) Assert.assertNotNull(maybeAttachmentId) @@ -34,17 +33,14 @@ class CordappProviderImplTests { @Test fun `empty jar is not loaded into the attachment store`() { val loader = CordappLoader.createDevMode(listOf(emptyJAR)) - val provider = CordappProviderImpl(loader) - - provider.start(attachmentStore) - + val provider = CordappProviderImpl(loader, attachmentStore) Assert.assertNull(provider.getCordappAttachmentId(provider.cordapps.first())) } @Test fun `test that we find a cordapp class that is loaded into the store`() { val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) - val provider = CordappProviderImpl(loader) + val provider = CordappProviderImpl(loader, mock()) val className = "net.corda.finance.contracts.isolated.AnotherDummyContract" val expected = provider.cordapps.first() @@ -57,10 +53,8 @@ class CordappProviderImplTests { @Test fun `test that we find an attachment for a cordapp contrat class`() { val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) - val provider = CordappProviderImpl(loader) + val provider = CordappProviderImpl(loader, attachmentStore) val className = "net.corda.finance.contracts.isolated.AnotherDummyContract" - - provider.start(attachmentStore) val expected = provider.getAppContext(provider.cordapps.first()).attachmentId val actual = provider.getContractAttachmentID(className) diff --git a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt index d5c966d31c..d54bfa8754 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt @@ -78,7 +78,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { calls = 0 val dataSourceProps = makeTestDataSourceProperties() val databaseProperties = makeTestDatabaseProperties() - database = configureDatabase(dataSourceProps, databaseProperties, createIdentityService = ::makeTestIdentityService) + database = configureDatabase(dataSourceProps, databaseProperties, ::makeTestIdentityService) val identityService = InMemoryIdentityService(trustRoot = DEV_TRUST_ROOT) val kms = MockKeyManagementService(identityService, ALICE_KEY) @@ -97,7 +97,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { network = mockMessagingService), TestReference { override val vaultService: VaultServiceInternal = NodeVaultService(testClock, kms, stateLoader, database.hibernateConfig) override val testReference = this@NodeSchedulerServiceTest - override val cordappProvider = CordappProviderImpl(CordappLoader.createWithTestPackages(listOf("net.corda.testing.contracts"))).start(attachments) + override val cordappProvider = CordappProviderImpl(CordappLoader.createWithTestPackages(listOf("net.corda.testing.contracts")), attachments) } smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1) scheduler = NodeSchedulerService(services, schedulerGatedExecutor, serverThread = smmExecutor) diff --git a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt index 6a1adee86a..418322b589 100644 --- a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt @@ -71,7 +71,7 @@ class ArtemisMessagingTests : TestDependencyInjectionBase() { baseDirectory = baseDirectory, myLegalName = ALICE.name) LogHelper.setLevel(PersistentUniquenessProvider::class) - database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) + database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), ::makeTestIdentityService) networkMapRegistrationFuture = doneFuture(Unit) networkMapCache = PersistentNetworkMapCache(serviceHub = object : MockServiceHubInternal(database, config) {}) } 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 aaa8e49db1..f71f266388 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 @@ -33,7 +33,7 @@ class DBCheckpointStorageTests : TestDependencyInjectionBase() { @Before fun setUp() { LogHelper.setLevel(PersistentUniquenessProvider::class) - database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) + database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), ::makeTestIdentityService) newCheckpointStorage() } 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 3a3148c0db..2b9ff96f1c 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 @@ -38,7 +38,7 @@ class DBTransactionStorageTests : TestDependencyInjectionBase() { fun setUp() { LogHelper.setLevel(PersistentUniquenessProvider::class) val dataSourceProps = makeTestDataSourceProperties() - database = configureDatabase(dataSourceProps, makeTestDatabaseProperties(), NodeSchemaService(), ::makeTestIdentityService) + database = configureDatabase(dataSourceProps, makeTestDatabaseProperties(), ::makeTestIdentityService) database.transaction { services = object : MockServices(BOB_KEY) { diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt index f0a381c411..0e1cdc1047 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt @@ -9,6 +9,7 @@ import net.corda.core.utilities.toBase58String import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultService import net.corda.core.schemas.CommonSchemaV1 +import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.deserialize @@ -25,7 +26,6 @@ import net.corda.finance.schemas.SampleCashSchemaV2 import net.corda.finance.schemas.SampleCashSchemaV3 import net.corda.finance.utils.sumCash import net.corda.node.services.schema.HibernateObserver -import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.vault.VaultSchemaV1 import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase @@ -77,7 +77,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { issuerServices = MockServices(cordappPackages, DUMMY_CASH_ISSUER_KEY, BOB_KEY, BOC_KEY) val dataSourceProps = makeTestDataSourceProperties() val defaultDatabaseProperties = makeTestDatabaseProperties() - database = configureDatabase(dataSourceProps, defaultDatabaseProperties, NodeSchemaService(), ::makeTestIdentityService) + database = configureDatabase(dataSourceProps, defaultDatabaseProperties, ::makeTestIdentityService) database.transaction { hibernateConfig = database.hibernateConfig services = object : MockServices(cordappPackages, BOB_KEY, BOC_KEY, DUMMY_NOTARY_KEY) { @@ -95,13 +95,13 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { hibernatePersister = services.hibernatePersister } setUpDb() - - val customSchemas = setOf(VaultSchemaV1, CashSchemaV1, SampleCashSchemaV2, SampleCashSchemaV3) - sessionFactory = hibernateConfig.sessionFactoryForSchemas(*customSchemas.toTypedArray()) + sessionFactory = sessionFactoryForSchemas(VaultSchemaV1, CashSchemaV1, SampleCashSchemaV2, SampleCashSchemaV3) entityManager = sessionFactory.createEntityManager() criteriaBuilder = sessionFactory.criteriaBuilder } + private fun sessionFactoryForSchemas(vararg schemas: MappedSchema) = hibernateConfig.sessionFactoryForSchemas(schemas.toSet()) + @After fun cleanUp() { database.close() @@ -536,8 +536,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { services.fillWithSomeTestDeals(listOf("123", "456", "789")) services.fillWithSomeTestLinearStates(2) } - - val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV1) + val sessionFactory = sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV1) val criteriaBuilder = sessionFactory.criteriaBuilder val entityManager = sessionFactory.createEntityManager() @@ -568,8 +567,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { services.fillWithSomeTestDeals(listOf("123", "456", "789")) services.fillWithSomeTestLinearStates(2) } - - val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV2) + val sessionFactory = sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV2) val criteriaBuilder = sessionFactory.criteriaBuilder val entityManager = sessionFactory.createEntityManager() @@ -635,8 +633,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) } } - - val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, CommonSchemaV1, SampleCashSchemaV3) + val sessionFactory = sessionFactoryForSchemas(VaultSchemaV1, CommonSchemaV1, SampleCashSchemaV3) val criteriaBuilder = sessionFactory.criteriaBuilder val entityManager = sessionFactory.createEntityManager() @@ -764,8 +761,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { services.fillWithSomeTestLinearStates(2, externalId = "222") services.fillWithSomeTestLinearStates(3, externalId = "333") } - - val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV2) + val sessionFactory = sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV2) val criteriaBuilder = sessionFactory.criteriaBuilder val entityManager = sessionFactory.createEntityManager() @@ -817,8 +813,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { services.fillWithSomeTestLinearStates(2, externalId = "222") services.fillWithSomeTestLinearStates(3, externalId = "333") } - - val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV1) + val sessionFactory = sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV1) val criteriaBuilder = sessionFactory.criteriaBuilder val entityManager = sessionFactory.createEntityManager() diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt index d3ee944069..a8a352cbf1 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt @@ -40,7 +40,7 @@ class NodeAttachmentStorageTest { LogHelper.setLevel(PersistentUniquenessProvider::class) val dataSourceProperties = makeTestDataSourceProperties() - database = configureDatabase(dataSourceProperties, makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) + database = configureDatabase(dataSourceProperties, makeTestDatabaseProperties(), ::makeTestIdentityService) fs = Jimfs.newFileSystem(Configuration.unix()) } diff --git a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt index 44fc3cb47b..33e8bcdad4 100644 --- a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt @@ -57,8 +57,6 @@ class HibernateObserverTests { val testSchema = TestSchema val rawUpdatesPublisher = PublishSubject.create>() val schemaService = object : SchemaService { - override fun registerCustomSchemas(customSchemas: Set) {} - override val schemaOptions: Map = emptyMap() override fun selectSchemas(state: ContractState): Iterable = setOf(testSchema) @@ -70,7 +68,7 @@ class HibernateObserverTests { return parent } } - val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), schemaService, createIdentityService = ::makeTestIdentityService) + val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), ::makeTestIdentityService, schemaService) @Suppress("UNUSED_VARIABLE") val observer = HibernateObserver(rawUpdatesPublisher, database.hibernateConfig) database.transaction { diff --git a/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt index 73bb252af5..be168e8b98 100644 --- a/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt @@ -3,11 +3,13 @@ package net.corda.node.services.schema import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC +import net.corda.core.internal.packageName import net.corda.core.messaging.startFlow import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.utilities.getOrThrow import net.corda.node.services.api.ServiceHubInternal +import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver import net.corda.testing.node.MockNetwork import net.corda.testing.schemas.DummyLinearStateSchemaV1 @@ -15,6 +17,7 @@ import org.hibernate.annotations.Cascade import org.hibernate.annotations.CascadeType import org.junit.Test import javax.persistence.* +import kotlin.test.assertEquals import kotlin.test.assertTrue class NodeSchemaServiceTest { @@ -23,11 +26,9 @@ class NodeSchemaServiceTest { */ @Test fun `registering custom schemas for testing with MockNode`() { - val mockNet = MockNetwork() + val mockNet = MockNetwork(cordappPackages = listOf(DummyLinearStateSchemaV1::class.packageName)) val mockNode = mockNet.createNode() mockNet.runNetwork() - - mockNode.internals.registerCustomSchemas(setOf(DummyLinearStateSchemaV1)) val schemaService = mockNode.services.schemaService assertTrue(schemaService.schemaOptions.containsKey(DummyLinearStateSchemaV1)) @@ -50,6 +51,16 @@ class NodeSchemaServiceTest { } } + @Test + fun `custom schemas are loaded eagerly`() { + val expected = setOf("PARENTS", "CHILDREN") + assertEquals>(expected, driver { + (startNode(startInSameProcess = true).getOrThrow() as NodeHandle.InProcess).node.database.transaction { + session.createNativeQuery("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES").list() + } + }.toMutableSet().apply { retainAll(expected) }) + } + @StartableByRPC class MappedSchemasFlow : FlowLogic>() { @Suspendable diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt index bd4c3358f3..ce79064a79 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt @@ -86,7 +86,7 @@ class DistributedImmutableMapTests : TestDependencyInjectionBase() { private fun createReplica(myAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): CompletableFuture { val storage = Storage.builder().withStorageLevel(StorageLevel.MEMORY).build() val address = Address(myAddress.host, myAddress.port) - val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties("serverNameTablePrefix", "PORT_${myAddress.port}_"), createIdentityService = ::makeTestIdentityService) + val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties("serverNameTablePrefix", "PORT_${myAddress.port}_"), ::makeTestIdentityService) databases.add(database) val stateMachineFactory = { DistributedImmutableMap(database, RaftUniquenessProvider.Companion::createMap) } 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 491931ab2a..088a14e18f 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 @@ -23,7 +23,7 @@ class PersistentUniquenessProviderTests : TestDependencyInjectionBase() { @Before fun setUp() { LogHelper.setLevel(PersistentUniquenessProvider::class) - database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) + database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), ::makeTestIdentityService) } @After 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 4740ac776d..46802e8a7f 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 @@ -9,6 +9,7 @@ import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party +import net.corda.core.internal.packageName import net.corda.core.node.services.* import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.QueryCriteria.* @@ -46,7 +47,7 @@ import kotlin.test.assertTrue class NodeVaultServiceTest : TestDependencyInjectionBase() { companion object { - private val cordappPackages = listOf("net.corda.finance.contracts.asset") + private val cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName) } lateinit var services: MockServices @@ -58,7 +59,6 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { fun setUp() { LogHelper.setLevel(NodeVaultService::class) val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(BOC_KEY, DUMMY_CASH_ISSUER_KEY), - customSchemas = setOf(CashSchemaV1), cordappPackages = cordappPackages) database = databaseAndServices.first services = databaseAndServices.second 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 56d6e90992..8d34babb97 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 @@ -6,6 +6,7 @@ import net.corda.core.crypto.entropyToKeyPair import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate +import net.corda.core.internal.packageName import net.corda.core.node.services.* import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.* @@ -45,10 +46,9 @@ import java.time.temporal.ChronoUnit import java.util.* class VaultQueryTests : TestDependencyInjectionBase() { - companion object { - private val cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts") - } - + private val cordappPackages = setOf( + "net.corda.testing.contracts", "net.corda.finance.contracts", + CashSchemaV1::class.packageName, CommercialPaperSchemaV1::class.packageName, DummyLinearStateSchemaV1::class.packageName).toMutableList() private lateinit var services: MockServices private lateinit var notaryServices: MockServices private val vaultService: VaultService get() = services.vaultService @@ -67,7 +67,6 @@ class VaultQueryTests : TestDependencyInjectionBase() { identitySvc.verifyAndRegisterIdentity(BOC_IDENTITY) val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(MEGA_CORP_KEY, DUMMY_NOTARY_KEY), createIdentityService = { identitySvc }, - customSchemas = setOf(CashSchemaV1, CommercialPaperSchemaV1, DummyLinearStateSchemaV1), cordappPackages = cordappPackages) database = databaseAndServices.first services = databaseAndServices.second @@ -85,8 +84,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Ignore @Test fun createPersistentTestDb() { - val database = configureDatabase(makePersistentDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = { identitySvc }) - + val database = configureDatabase(makePersistentDataSourceProperties(), makeTestDatabaseProperties(), { identitySvc }) setUpDb(database, 5000) database.close() @@ -1753,6 +1751,9 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `query attempting to use unregistered schema`() { + tearDown() + cordappPackages -= SampleCashSchemaV3::class.packageName + setUp() database.transaction { services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index a42e13d55c..feb6530062 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -4,6 +4,7 @@ import net.corda.core.contracts.ContractState import net.corda.core.contracts.LinearState import net.corda.core.contracts.UniqueIdentifier import net.corda.core.identity.AnonymousParty +import net.corda.core.internal.packageName import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultService import net.corda.core.node.services.queryBy @@ -35,7 +36,7 @@ import kotlin.test.assertEquals class VaultWithCashTest : TestDependencyInjectionBase() { companion object { - private val cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts.asset") + private val cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts.asset", CashSchemaV1::class.packageName) } lateinit var services: MockServices @@ -48,7 +49,6 @@ class VaultWithCashTest : TestDependencyInjectionBase() { fun setUp() { LogHelper.setLevel(VaultWithCashTest::class) val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(DUMMY_CASH_ISSUER_KEY, DUMMY_NOTARY_KEY), - customSchemas = setOf(CashSchemaV1), cordappPackages = cordappPackages) database = databaseAndServices.first services = databaseAndServices.second diff --git a/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt b/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt index ffac359f49..e7b9ca1623 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt @@ -21,7 +21,7 @@ class ObservablesTests { val toBeClosed = mutableListOf() fun createDatabase(): CordaPersistence { - val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) + val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), ::makeTestIdentityService) toBeClosed += database return database } diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index 4e9d129c26..d43192cdf8 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -62,7 +62,7 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { @Before fun setUp() { - database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) + database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), ::makeTestIdentityService) database.transaction { oracle = createMockCordaService(services, NodeInterestRates::Oracle) oracle.knownFixes = TEST_DATA diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt index 90a3ca2d8b..b9abcd6538 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt @@ -12,7 +12,6 @@ import net.corda.node.internal.StartedNode import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.statemachine.StateMachineManager import net.corda.nodeapi.internal.ServiceInfo -import net.corda.nodeapi.internal.ServiceType import net.corda.testing.* import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.MockNetwork @@ -270,9 +269,3 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, mockNet.stopNodes() } } - -/** - * Helper function for verifying that a service info contains the given type of advertised service. For non-simulation cases - * this is a configuration matter rather than implementation. - */ -fun Iterable.containsType(type: ServiceType) = any { it.type == type } \ No newline at end of file diff --git a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt index 6fb3556c25..61580c8632 100644 --- a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt +++ b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt @@ -1,6 +1,7 @@ package net.corda.traderdemo import net.corda.client.rpc.CordaRPCClient +import net.corda.core.internal.packageName import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.millis import net.corda.finance.DOLLARS @@ -20,7 +21,9 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.Test import java.util.concurrent.Executors -class TraderDemoTest : NodeBasedTest(listOf("net.corda.finance.contracts.asset", "net.corda.finance.contracts")) { +class TraderDemoTest : NodeBasedTest(listOf( + "net.corda.finance.contracts.asset", "net.corda.finance.contracts", + CashSchemaV1::class.packageName, CommercialPaperSchemaV1::class.packageName)) { @Test fun `runs trader demo`() { val demoUser = User("demo", "demo", setOf(startFlowPermission())) @@ -35,9 +38,6 @@ class TraderDemoTest : NodeBasedTest(listOf("net.corda.finance.contracts.asset", val (nodeA, nodeB, bankNode) = listOf(nodeAFuture, nodeBFuture, bankNodeFuture, notaryFuture).map { it.getOrThrow() } nodeA.internals.registerInitiatedFlow(BuyerFlow::class.java) - nodeA.internals.registerCustomSchemas(setOf(CashSchemaV1)) - nodeB.internals.registerCustomSchemas(setOf(CashSchemaV1, CommercialPaperSchemaV1)) - val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map { val client = CordaRPCClient(it.internals.configuration.rpcAddress!!) client.start(demoUser.username, demoUser.password).proxy diff --git a/testing/node-driver/src/main/kotlin/net/corda/node/testing/MockServiceHubInternal.kt b/testing/node-driver/src/main/kotlin/net/corda/node/testing/MockServiceHubInternal.kt index c4ca2077cf..1188e1571e 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/node/testing/MockServiceHubInternal.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/node/testing/MockServiceHubInternal.kt @@ -1,7 +1,6 @@ package net.corda.node.testing import com.codahale.metrics.MetricRegistry -import net.corda.core.cordapp.CordappProvider import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic import net.corda.core.identity.Party @@ -13,11 +12,11 @@ import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.internal.StateLoaderImpl import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl +import net.corda.node.internal.cordapp.CordappProviderInternal import net.corda.node.serialization.NodeClock import net.corda.node.services.api.* import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService -import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.transactions.InMemoryTransactionVerifierService @@ -46,10 +45,9 @@ open class MockServiceHubInternal( val mapCache: NetworkMapCacheInternal? = null, val scheduler: SchedulerService? = null, val overrideClock: Clock? = NodeClock(), - val schemas: SchemaService? = NodeSchemaService(), val customContractUpgradeService: ContractUpgradeService? = null, val customTransactionVerifierService: TransactionVerifierService? = InMemoryTransactionVerifierService(2), - override val cordappProvider: CordappProvider = CordappProviderImpl(CordappLoader.createDefault(Paths.get("."))).start(attachments), + override val cordappProvider: CordappProviderInternal = CordappProviderImpl(CordappLoader.createDefault(Paths.get(".")), attachments), protected val stateLoader: StateLoaderImpl = StateLoaderImpl(validatedTransactions) ) : ServiceHubInternal, StateLoader by stateLoader { override val transactionVerifierService: TransactionVerifierService @@ -75,8 +73,7 @@ open class MockServiceHubInternal( override val monitoringService: MonitoringService = MonitoringService(MetricRegistry()) override val rpcFlows: List>> get() = throw UnsupportedOperationException() - override val schemaService: SchemaService - get() = schemas ?: throw UnsupportedOperationException() + override val schemaService get() = throw UnsupportedOperationException() override val auditService: AuditService = DummyAuditService() lateinit var smm: StateMachineManager 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 47fd8589a3..fcd5689800 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 @@ -14,7 +14,6 @@ import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub import net.corda.core.node.StateLoader import net.corda.core.node.services.* -import net.corda.core.schemas.MappedSchema import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction @@ -51,7 +50,7 @@ import java.util.* * building chains of transactions and verifying them. It isn't sufficient for testing flows however. */ open class MockServices( - cordappPackages: List, + cordappLoader: CordappLoader, override val validatedTransactions: WritableTransactionStorage, protected val stateLoader: StateLoaderImpl = StateLoaderImpl(validatedTransactions), vararg val keys: KeyPair @@ -101,24 +100,22 @@ open class MockServices( /** * Makes database and mock services appropriate for unit tests. - * - * @param customSchemas a set of schemas being used by [NodeSchemaService] - * @param keys a lis of [KeyPair] instances to be used by [MockServices]. Defualts to [MEGA_CORP_KEY] + * @param keys a list of [KeyPair] instances to be used by [MockServices]. Defualts to [MEGA_CORP_KEY] * @param createIdentityService a lambda function returning an instance of [IdentityService]. Defauts to [InMemoryIdentityService]. * * @return a pair where the first element is the instance of [CordaPersistence] and the second is [MockServices]. */ @JvmStatic - fun makeTestDatabaseAndMockServices(customSchemas: Set = emptySet(), - keys: List = listOf(MEGA_CORP_KEY), + fun makeTestDatabaseAndMockServices(keys: List = listOf(MEGA_CORP_KEY), createIdentityService: () -> IdentityService = { makeTestIdentityService() }, cordappPackages: List = emptyList()): Pair { + val cordappLoader = CordappLoader.createWithTestPackages(cordappPackages) val dataSourceProps = makeTestDataSourceProperties() val databaseProperties = makeTestDatabaseProperties() val identityServiceRef: IdentityService by lazy { createIdentityService() } - val database = configureDatabase(dataSourceProps, databaseProperties, NodeSchemaService(customSchemas), { identityServiceRef }) + val database = configureDatabase(dataSourceProps, databaseProperties, { identityServiceRef }, NodeSchemaService(cordappLoader)) val mockService = database.transaction { - object : MockServices(cordappPackages, *(keys.toTypedArray())) { + object : MockServices(cordappLoader, *(keys.toTypedArray())) { override val identityService: IdentityService = database.transaction { identityServiceRef } override val vaultService = makeVaultService(database.hibernateConfig) @@ -137,7 +134,8 @@ open class MockServices( } } - constructor(cordappPackages: List, vararg keys: KeyPair) : this(cordappPackages, MockTransactionStorage(), keys = *keys) + private constructor(cordappLoader: CordappLoader, vararg keys: KeyPair) : this(cordappLoader, MockTransactionStorage(), keys = *keys) + constructor(cordappPackages: List, vararg keys: KeyPair) : this(CordappLoader.createWithTestPackages(cordappPackages), keys = *keys) constructor(vararg keys: KeyPair) : this(emptyList(), *keys) constructor() : this(generateKeyPair()) @@ -167,11 +165,11 @@ open class MockServices( return NodeInfo(emptyList(), listOf(identity), 1, serial = 1L) } override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2) - val mockCordappProvider = MockCordappProvider(CordappLoader.createWithTestPackages(cordappPackages)).start(attachments) as MockCordappProvider + val mockCordappProvider = MockCordappProvider(cordappLoader, attachments) override val cordappProvider: CordappProvider get() = mockCordappProvider lateinit var hibernatePersister: HibernateObserver - fun makeVaultService(hibernateConfig: HibernateConfiguration = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties(), { identityService })): VaultServiceInternal { + fun makeVaultService(hibernateConfig: HibernateConfiguration): VaultServiceInternal { val vaultService = NodeVaultService(Clock.systemUTC(), keyManagementService, stateLoader, hibernateConfig) hibernatePersister = HibernateObserver(vaultService.rawUpdates, hibernateConfig) return vaultService diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/SimpleNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/SimpleNode.kt index cfb1e3a29d..3b7bf9130e 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/SimpleNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/SimpleNode.kt @@ -37,7 +37,7 @@ class SimpleNode(val config: NodeConfiguration, val address: NetworkHostAndPort val monitoringService = MonitoringService(MetricRegistry()) val identity: KeyPair = generateKeyPair() val identityService: IdentityService = InMemoryIdentityService(trustRoot = trustRoot) - val database: CordaPersistence = configureDatabase(config.dataSourceProperties, config.database, NodeSchemaService(), { InMemoryIdentityService(trustRoot = trustRoot) }) + val database: CordaPersistence = configureDatabase(config.dataSourceProperties, config.database, { InMemoryIdentityService(trustRoot = trustRoot) }) val keyService: KeyManagementService = E2ETestKeyManagementService(identityService, setOf(identity)) val executor = ServiceAffinityExecutor(config.myLegalName.organisation, 1) // TODO: We should have a dummy service hub rather than change behaviour in tests diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt index ad2e488e91..1fab8508fc 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt @@ -1,5 +1,6 @@ package net.corda.testing +import net.corda.core.internal.packageName import org.apache.logging.log4j.Level import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.core.LoggerContext @@ -25,7 +26,7 @@ object LogHelper { } } - fun setLevel(vararg classes: KClass<*>) = setLevel(*classes.map { "+" + it.java.`package`.name }.toTypedArray()) + fun setLevel(vararg classes: KClass<*>) = setLevel(*classes.map { "+" + it.packageName }.toTypedArray()) /** Removes custom configuration for the specified logger names */ fun reset(vararg names: String) { @@ -35,7 +36,7 @@ object LogHelper { loggerContext.updateLoggers(config) } - fun reset(vararg classes: KClass<*>) = reset(*classes.map { it.java.`package`.name }.toTypedArray()) + fun reset(vararg classes: KClass<*>) = reset(*classes.map { it.packageName }.toTypedArray()) /** Updates logging level for the specified Log4j logger name */ private fun setLevel(name: String, level: Level) { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt index dae72192a6..5c8736335e 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt @@ -4,12 +4,13 @@ import net.corda.core.contracts.ContractClassName import net.corda.core.cordapp.Cordapp import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.node.services.AttachmentId +import net.corda.core.node.services.AttachmentStorage import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import java.nio.file.Paths import java.util.* -class MockCordappProvider(cordappLoader: CordappLoader) : CordappProviderImpl(cordappLoader) { +class MockCordappProvider(cordappLoader: CordappLoader, attachmentStorage: AttachmentStorage) : CordappProviderImpl(cordappLoader, attachmentStorage) { val cordappRegistry = mutableListOf>() fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage) { From 2a68e23e6930293f2a4cf5d18ef585fe5b47b549 Mon Sep 17 00:00:00 2001 From: josecoll Date: Mon, 16 Oct 2017 13:47:35 +0100 Subject: [PATCH 178/185] Improved Cash Selection interface (#1858) * Cash selection refactoring such that 3d party DB providers are only required to implement Coin Selection SQL logic. * Re-added debug logging statement. * Updated to include PR review feedback from VK * Refactoring following rebase from master. * Fix broken JUnits following rebase. * Use JDBC ResultSet getBlob() and added custom serializer to address concern raised by tomtau in PR. * Fix failing JUnits. --- .../core/serialization/SerializationAPI.kt | 6 + .../net/corda/finance/contracts/asset/Cash.kt | 62 +------ .../cash/selection/AbstractCashSelection.kt | 168 ++++++++++++++++++ .../cash/selection/CashSelectionH2Impl.kt | 156 +++------------- .../cash/selection/CashSelectionMySQLImpl.kt | 17 +- .../net/corda/finance/flows/CashExitFlow.kt | 4 +- ...sset.cash.selection.AbstractCashSelection} | 0 7 files changed, 212 insertions(+), 201 deletions(-) create mode 100644 finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/AbstractCashSelection.kt rename finance/src/main/resources/META-INF/services/{net.corda.finance.contracts.asset.CashSelection => net.corda.finance.contracts.asset.cash.selection.AbstractCashSelection} (100%) 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 f77e3ae122..a91636ddc7 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt @@ -6,6 +6,7 @@ import net.corda.core.internal.WriteOnceProperty import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.sequence +import java.sql.Blob /** * An abstraction for serializing and deserializing objects, with support for versioning of the wire format via @@ -185,6 +186,11 @@ inline fun SerializedBytes.deserialize(serializationFactory */ inline fun ByteArray.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): T = this.sequence().deserialize(serializationFactory, context) +/** + * Convenience extension method for deserializing a JDBC Blob, utilising the defaults. + */ +inline fun Blob.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): T = this.getBytes(1, this.length().toInt()).deserialize(serializationFactory, context) + /** * Convenience extension method for serializing an object of type T, utilising the defaults. */ diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt index 55dffe1d36..d8ead23d9d 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt @@ -19,79 +19,21 @@ import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toBase58String -import net.corda.finance.contracts.asset.cash.selection.CashSelectionH2Impl +import net.corda.finance.contracts.asset.cash.selection.AbstractCashSelection import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.utils.sumCash import net.corda.finance.utils.sumCashOrNull import net.corda.finance.utils.sumCashOrZero import java.math.BigInteger import java.security.PublicKey -import java.sql.DatabaseMetaData import java.util.* -import java.util.concurrent.atomic.AtomicReference ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Cash // -/** - * Pluggable interface to allow for different cash selection provider implementations - * Default implementation [CashSelectionH2Impl] uses H2 database and a custom function within H2 to perform aggregation. - * Custom implementations must implement this interface and declare their implementation in - * META-INF/services/net.corda.contracts.asset.CashSelection - */ -interface CashSelection { - companion object { - val instance = AtomicReference() - - fun getInstance(metadata: () -> java.sql.DatabaseMetaData): CashSelection { - return instance.get() ?: { - val _metadata = metadata() - val cashSelectionAlgos = ServiceLoader.load(CashSelection::class.java).toList() - val cashSelectionAlgo = cashSelectionAlgos.firstOrNull { it.isCompatible(_metadata) } - cashSelectionAlgo?.let { - instance.set(cashSelectionAlgo) - cashSelectionAlgo - } ?: throw ClassNotFoundException("\nUnable to load compatible cash selection algorithm implementation for JDBC driver ($_metadata)." + - "\nPlease specify an implementation in META-INF/services/net.corda.finance.contracts.asset.CashSelection") - }.invoke() - } - } - - /** - * Upon dynamically loading configured Cash Selection algorithms declared in META-INF/services - * this method determines whether the loaded implementation is compatible and usable with the currently - * loaded JDBC driver. - * Note: the first loaded implementation to pass this check will be used at run-time. - */ - fun isCompatible(metadata: DatabaseMetaData): Boolean - - /** - * Query to gather Cash states that are available - * @param services The service hub to allow access to the database session - * @param amount The amount of currency desired (ignoring issues, but specifying the currency) - * @param onlyFromIssuerParties If empty the operation ignores the specifics of the issuer, - * otherwise the set of eligible states wil be filtered to only include those from these issuers. - * @param notary If null the notary source is ignored, if specified then only states marked - * with this notary are included. - * @param lockId The FlowLogic.runId.uuid of the flow, which is used to soft reserve the states. - * Also, previous outputs of the flow will be eligible as they are implicitly locked with this id until the flow completes. - * @param withIssuerRefs If not empty the specific set of issuer references to match against. - * @return The matching states that were found. If sufficient funds were found these will be locked, - * otherwise what is available is returned unlocked for informational purposes. - */ - @Suspendable - fun unconsumedCashStatesForSpending(services: ServiceHub, - amount: Amount, - onlyFromIssuerParties: Set = emptySet(), - notary: Party? = null, - lockId: UUID, - withIssuerRefs: Set = emptySet()): List> -} - /** * A cash transaction may split and merge money represented by a set of (issuer, depositRef) pairs, across multiple * input and output states. Imagine a Bitcoin transaction but in which all UTXOs had a colour @@ -384,7 +326,7 @@ class Cash : OnLedgerAsset() { // Retrieve unspent and unlocked cash states that meet our spending criteria. val totalAmount = payments.map { it.amount }.sumOrThrow() - val cashSelection = CashSelection.getInstance({ services.jdbcSession().metaData }) + val cashSelection = AbstractCashSelection.getInstance({ services.jdbcSession().metaData }) val acceptableCoins = cashSelection.unconsumedCashStatesForSpending(services, totalAmount, onlyFromParties, tx.notary, tx.lockId) val revocationEnabled = false // Revocation is currently unsupported // Generate a new identity that change will be sent to for confidentiality purposes. This means that a diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/AbstractCashSelection.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/AbstractCashSelection.kt new file mode 100644 index 0000000000..fdc3d0d5a7 --- /dev/null +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/AbstractCashSelection.kt @@ -0,0 +1,168 @@ +package net.corda.finance.contracts.asset.cash.selection + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Amount +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionState +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FlowLogic +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub +import net.corda.core.node.services.StatesNotAvailableException +import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.deserialize +import net.corda.core.utilities.* +import net.corda.finance.contracts.asset.Cash +import java.sql.* +import java.util.* +import java.util.concurrent.atomic.AtomicReference +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +/** + * Pluggable interface to allow for different cash selection provider implementations + * Default implementation [CashSelectionH2Impl] uses H2 database and a custom function within H2 to perform aggregation. + * Custom implementations must implement this interface and declare their implementation in + * META-INF/services/net.corda.contracts.asset.CashSelection + */ +abstract class AbstractCashSelection { + companion object { + val instance = AtomicReference() + + fun getInstance(metadata: () -> java.sql.DatabaseMetaData): AbstractCashSelection { + return instance.get() ?: { + val _metadata = metadata() + val cashSelectionAlgos = ServiceLoader.load(AbstractCashSelection::class.java).toList() + val cashSelectionAlgo = cashSelectionAlgos.firstOrNull { it.isCompatible(_metadata) } + cashSelectionAlgo?.let { + instance.set(cashSelectionAlgo) + cashSelectionAlgo + } ?: throw ClassNotFoundException("\nUnable to load compatible cash selection algorithm implementation for JDBC driver ($_metadata)." + + "\nPlease specify an implementation in META-INF/services/${AbstractCashSelection::class.java}") + }.invoke() + } + + val log = loggerFor() + } + + // coin selection retry loop counter, sleep (msecs) and lock for selecting states + // TODO: make parameters configurable when we get CorDapp configuration. + private val MAX_RETRIES = 8 + private val RETRY_SLEEP = 100 + private val RETRY_CAP = 2000 + private val spendLock: ReentrantLock = ReentrantLock() + + /** + * Upon dynamically loading configured Cash Selection algorithms declared in META-INF/services + * this method determines whether the loaded implementation is compatible and usable with the currently + * loaded JDBC driver. + * Note: the first loaded implementation to pass this check will be used at run-time. + */ + abstract fun isCompatible(metadata: DatabaseMetaData): Boolean + + /** + * A vendor specific query(ies) to gather Cash states that are available. + * @param statement The service hub to allow access to the database session + * @param amount The amount of currency desired (ignoring issues, but specifying the currency) + * @param lockId The FlowLogic.runId.uuid of the flow, which is used to soft reserve the states. + * Also, previous outputs of the flow will be eligible as they are implicitly locked with this id until the flow completes. + * @param notary If null the notary source is ignored, if specified then only states marked + * with this notary are included. + * @param onlyFromIssuerParties Optional issuer parties to match against. + * @param withIssuerRefs Optional issuer references to match against. + * @return JDBC ResultSet with the matching states that were found. If sufficient funds were found these will be locked, + * otherwise what is available is returned unlocked for informational purposes. + */ + abstract fun executeQuery(connection: Connection, amount: Amount, lockId: UUID, notary: Party?, + onlyFromIssuerParties: Set, withIssuerRefs: Set) : ResultSet + + override abstract fun toString() : String + + /** + * Query to gather Cash states that are available and retry if they are temporarily unavailable. + * @param services The service hub to allow access to the database session + * @param amount The amount of currency desired (ignoring issues, but specifying the currency) + * @param onlyFromIssuerParties If empty the operation ignores the specifics of the issuer, + * otherwise the set of eligible states wil be filtered to only include those from these issuers. + * @param notary If null the notary source is ignored, if specified then only states marked + * with this notary are included. + * @param lockId The FlowLogic.runId.uuid of the flow, which is used to soft reserve the states. + * Also, previous outputs of the flow will be eligible as they are implicitly locked with this id until the flow completes. + * @param withIssuerRefs If not empty the specific set of issuer references to match against. + * @return The matching states that were found. If sufficient funds were found these will be locked, + * otherwise what is available is returned unlocked for informational purposes. + */ + @Suspendable + fun unconsumedCashStatesForSpending(services: ServiceHub, + amount: Amount, + onlyFromIssuerParties: Set = emptySet(), + notary: Party? = null, + lockId: UUID, + withIssuerRefs: Set = emptySet()): List> { + val stateAndRefs = mutableListOf>() + + for (retryCount in 1..MAX_RETRIES) { + if (!attemptSpend(services, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs, stateAndRefs)) { + log.warn("Coin selection failed on attempt $retryCount") + // TODO: revisit the back off strategy for contended spending. + if (retryCount != MAX_RETRIES) { + stateAndRefs.clear() + val durationMillis = (minOf(RETRY_SLEEP.shl(retryCount), RETRY_CAP / 2) * (1.0 + Math.random())).toInt() + FlowLogic.sleep(durationMillis.millis) + } else { + log.warn("Insufficient spendable states identified for $amount") + } + } else { + break + } + } + return stateAndRefs + } + + private fun attemptSpend(services: ServiceHub, amount: Amount, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set, withIssuerRefs: Set, stateAndRefs: MutableList>): Boolean { + spendLock.withLock { + val connection = services.jdbcSession() + try { + // we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null) + // the softLockReserve update will detect whether we try to lock states locked by others + val rs = executeQuery(connection, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs) + stateAndRefs.clear() + + var totalPennies = 0L + while (rs.next()) { + val txHash = SecureHash.parse(rs.getString(1)) + val index = rs.getInt(2) + val stateRef = StateRef(txHash, index) + val state = rs.getBlob(3).deserialize>(context = SerializationDefaults.STORAGE_CONTEXT) + val pennies = rs.getLong(4) + totalPennies = rs.getLong(5) + val rowLockId = rs.getString(6) + stateAndRefs.add(StateAndRef(state, stateRef)) + log.trace { "ROW: $rowLockId ($lockId): $stateRef : $pennies ($totalPennies)" } + } + + if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) { + // we should have a minimum number of states to satisfy our selection `amount` criteria + log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs") + + // With the current single threaded state machine available states are guaranteed to lock. + // TODO However, we will have to revisit these methods in the future multi-threaded. + services.vaultService.softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet()) + return true + } + log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}") + // retry as more states may become available + } catch (e: SQLException) { + log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId] + $e. + """) + } catch (e: StatesNotAvailableException) { // Should never happen with single threaded state machine + log.warn(e.message) + // retry only if there are locked states that may become available again (or consumed with change) + } + } + return false + } +} \ No newline at end of file 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 82bc6ad763..5894eff73f 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 @@ -1,28 +1,15 @@ package net.corda.finance.contracts.asset.cash.selection -import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Amount -import net.corda.core.contracts.StateAndRef -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionState -import net.corda.core.crypto.SecureHash -import net.corda.core.flows.FlowLogic import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party -import net.corda.core.node.ServiceHub -import net.corda.core.node.services.StatesNotAvailableException -import net.corda.core.serialization.SerializationDefaults -import net.corda.core.serialization.deserialize import net.corda.core.utilities.* -import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.CashSelection +import java.sql.Connection import java.sql.DatabaseMetaData -import java.sql.SQLException +import java.sql.ResultSet import java.util.* -import java.util.concurrent.locks.ReentrantLock -import kotlin.concurrent.withLock -class CashSelectionH2Impl : CashSelection { +class CashSelectionH2Impl : AbstractCashSelection() { companion object { const val JDBC_DRIVER_NAME = "H2 JDBC Driver" @@ -33,53 +20,8 @@ class CashSelectionH2Impl : CashSelection { return metadata.driverName == JDBC_DRIVER_NAME } - // coin selection retry loop counter, sleep (msecs) and lock for selecting states - private val MAX_RETRIES = 8 - private val RETRY_SLEEP = 100 - private val RETRY_CAP = 2000 - private val spendLock: ReentrantLock = ReentrantLock() + override fun toString() = "${this::class.java} for $JDBC_DRIVER_NAME" - /** - * An optimised query to gather Cash states that are available and retry if they are temporarily unavailable. - * @param services The service hub to allow access to the database session - * @param amount The amount of currency desired (ignoring issues, but specifying the currency) - * @param onlyFromIssuerParties If empty the operation ignores the specifics of the issuer, - * otherwise the set of eligible states wil be filtered to only include those from these issuers. - * @param notary If null the notary source is ignored, if specified then only states marked - * with this notary are included. - * @param lockId The FlowLogic.runId.uuid of the flow, which is used to soft reserve the states. - * Also, previous outputs of the flow will be eligible as they are implicitly locked with this id until the flow completes. - * @param withIssuerRefs If not empty the specific set of issuer references to match against. - * @return The matching states that were found. If sufficient funds were found these will be locked, - * otherwise what is available is returned unlocked for informational purposes. - */ - @Suspendable - override fun unconsumedCashStatesForSpending(services: ServiceHub, - amount: Amount, - onlyFromIssuerParties: Set, - notary: Party?, - lockId: UUID, - withIssuerRefs: Set): List> { - - val stateAndRefs = mutableListOf>() - - for (retryCount in 1..MAX_RETRIES) { - if (!attemptSpend(services, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs, stateAndRefs)) { - log.warn("Coin selection failed on attempt $retryCount") - // TODO: revisit the back off strategy for contended spending. - if (retryCount != MAX_RETRIES) { - stateAndRefs.clear() - val durationMillis = (minOf(RETRY_SLEEP.shl(retryCount), RETRY_CAP / 2) * (1.0 + Math.random())).toInt() - FlowLogic.sleep(durationMillis.millis) - } else { - log.warn("Insufficient spendable states identified for $amount") - } - } else { - break - } - } - return stateAndRefs - } // We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins: // 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the @@ -87,15 +29,11 @@ class CashSelectionH2Impl : CashSelection { // 2) H2 uses session variables to perform this accumulator function: // http://www.h2database.com/html/functions.html#set // 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries) + override fun executeQuery(connection: Connection, amount: Amount, lockId: UUID, notary: Party?, + onlyFromIssuerParties: Set, withIssuerRefs: Set) : ResultSet { + connection.createStatement().execute("CALL SET(@t, 0);") - private fun attemptSpend(services: ServiceHub, amount: Amount, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set, withIssuerRefs: Set, stateAndRefs: MutableList>): Boolean { - val connection = services.jdbcSession() - spendLock.withLock { - val statement = connection.createStatement() - try { - statement.execute("CALL SET(@t, CAST(0 AS BIGINT));") - - val selectJoin = """ + val selectJoin = """ SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id FROM vault_states AS vs, contract_cash_states AS ccs WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index @@ -103,65 +41,27 @@ class CashSelectionH2Impl : CashSelection { AND ccs.ccy_code = ? and @t < ? AND (vs.lock_id = ? OR vs.lock_id is null) """ + - (if (notary != null) - " AND vs.notary_name = ?" else "") + - (if (onlyFromIssuerParties.isNotEmpty()) - " AND ccs.issuer_key IN (?)" else "") + - (if (withIssuerRefs.isNotEmpty()) - " AND ccs.issuer_ref IN (?)" else "") + (if (notary != null) + " AND vs.notary_name = ?" else "") + + (if (onlyFromIssuerParties.isNotEmpty()) + " AND ccs.issuer_key IN (?)" else "") + + (if (withIssuerRefs.isNotEmpty()) + " AND ccs.issuer_ref IN (?)" else "") - // Use prepared statement for protection against SQL Injection (http://www.h2database.com/html/advanced.html#sql_injection) - val psSelectJoin = connection.prepareStatement(selectJoin) - var pIndex = 0 - psSelectJoin.setString(++pIndex, amount.token.currencyCode) - psSelectJoin.setLong(++pIndex, amount.quantity) - psSelectJoin.setString(++pIndex, lockId.toString()) - if (notary != null) - psSelectJoin.setString(++pIndex, notary.name.toString()) - if (onlyFromIssuerParties.isNotEmpty()) - psSelectJoin.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toBase58String() as Any}.toTypedArray() ) - if (withIssuerRefs.isNotEmpty()) - psSelectJoin.setObject(++pIndex, withIssuerRefs.map { it.bytes.toHexString() as Any }.toTypedArray()) - log.debug { psSelectJoin.toString() } + // Use prepared statement for protection against SQL Injection (http://www.h2database.com/html/advanced.html#sql_injection) + val psSelectJoin = connection.prepareStatement(selectJoin) + var pIndex = 0 + psSelectJoin.setString(++pIndex, amount.token.currencyCode) + psSelectJoin.setLong(++pIndex, amount.quantity) + psSelectJoin.setString(++pIndex, lockId.toString()) + if (notary != null) + psSelectJoin.setString(++pIndex, notary.name.toString()) + if (onlyFromIssuerParties.isNotEmpty()) + psSelectJoin.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toBase58String() as Any}.toTypedArray() ) + if (withIssuerRefs.isNotEmpty()) + psSelectJoin.setObject(++pIndex, withIssuerRefs.map { it.bytes.toHexString() as Any }.toTypedArray()) + log.debug { psSelectJoin.toString() } - // Retrieve spendable state refs - val rs = psSelectJoin.executeQuery() - stateAndRefs.clear() - var totalPennies = 0L - while (rs.next()) { - val txHash = SecureHash.parse(rs.getString(1)) - val index = rs.getInt(2) - val stateRef = StateRef(txHash, index) - val state = rs.getBytes(3).deserialize>(context = SerializationDefaults.STORAGE_CONTEXT) - val pennies = rs.getLong(4) - totalPennies = rs.getLong(5) - val rowLockId = rs.getString(6) - stateAndRefs.add(StateAndRef(state, stateRef)) - log.trace { "ROW: $rowLockId ($lockId): $stateRef : $pennies ($totalPennies)" } - } - - if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) { - // we should have a minimum number of states to satisfy our selection `amount` criteria - log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs") - - // With the current single threaded state machine available states are guaranteed to lock. - // TODO However, we will have to revisit these methods in the future multi-threaded. - services.vaultService.softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet()) - return true - } - log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}") - // retry as more states may become available - } catch (e: SQLException) { - log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId] - $e. - """) - } catch (e: StatesNotAvailableException) { // Should never happen with single threaded state machine - log.warn(e.message) - // retry only if there are locked states that may become available again (or consumed with change) - } finally { - statement.close() - } - } - return false + return psSelectJoin.executeQuery() } } \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionMySQLImpl.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionMySQLImpl.kt index 1a0eac0169..853ba23d07 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionMySQLImpl.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionMySQLImpl.kt @@ -1,17 +1,15 @@ package net.corda.finance.contracts.asset.cash.selection import net.corda.core.contracts.Amount -import net.corda.core.contracts.StateAndRef import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party -import net.corda.core.node.ServiceHub import net.corda.core.utilities.OpaqueBytes -import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.CashSelection +import java.sql.Connection import java.sql.DatabaseMetaData +import java.sql.ResultSet import java.util.* -class CashSelectionMySQLImpl : CashSelection { +class CashSelectionMySQLImpl : AbstractCashSelection() { companion object { const val JDBC_DRIVER_NAME = "MySQL JDBC Driver" @@ -21,12 +19,9 @@ class CashSelectionMySQLImpl : CashSelection { return metadata.driverName == JDBC_DRIVER_NAME } - override fun unconsumedCashStatesForSpending(services: ServiceHub, - amount: Amount, - onlyFromIssuerParties: Set, - notary: Party?, - lockId: UUID, - withIssuerRefs: Set): List> { + override fun executeQuery(statement: Connection, amount: Amount, lockId: UUID, notary: Party?, issuerKeysStr: Set, issuerRefsStr: Set): ResultSet { TODO("MySQL cash selection not implemented") } + + override fun toString() = "${this::class.java} for ${CashSelectionH2Impl.JDBC_DRIVER_NAME}" } \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt index c978ded6f0..23b05a1e8f 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt @@ -14,7 +14,7 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.ProgressTracker import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.CashSelection +import net.corda.finance.contracts.asset.cash.selection.AbstractCashSelection import net.corda.finance.issuedBy import java.util.* @@ -46,7 +46,7 @@ class CashExitFlow(private val amount: Amount, progressTracker.currentStep = GENERATING_TX val builder = TransactionBuilder(notary = null) val issuer = ourIdentity.ref(issuerRef) - val exitStates = CashSelection + val exitStates = AbstractCashSelection .getInstance { serviceHub.jdbcSession().metaData } .unconsumedCashStatesForSpending(serviceHub, amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference)) val signers = try { diff --git a/finance/src/main/resources/META-INF/services/net.corda.finance.contracts.asset.CashSelection b/finance/src/main/resources/META-INF/services/net.corda.finance.contracts.asset.cash.selection.AbstractCashSelection similarity index 100% rename from finance/src/main/resources/META-INF/services/net.corda.finance.contracts.asset.CashSelection rename to finance/src/main/resources/META-INF/services/net.corda.finance.contracts.asset.cash.selection.AbstractCashSelection From 83b908050241776a25ebc814e1e63c5dd342b3b6 Mon Sep 17 00:00:00 2001 From: mkit Date: Mon, 16 Oct 2017 14:03:51 +0100 Subject: [PATCH 179/185] Adding an option to override the default isolation level on per transaction basis (#1886) --- .../corda/node/utilities/CordaPersistence.kt | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt b/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt index b8fc4f333d..77055c6073 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt @@ -40,10 +40,21 @@ class CordaPersistence(var dataSource: HikariDataSource, private val schemaServi } } - fun createTransaction(): DatabaseTransaction { + /** + * Creates an instance of [DatabaseTransaction], with the given isolation level. + * @param isolationLevel isolation level for the transaction. If not specified the default (i.e. provided at the creation time) is used. + */ + fun createTransaction(isolationLevel: Int): DatabaseTransaction { // We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases. DatabaseTransactionManager.dataSource = this - return DatabaseTransactionManager.currentOrNew(transactionIsolationLevel) + return DatabaseTransactionManager.currentOrNew(isolationLevel) + } + + /** + * Creates an instance of [DatabaseTransaction], with the transaction isolation level specified at the creation time. + */ + fun createTransaction(): DatabaseTransaction { + return createTransaction(transactionIsolationLevel) } fun createSession(): Connection { @@ -53,9 +64,22 @@ class CordaPersistence(var dataSource: HikariDataSource, private val schemaServi return ctx?.connection ?: throw IllegalStateException("Was expecting to find database transaction: must wrap calling code within a transaction.") } - fun transaction(statement: DatabaseTransaction.() -> T): T { + /** + * Executes given statement in the scope of transaction, with the given isolation level. + * @param isolationLevel isolation level for the transaction. + * @param statement to be executed in the scope of this transaction. + */ + fun transaction(isolationLevel: Int, statement: DatabaseTransaction.() -> T): T { DatabaseTransactionManager.dataSource = this - return transaction(transactionIsolationLevel, 3, statement) + return transaction(isolationLevel, 3, statement) + } + + /** + * Executes given statement in the scope of transaction with the transaction level specified at the creation time. + * @param statement to be executed in the scope of this transaction. + */ + fun transaction(statement: DatabaseTransaction.() -> T): T { + return transaction(transactionIsolationLevel, statement) } private fun transaction(transactionIsolation: Int, repetitionAttempts: Int, statement: DatabaseTransaction.() -> T): T { From ead805fb218b766035f4c05c84ec2954ad5ca857 Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Mon, 16 Oct 2017 14:32:16 +0100 Subject: [PATCH 180/185] Make it handle whitespace in paths correctly (needs to be URL decoded) --- .../com/r3/corda/signing/configuration/ConfigurationTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/signing-server/src/test/kotlin/com/r3/corda/signing/configuration/ConfigurationTest.kt b/signing-server/src/test/kotlin/com/r3/corda/signing/configuration/ConfigurationTest.kt index 979033a086..385049b23b 100644 --- a/signing-server/src/test/kotlin/com/r3/corda/signing/configuration/ConfigurationTest.kt +++ b/signing-server/src/test/kotlin/com/r3/corda/signing/configuration/ConfigurationTest.kt @@ -8,8 +8,8 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith class ConfigurationTest { - private val validConfigPath = File(javaClass.getResource("/signing_service.conf").path).absolutePath - private val invalidConfigPath = File(javaClass.getResource("/signing_service_fail.conf").path).absolutePath + private val validConfigPath = File(java.net.URLDecoder.decode(javaClass.getResource("/signing_service.conf").path, "UTF-8")).absolutePath + private val invalidConfigPath = File(java.net.URLDecoder.decode(javaClass.getResource("/signing_service_fail.conf").path, "UTF-8")).absolutePath @Test fun `authMode is parsed correctly`() { From d8462ad6628410b832c72e638bf3a0c29fec4bda Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Mon, 16 Oct 2017 14:36:43 +0100 Subject: [PATCH 181/185] Make it handle whitespace in paths correctly - even better by using toURI() --- .idea/compiler.xml | 3 +++ .../com/r3/corda/signing/configuration/ConfigurationTest.kt | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 25abc91c93..1d5e5c9c1f 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -99,6 +99,9 @@ + + + diff --git a/signing-server/src/test/kotlin/com/r3/corda/signing/configuration/ConfigurationTest.kt b/signing-server/src/test/kotlin/com/r3/corda/signing/configuration/ConfigurationTest.kt index 385049b23b..7883e13ba7 100644 --- a/signing-server/src/test/kotlin/com/r3/corda/signing/configuration/ConfigurationTest.kt +++ b/signing-server/src/test/kotlin/com/r3/corda/signing/configuration/ConfigurationTest.kt @@ -8,8 +8,8 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith class ConfigurationTest { - private val validConfigPath = File(java.net.URLDecoder.decode(javaClass.getResource("/signing_service.conf").path, "UTF-8")).absolutePath - private val invalidConfigPath = File(java.net.URLDecoder.decode(javaClass.getResource("/signing_service_fail.conf").path, "UTF-8")).absolutePath + private val validConfigPath = File(javaClass.getResource("/signing_service.conf").toURI()).absolutePath + private val invalidConfigPath = File(javaClass.getResource("/signing_service_fail.conf").toURI()).absolutePath @Test fun `authMode is parsed correctly`() { From d3e8df1644d84f9505452e65a219c0c67eb12b48 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Mon, 16 Oct 2017 14:39:28 +0100 Subject: [PATCH 182/185] Separates out the tutorial code into separate docs to ensure it compiles. --- .../java/tutorial/helloworld/IOUContract.java | 46 +++++ .../java/tutorial/helloworld/IOUFlow.java | 68 ++++++++ .../java/tutorial/helloworld/IOUState.java | 39 +++++ .../java/tutorial/twoparty/IOUContract.java | 50 ++++++ .../docs/java/tutorial/twoparty/IOUFlow.java | 82 +++++++++ .../tutorial/twoparty/IOUFlowResponder.java | 47 +++++ .../docs/java/tutorial/twoparty/IOUState.java | 37 ++++ .../docs/tutorial/helloworld/contract.kt | 33 ++++ .../corda/docs/tutorial/helloworld/flow.kt | 52 ++++++ .../corda/docs/tutorial/helloworld/state.kt | 12 ++ .../corda/docs/tutorial/twoparty/contract.kt | 36 ++++ .../net/corda/docs/tutorial/twoparty/flow.kt | 57 ++++++ .../docs/tutorial/twoparty/flowResponder.kt | 30 ++++ .../net/corda/docs/tutorial/twoparty/state.kt | 10 ++ docs/source/hello-world-contract.rst | 81 +-------- docs/source/hello-world-flow.rst | 128 ++------------ docs/source/hello-world-running.rst | 74 ++++---- docs/source/hello-world-state.rst | 54 +----- docs/source/hello-world-template.rst | 11 +- docs/source/tut-two-party-contract.rst | 40 +++-- docs/source/tut-two-party-flow.rst | 165 +++--------------- docs/source/tut-two-party-introduction.rst | 2 +- 22 files changed, 720 insertions(+), 434 deletions(-) create mode 100644 docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUContract.java create mode 100644 docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlow.java create mode 100644 docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUState.java create mode 100644 docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUContract.java create mode 100644 docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java create mode 100644 docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlowResponder.java create mode 100644 docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUState.java create mode 100644 docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/contract.kt create mode 100644 docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/flow.kt create mode 100644 docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/state.kt create mode 100644 docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/contract.kt create mode 100644 docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt create mode 100644 docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flowResponder.kt create mode 100644 docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/state.kt diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUContract.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUContract.java new file mode 100644 index 0000000000..3023396e81 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUContract.java @@ -0,0 +1,46 @@ +package net.corda.docs.java.tutorial.helloworld; + +// DOCSTART 01 +import net.corda.core.contracts.CommandData; +import net.corda.core.contracts.CommandWithParties; +import net.corda.core.contracts.Contract; +import net.corda.core.identity.Party; +import net.corda.core.transactions.LedgerTransaction; + +import java.security.PublicKey; +import java.util.List; + +import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; +import static net.corda.core.contracts.ContractsDSL.requireThat; + +public class IOUContract implements Contract { + // Our Create command. + public static class Create implements CommandData { + } + + @Override + public void verify(LedgerTransaction tx) { + final CommandWithParties command = requireSingleCommand(tx.getCommands(), Create.class); + + requireThat(check -> { + // Constraints on the shape of the transaction. + check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty()); + check.using("There should be one output state of type IOUState.", tx.getOutputs().size() == 1); + + // IOU-specific constraints. + final IOUState out = tx.outputsOfType(IOUState.class).get(0); + final Party lender = out.getLender(); + final Party borrower = out.getBorrower(); + check.using("The IOU's value must be non-negative.", out.getValue() > 0); + check.using("The lender and the borrower cannot be the same entity.", lender != borrower); + + // Constraints on the signers. + final List signers = command.getSigners(); + check.using("There must only be one signer.", signers.size() == 1); + check.using("The signer must be the lender.", signers.contains(lender.getOwningKey())); + + return null; + }); + } +} +// DOCEND 01 \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlow.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlow.java new file mode 100644 index 0000000000..5b2d5d47f4 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlow.java @@ -0,0 +1,68 @@ +package net.corda.docs.java.tutorial.helloworld; + +// DOCSTART 01 +import co.paralleluniverse.fibers.Suspendable; +import net.corda.core.contracts.Command; +import net.corda.core.contracts.StateAndContract; +import net.corda.core.flows.*; +import net.corda.core.identity.Party; +import net.corda.core.transactions.SignedTransaction; +import net.corda.core.transactions.TransactionBuilder; +import net.corda.core.utilities.ProgressTracker; + +@InitiatingFlow +@StartableByRPC +public class IOUFlow extends FlowLogic { + private final Integer iouValue; + private final Party otherParty; + + /** + * The progress tracker provides checkpoints indicating the progress of the flow to observers. + */ + private final ProgressTracker progressTracker = new ProgressTracker(); + + public IOUFlow(Integer iouValue, Party otherParty) { + this.iouValue = iouValue; + this.otherParty = otherParty; + } + + @Override + public ProgressTracker getProgressTracker() { + return progressTracker; + } + + /** + * The flow logic is encapsulated within the call() method. + */ + @Suspendable + @Override + public Void call() throws FlowException { + // We retrieve the notary identity from the network map. + final Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0); + + // We create a transaction builder. + final TransactionBuilder txBuilder = new TransactionBuilder(); + txBuilder.setNotary(notary); + + // We create the transaction components. + IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty); + String outputContract = IOUContract.class.getName(); + StateAndContract outputContractAndState = new StateAndContract(outputState, outputContract); + Command cmd = new Command<>(new IOUContract.Create(), getOurIdentity().getOwningKey()); + + // We add the items to the builder. + txBuilder.withItems(outputContractAndState, cmd); + + // Verifying the transaction. + txBuilder.verify(getServiceHub()); + + // Signing the transaction. + final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); + + // Finalising the transaction. + subFlow(new FinalityFlow(signedTx)); + + return null; + } +} +// DOCEND 01 \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUState.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUState.java new file mode 100644 index 0000000000..977457fd29 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUState.java @@ -0,0 +1,39 @@ +package net.corda.docs.java.tutorial.helloworld; + +// DOCSTART 01 +import com.google.common.collect.ImmutableList; +import net.corda.core.contracts.ContractState; +import net.corda.core.identity.AbstractParty; +import net.corda.core.identity.Party; + +import java.util.List; + +public class IOUState implements ContractState { + private final int value; + private final Party lender; + private final Party borrower; + + public IOUState(int value, Party lender, Party borrower) { + this.value = value; + this.lender = lender; + this.borrower = borrower; + } + + public int getValue() { + return value; + } + + public Party getLender() { + return lender; + } + + public Party getBorrower() { + return borrower; + } + + @Override + public List getParticipants() { + return ImmutableList.of(lender, borrower); + } +} +// DOCEND 01 \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUContract.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUContract.java new file mode 100644 index 0000000000..bcf8dded07 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUContract.java @@ -0,0 +1,50 @@ +package net.corda.docs.java.tutorial.twoparty; + +// DOCSTART 01 +import com.google.common.collect.ImmutableList; +import net.corda.core.contracts.CommandData; +import net.corda.core.contracts.CommandWithParties; +import net.corda.core.contracts.Contract; +import net.corda.core.identity.Party; +import net.corda.core.transactions.LedgerTransaction; + +import java.security.PublicKey; +import java.util.List; + +import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; +import static net.corda.core.contracts.ContractsDSL.requireThat; +// DOCEND 01 + +public class IOUContract implements Contract { + // Our Create command. + public static class Create implements CommandData { + } + + @Override + public void verify(LedgerTransaction tx) { + final CommandWithParties command = requireSingleCommand(tx.getCommands(), net.corda.docs.java.tutorial.helloworld.IOUContract.Create.class); + + requireThat(check -> { + // Constraints on the shape of the transaction. + check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty()); + check.using("There should be one output state of type IOUState.", tx.getOutputs().size() == 1); + + // IOU-specific constraints. + final IOUState out = tx.outputsOfType(IOUState.class).get(0); + final Party lender = out.getLender(); + final Party borrower = out.getBorrower(); + check.using("The IOU's value must be non-negative.", out.getValue() > 0); + check.using("The lender and the borrower cannot be the same entity.", lender != borrower); + + // DOCSTART 02 + // Constraints on the signers. + final List signers = command.getSigners(); + check.using("There must be two signers.", signers.size() == 2); + check.using("The borrower and lender must be signers.", signers.containsAll( + ImmutableList.of(borrower.getOwningKey(), lender.getOwningKey()))); + // DOCEND 02 + + return null; + }); + } +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java new file mode 100644 index 0000000000..4e0533d323 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java @@ -0,0 +1,82 @@ +package net.corda.docs.java.tutorial.twoparty; + +// DOCSTART 01 +import co.paralleluniverse.fibers.Suspendable; +import com.google.common.collect.ImmutableList; +import net.corda.core.contracts.Command; +import net.corda.core.contracts.StateAndContract; +import net.corda.core.flows.*; +import net.corda.core.identity.Party; +import net.corda.core.transactions.SignedTransaction; +import net.corda.core.transactions.TransactionBuilder; +import net.corda.core.utilities.ProgressTracker; + +import java.security.PublicKey; +import java.util.List; +// DOCEND 01 + +@InitiatingFlow +@StartableByRPC +public class IOUFlow extends FlowLogic { + private final Integer iouValue; + private final Party otherParty; + + /** + * The progress tracker provides checkpoints indicating the progress of the flow to observers. + */ + private final ProgressTracker progressTracker = new ProgressTracker(); + + public IOUFlow(Integer iouValue, Party otherParty) { + this.iouValue = iouValue; + this.otherParty = otherParty; + } + + @Override + public ProgressTracker getProgressTracker() { + return progressTracker; + } + + /** + * The flow logic is encapsulated within the call() method. + */ + @Suspendable + @Override + public Void call() throws FlowException { + // We retrieve the notary identity from the network map. + final Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0); + + // We create a transaction builder. + final TransactionBuilder txBuilder = new TransactionBuilder(); + txBuilder.setNotary(notary); + + // DOCSTART 02 + // We create the transaction components. + IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty); + String outputContract = IOUContract.class.getName(); + StateAndContract outputContractAndState = new StateAndContract(outputState, outputContract); + List requiredSigners = ImmutableList.of(getOurIdentity().getOwningKey(), otherParty.getOwningKey()); + Command cmd = new Command<>(new IOUContract.Create(), requiredSigners); + + // We add the items to the builder. + txBuilder.withItems(outputContractAndState, cmd); + + // Verifying the transaction. + txBuilder.verify(getServiceHub()); + + // Signing the transaction. + final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); + + // Creating a session with the other party. + FlowSession otherpartySession = initiateFlow(otherParty); + + // Obtaining the counterparty's signature. + SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow( + signedTx, ImmutableList.of(otherpartySession), CollectSignaturesFlow.tracker())); + + // Finalising the transaction. + subFlow(new FinalityFlow(fullySignedTx)); + + return null; + // DOCEND 02 + } +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlowResponder.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlowResponder.java new file mode 100644 index 0000000000..ac1f312ab6 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlowResponder.java @@ -0,0 +1,47 @@ +package net.corda.docs.java.tutorial.twoparty; + +// DOCSTART 01 +import co.paralleluniverse.fibers.Suspendable; +import net.corda.core.contracts.ContractState; +import net.corda.core.flows.*; +import net.corda.core.transactions.SignedTransaction; +import net.corda.core.utilities.ProgressTracker; +import net.corda.docs.java.tutorial.helloworld.IOUFlow; +import net.corda.docs.java.tutorial.helloworld.IOUState; + +import static net.corda.core.contracts.ContractsDSL.requireThat; + +@InitiatedBy(IOUFlow.class) +public class IOUFlowResponder extends FlowLogic { + private final FlowSession otherPartySession; + + public IOUFlowResponder(FlowSession otherPartySession) { + this.otherPartySession = otherPartySession; + } + + @Suspendable + @Override + public Void call() throws FlowException { + class SignTxFlow extends SignTransactionFlow { + private SignTxFlow(FlowSession otherPartySession, ProgressTracker progressTracker) { + super(otherPartySession, progressTracker); + } + + @Override + protected void checkTransaction(SignedTransaction stx) { + requireThat(require -> { + ContractState output = stx.getTx().getOutputs().get(0).getData(); + require.using("This must be an IOU transaction.", output instanceof IOUState); + IOUState iou = (IOUState) output; + require.using("The IOU's value can't be too high.", iou.getValue() < 100); + return null; + }); + } + } + + subFlow(new SignTxFlow(otherPartySession, SignTransactionFlow.Companion.tracker())); + + return null; + } +} +// DOCEND 01 \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUState.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUState.java new file mode 100644 index 0000000000..4cc6f9f76c --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUState.java @@ -0,0 +1,37 @@ +package net.corda.docs.java.tutorial.twoparty; + +import com.google.common.collect.ImmutableList; +import net.corda.core.contracts.ContractState; +import net.corda.core.identity.AbstractParty; +import net.corda.core.identity.Party; + +import java.util.List; + +public class IOUState implements ContractState { + private final int value; + private final Party lender; + private final Party borrower; + + public IOUState(int value, Party lender, Party borrower) { + this.value = value; + this.lender = lender; + this.borrower = borrower; + } + + public int getValue() { + return value; + } + + public Party getLender() { + return lender; + } + + public Party getBorrower() { + return borrower; + } + + @Override + public List getParticipants() { + return ImmutableList.of(lender, borrower); + } +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/contract.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/contract.kt new file mode 100644 index 0000000000..39f00d60ea --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/contract.kt @@ -0,0 +1,33 @@ +package net.corda.docs.tutorial.helloworld + +// DOCSTART 01 +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.Contract +import net.corda.core.contracts.requireSingleCommand +import net.corda.core.contracts.requireThat +import net.corda.core.transactions.LedgerTransaction + +class IOUContract : Contract { + // Our Create command. + class Create : CommandData + + override fun verify(tx: LedgerTransaction) { + val command = tx.commands.requireSingleCommand() + + requireThat { + // Constraints on the shape of the transaction. + "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty()) + "There should be one output state of type IOUState." using (tx.outputs.size == 1) + + // IOU-specific constraints. + val out = tx.outputsOfType().single() + "The IOU's value must be non-negative." using (out.value > 0) + "The lender and the borrower cannot be the same entity." using (out.lender != out.borrower) + + // Constraints on the signers. + "There must only be one signer." using (command.signers.toSet().size == 1) + "The signer must be the lender." using (command.signers.contains(out.lender.owningKey)) + } + } +} +// DOCEND 01 \ No newline at end of file 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/tutorial/helloworld/flow.kt new file mode 100644 index 0000000000..7bb068cba5 --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/flow.kt @@ -0,0 +1,52 @@ +package net.corda.docs.tutorial.helloworld + +// DOCSTART 01 +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Command +import net.corda.core.contracts.StateAndContract +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.ProgressTracker +import kotlin.reflect.jvm.jvmName + +@InitiatingFlow +@StartableByRPC +class IOUFlow(val iouValue: Int, + val otherParty: Party) : FlowLogic() { + + /** The progress tracker provides checkpoints indicating the progress of the flow to observers. */ + override val progressTracker = ProgressTracker() + + /** The flow logic is encapsulated within the call() method. */ + @Suspendable + override fun call() { + // We retrieve the notary identity from the network map. + val notary = serviceHub.networkMapCache.notaryIdentities[0] + + // We create a transaction builder + val txBuilder = TransactionBuilder(notary = notary) + + // We create the transaction components. + val outputState = IOUState(iouValue, ourIdentity, otherParty) + val outputContract = IOUContract::class.jvmName + val outputContractAndState = StateAndContract(outputState, outputContract) + val cmd = Command(IOUContract.Create(), ourIdentity.owningKey) + + // We add the items to the builder. + txBuilder.withItems(outputContractAndState, cmd) + + // Verifying the transaction. + txBuilder.verify(serviceHub) + + // Signing the transaction. + val signedTx = serviceHub.signInitialTransaction(txBuilder) + + // Finalising the transaction. + subFlow(FinalityFlow(signedTx)) + } +} +// DOCEND 01 \ No newline at end of file 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/tutorial/helloworld/state.kt new file mode 100644 index 0000000000..447265a2ae --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/state.kt @@ -0,0 +1,12 @@ +package net.corda.docs.tutorial.helloworld + +// DOCSTART 01 +import net.corda.core.contracts.ContractState +import net.corda.core.identity.Party + +class IOUState(val value: Int, + val lender: Party, + val borrower: Party) : ContractState { + override val participants get() = listOf(lender, borrower) +} +// DOCEND 01 \ No newline at end of file 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/tutorial/twoparty/contract.kt new file mode 100644 index 0000000000..25b5b3f6a1 --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/contract.kt @@ -0,0 +1,36 @@ +package net.corda.docs.tutorial.twoparty + +// DOCSTART 01 +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.Contract +import net.corda.core.contracts.requireSingleCommand +import net.corda.core.contracts.requireThat +import net.corda.core.transactions.LedgerTransaction +// DOCEND 01 + +class IOUContract : Contract { + // Our Create command. + class Create : CommandData + + override fun verify(tx: LedgerTransaction) { + val command = tx.commands.requireSingleCommand() + + requireThat { + // Constraints on the shape of the transaction. + "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty()) + "There should be one output state of type IOUState." using (tx.outputs.size == 1) + + // IOU-specific constraints. + val out = tx.outputsOfType().single() + "The IOU's value must be non-negative." using (out.value > 0) + "The lender and the borrower cannot be the same entity." using (out.lender != out.borrower) + + // DOCSTART 02 + // Constraints on the signers. + "There must be two signers." using (command.signers.toSet().size == 2) + "The borrower and lender must be signers." using (command.signers.containsAll(listOf( + out.borrower.owningKey, out.lender.owningKey))) + // DOCEND 02 + } + } +} \ No newline at end of file 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/tutorial/twoparty/flow.kt new file mode 100644 index 0000000000..0d8ac221ad --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt @@ -0,0 +1,57 @@ +package net.corda.docs.tutorial.twoparty + +// DOCSTART 01 +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Command +import net.corda.core.contracts.StateAndContract +import net.corda.core.flows.* +import net.corda.core.identity.Party +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.ProgressTracker +import kotlin.reflect.jvm.jvmName +// DOCEND 01 + +@InitiatingFlow +@StartableByRPC +class IOUFlow(val iouValue: Int, + val otherParty: Party) : FlowLogic() { + + /** The progress tracker provides checkpoints indicating the progress of the flow to observers. */ + override val progressTracker = ProgressTracker() + + /** The flow logic is encapsulated within the call() method. */ + @Suspendable + override fun call() { + // We retrieve the notary identity from the network map. + val notary = serviceHub.networkMapCache.notaryIdentities[0] + + // We create a transaction builder + val txBuilder = TransactionBuilder(notary = notary) + + // DOCSTART 02 + // We create the transaction components. + val outputState = IOUState(iouValue, ourIdentity, otherParty) + val outputContract = IOUContract::class.jvmName + val outputContractAndState = StateAndContract(outputState, outputContract) + val cmd = Command(IOUContract.Create(), listOf(ourIdentity.owningKey, otherParty.owningKey)) + + // We add the items to the builder. + txBuilder.withItems(outputContractAndState, cmd) + + // Verifying the transaction. + txBuilder.verify(serviceHub) + + // Signing the transaction. + val signedTx = serviceHub.signInitialTransaction(txBuilder) + + // Creating a session with the other party. + val otherpartySession = initiateFlow(otherParty) + + // Obtaining the counterparty's signature. + val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx, listOf(otherpartySession), CollectSignaturesFlow.tracker())) + + // Finalising the transaction. + subFlow(FinalityFlow(fullySignedTx)) + // DOCEND 02 + } +} \ No newline at end of file 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/tutorial/twoparty/flowResponder.kt new file mode 100644 index 0000000000..b8007cc2ec --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flowResponder.kt @@ -0,0 +1,30 @@ +package net.corda.docs.tutorial.twoparty + +// DOCSTART 01 +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.requireThat +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.core.transactions.SignedTransaction +import net.corda.docs.tutorial.helloworld.IOUFlow +import net.corda.docs.tutorial.helloworld.IOUState + +@InitiatedBy(IOUFlow::class) +class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + val signTransactionFlow = object : SignTransactionFlow(otherPartySession, SignTransactionFlow.tracker()) { + override fun checkTransaction(stx: SignedTransaction) = requireThat { + val output = stx.tx.outputs.single().data + "This must be an IOU transaction." using (output is IOUState) + val iou = output as IOUState + "The IOU's value can't be too high." using (iou.value < 100) + } + } + + subFlow(signTransactionFlow) + } +} +// DOCEND 01 \ No newline at end of file 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/tutorial/twoparty/state.kt new file mode 100644 index 0000000000..a690625d65 --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/state.kt @@ -0,0 +1,10 @@ +package net.corda.docs.tutorial.twoparty + +import net.corda.core.contracts.ContractState +import net.corda.core.identity.Party + +class IOUState(val value: Int, + val lender: Party, + val borrower: Party) : ContractState { + override val participants get() = listOf(lender, borrower) +} \ No newline at end of file diff --git a/docs/source/hello-world-contract.rst b/docs/source/hello-world-contract.rst index 7e367ff7eb..05ceaace01 100644 --- a/docs/source/hello-world-contract.rst +++ b/docs/source/hello-world-contract.rst @@ -78,80 +78,15 @@ Let's write a contract that enforces these constraints. We'll do this by modifyi .. container:: codeset - .. code-block:: kotlin + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/contract.kt + :language: kotlin + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - ... - - import net.corda.core.contracts.* - - ... - - class IOUContract : Contract { - // Our Create command. - class Create : CommandData - - override fun verify(tx: LedgerTransaction) { - val command = tx.commands.requireSingleCommand() - - requireThat { - // Constraints on the shape of the transaction. - "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty()) - "There should be one output state of type IOUState." using (tx.outputs.size == 1) - - // IOU-specific constraints. - val out = tx.outputs.single().data as IOUState - "The IOU's value must be non-negative." using (out.value > 0) - "The lender and the borrower cannot be the same entity." using (out.lender != out.borrower) - - // Constraints on the signers. - "There must only be one signer." using (command.signers.toSet().size == 1) - "The signer must be the lender." using (command.signers.contains(out.lender.owningKey)) - } - } - } - - .. code-block:: java - - package com.template.contract; - - import com.template.state.IOUState; - import net.corda.core.contracts.CommandWithParties; - import net.corda.core.contracts.CommandData; - import net.corda.core.contracts.Contract; - import net.corda.core.transactions.LedgerTransaction; - import net.corda.core.identity.Party; - - import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; - import static net.corda.core.contracts.ContractsDSL.requireThat; - - public class IOUContract implements Contract { - // Our Create command. - public static class Create implements CommandData {} - - @Override - public void verify(LedgerTransaction tx) { - final CommandWithParties command = requireSingleCommand(tx.getCommands(), Create.class); - - requireThat(check -> { - // Constraints on the shape of the transaction. - check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty()); - check.using("There should be one output state of type IOUState.", tx.getOutputs().size() == 1); - - // IOU-specific constraints. - final IOUState out = (IOUState) tx.getOutputs().get(0).getData(); - final Party lender = out.getLender(); - final Party borrower = out.getBorrower(); - check.using("The IOU's value must be non-negative.",out.getValue() > 0); - check.using("The lender and the borrower cannot be the same entity.", lender != borrower); - - // Constraints on the signers. - check.using("There must only be one signer.", command.getSigners().size() == 1); - check.using("The signer must be the lender.", command.getSigners().contains(lender.getOwningKey())); - - return null; - }); - } - } + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUContract.java + :language: java + :start-after: DOCSTART 01 + :end-before: DOCEND 01 If you're following along in Java, you'll also need to rename ``TemplateContract.java`` to ``IOUContract.java``. diff --git a/docs/source/hello-world-flow.rst b/docs/source/hello-world-flow.rst index 351888fd0a..bedcf6842a 100644 --- a/docs/source/hello-world-flow.rst +++ b/docs/source/hello-world-flow.rst @@ -33,128 +33,20 @@ FlowLogic Flows are implemented as ``FlowLogic`` subclasses. You define the steps taken by the flow by overriding ``FlowLogic.call``. -We'll write our flow in either ``TemplateFlow.java`` or ``App.kt``. Overwrite both the existing flows in the template -with the following: +We'll write our flow in either ``TemplateFlow.java`` or ``App.kt``. Delete both the existing flows in the template, and +replace them with the following: .. container:: codeset - .. code-block:: kotlin + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/flow.kt + :language: kotlin + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - ... - - import net.corda.core.utilities.ProgressTracker - import net.corda.core.transactions.TransactionBuilder - import net.corda.core.flows.* - - ... - - @InitiatingFlow - @StartableByRPC - class IOUFlow(val iouValue: Int, - val otherParty: Party) : FlowLogic() { - - /** The progress tracker provides checkpoints indicating the progress of the flow to observers. */ - override val progressTracker = ProgressTracker() - - /** The flow logic is encapsulated within the call() method. */ - @Suspendable - override fun call() { - // We retrieve the notary identity from the network map. - val notary = serviceHub.networkMapCache.notaryIdentities[0] - - // We create a transaction builder - val txBuilder = TransactionBuilder(notary = notary) - - // We create the transaction components. - val outputState = IOUState(iouValue, ourIdentity, otherParty) - val outputContract = IOUContract::class.jvmName - val outputContractAndState = StateAndContract(outputState, outputContract) - val cmd = Command(IOUContract.Create(), ourIdentity.owningKey) - - // We add the items to the builder. - txBuilder.withItems(outputContractAndState, cmd) - - // Verifying the transaction. - txBuilder.verify(serviceHub) - - // Signing the transaction. - val signedTx = serviceHub.signInitialTransaction(txBuilder) - - // Finalising the transaction. - subFlow(FinalityFlow(signedTx)) - } - } - - .. code-block:: java - - package com.template.flow; - - import co.paralleluniverse.fibers.Suspendable; - import com.template.contract.IOUContract; - import com.template.state.IOUState; - import net.corda.core.contracts.Command; - import net.corda.core.contracts.StateAndContract; - import net.corda.core.flows.*; - import net.corda.core.identity.Party; - import net.corda.core.transactions.SignedTransaction; - import net.corda.core.transactions.TransactionBuilder; - import net.corda.core.utilities.ProgressTracker; - - @InitiatingFlow - @StartableByRPC - public class IOUFlow extends FlowLogic { - private final Integer iouValue; - private final Party otherParty; - - /** - * The progress tracker provides checkpoints indicating the progress of the flow to observers. - */ - private final ProgressTracker progressTracker = new ProgressTracker(); - - public IOUFlow(Integer iouValue, Party otherParty) { - this.iouValue = iouValue; - this.otherParty = otherParty; - } - - @Override - public ProgressTracker getProgressTracker() { - return progressTracker; - } - - /** - * The flow logic is encapsulated within the call() method. - */ - @Suspendable - @Override - public Void call() throws FlowException { - // We retrieve the notary identity from the network map. - final Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0); - - // We create a transaction builder. - final TransactionBuilder txBuilder = new TransactionBuilder(); - txBuilder.setNotary(notary); - - // We create the transaction components. - IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty); - String outputContract = IOUContract.class.getName(); - StateAndContract outputContractAndState = new StateAndContract(outputState, outputContract); - Command cmd = new Command<>(new IOUContract.Create(), getOurIdentity().getOwningKey()); - - // We add the items to the builder. - txBuilder.withItems(outputContractAndState, cmd); - - // Verifying the transaction. - txBuilder.verify(getServiceHub()); - - // Signing the transaction. - final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); - - // Finalising the transaction. - subFlow(new FinalityFlow(signedTx)); - - return null; - } - } + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlow.java + :language: java + :start-after: DOCSTART 01 + :end-before: DOCEND 01 If you're following along in Java, you'll also need to rename ``TemplateFlow.java`` to ``IOUFlow.java``. diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index 72c573b789..377fd90e05 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -17,38 +17,36 @@ Kotlin) file. We won't be using it, and it will cause build errors unless we rem Deploying our CorDapp --------------------- Let's take a look at the nodes we're going to deploy. Open the project's ``build.gradle`` file and scroll down to the -``task deployNodes`` section. This section defines three nodes - the Controller, NodeA, and NodeB: +``task deployNodes`` section. This section defines three nodes - the Controller, PartyA, and PartyB: -.. container:: codeset +.. code:: bash - .. code-block:: kotlin - - task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { - directory "./build/nodes" - node { - name "O=Controller,L=London,C=GB" - notary = [validating : true] - p2pPort 10002 - rpcPort 10003 - cordapps = ["net.corda:corda-finance:$corda_release_version"] - } - node { - name "O=PartyA,L=London,C=GB" - p2pPort 10005 - rpcPort 10006 - webPort 10007 - cordapps = ["net.corda:corda-finance:$corda_release_version"] - rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] - } - node { - name "O=PartyB,L=New York,C=US" - p2pPort 10008 - rpcPort 10009 - webPort 10010 - cordapps = ["net.corda:corda-finance:$corda_release_version"] - rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] - } + task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { + directory "./build/nodes" + node { + name "O=Controller,L=London,C=GB" + advertisedServices = ["corda.notary.validating"] + p2pPort 10002 + rpcPort 10003 + cordapps = ["net.corda:corda-finance:$corda_release_version"] } + node { + name "O=PartyA,L=London,C=GB" + p2pPort 10005 + rpcPort 10006 + webPort 10007 + cordapps = ["net.corda:corda-finance:$corda_release_version"] + rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] + } + node { + name "O=PartyB,L=New York,C=US" + p2pPort 10008 + rpcPort 10009 + webPort 10010 + cordapps = ["net.corda:corda-finance:$corda_release_version"] + rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] + } + } We have three standard nodes, plus a special Controller node that is running the network map service, and is also advertising a validating notary service. Feel free to add additional node definitions here to expand the size of the @@ -61,7 +59,7 @@ We can run this ``deployNodes`` task using Gradle. For each node definition, Gra We can do that now by running the following commands from the root of the project: -.. code:: python +.. code:: bash // On Windows gradlew clean deployNodes @@ -74,20 +72,18 @@ Running the nodes Running ``deployNodes`` will build the nodes under ``build/nodes``. If we navigate to one of these folders, we'll see the three node folders. Each node folder has the following structure: - .. code:: python + .. code:: bash . |____corda.jar // The runnable node |____corda-webserver.jar // The node's webserver - |____dependencies |____node.conf // The node's configuration file - |____additional-node-infos/ // Directory containing all the other nodes' addresses and identities |____cordapps - |____java/kotlin-source-0.1.jar // Our IOU CorDapp + |____java/kotlin-source-0.1.jar // Our IOU CorDapp Let's start the nodes by running the following commands from the root of the project: -.. code:: python +.. code:: bash // On Windows build/nodes/runnodes.bat @@ -136,7 +132,7 @@ will display a list of the available commands. We can examine the contents of a The vaults of PartyA and PartyB should both display the following output: -.. code:: python +.. code:: bash states: - state: @@ -192,11 +188,9 @@ There are a number of improvements we could make to this CorDapp: * We could add an API, to make it easier to interact with the CorDapp We will explore some of these improvements in future tutorials. But you should now be ready to develop your own -CorDapps. There's `a more fleshed-out version of the IOU CorDapp `_ with an -API and web front-end, and a set of example CorDapps in `the main Corda repo `_, under -``samples``. An explanation of how to run these samples :doc:`here `. +CorDapps. You can find a list of sample CorDapps `here `_. -As you write CorDapps, you can learn more about the API available :doc:`here `. +As you write CorDapps, you can learn more about the Corda API :doc:`here `. If you get stuck at any point, please reach out on `Slack `_, `Discourse `_, or `Stack Overflow `_. diff --git a/docs/source/hello-world-state.rst b/docs/source/hello-world-state.rst index dbcbe7c4b7..a933cc920b 100644 --- a/docs/source/hello-world-state.rst +++ b/docs/source/hello-world-state.rst @@ -63,53 +63,15 @@ define an ``IOUState``: .. container:: codeset - .. code-block:: kotlin + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/helloworld/state.kt + :language: kotlin + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - class IOUState(val value: Int, - val lender: Party, - val borrower: Party) : ContractState { - override val participants get() = listOf(lender, borrower) - } - - .. code-block:: java - - package com.template.state; - - import com.google.common.collect.ImmutableList; - import net.corda.core.contracts.ContractState; - import net.corda.core.identity.AbstractParty; - import net.corda.core.identity.Party; - - import java.util.List; - - public class IOUState implements ContractState { - private final int value; - private final Party lender; - private final Party borrower; - - public IOUState(int value, Party lender, Party borrower) { - this.value = value; - this.lender = lender; - this.borrower = borrower; - } - - public int getValue() { - return value; - } - - public Party getLender() { - return lender; - } - - public Party getBorrower() { - return borrower; - } - - @Override - public List getParticipants() { - return ImmutableList.of(lender, borrower); - } - } + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUState.java + :language: java + :start-after: DOCSTART 01 + :end-before: DOCEND 01 If you're following along in Java, you'll also need to rename ``TemplateState.java`` to ``IOUState.java``. diff --git a/docs/source/hello-world-template.rst b/docs/source/hello-world-template.rst index 5a6e3920a1..d9358d60eb 100644 --- a/docs/source/hello-world-template.rst +++ b/docs/source/hello-world-template.rst @@ -42,18 +42,23 @@ implement our IOU CorDapp in Java, we'll need to modify three files. For Kotlin, .. code-block:: java // 1. The state - src/main/java/com/template/state/TemplateState.java + src/main/java/com/template/TemplateState.java // 2. The contract - src/main/java/com/template/contract/TemplateContract.java + src/main/java/com/template/TemplateContract.java // 3. The flow - src/main/java/com/template/flow/TemplateFlow.java + src/main/java/com/template/TemplateFlow.java .. code-block:: kotlin src/main/kotlin/com/template/App.kt +To prevent build errors later on, you should delete the following file: + +* Java: ``src/test/java/com/template/FlowTests.java`` +* Kotlin: ``src/test/kotlin/com/template/FlowTests.kt`` + Progress so far --------------- We now have a template that we can build upon to define our IOU CorDapp. diff --git a/docs/source/tut-two-party-contract.rst b/docs/source/tut-two-party-contract.rst index 077133b378..2e59e2f0ef 100644 --- a/docs/source/tut-two-party-contract.rst +++ b/docs/source/tut-two-party-contract.rst @@ -11,31 +11,37 @@ Remember that each state references a contract. The contract imposes constraints If the transaction does not obey the constraints of all the contracts of all its states, it cannot become a valid ledger update. -We need to modify our contract so that the borrower's signature is required in any IOU creation transaction. This will -only require changing a single line of code. In ``IOUContract.java``/``IOUContract.kt``, update the final two lines of -the ``requireThat`` block as follows: +We need to modify our contract so that the borrower's signature is required in any IOU creation transaction. + +In ``IOUContract.java``/``IOUContract.kt``, change the imports block to the following: .. container:: codeset - .. code-block:: kotlin + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/contract.kt + :language: kotlin + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - // Constraints on the signers. - "There must be two signers." using (command.signers.toSet().size == 2) - "The borrower and lender must be signers." using (command.signers.containsAll(listOf( - out.borrower.owningKey, out.lender.owningKey))) + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUContract.java + :language: java + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - .. code-block:: java +And update the final block of constraints in the ``requireThat`` block as follows: - ... +.. container:: codeset - import com.google.common.collect.ImmutableList; + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/contract.kt + :language: kotlin + :start-after: DOCSTART 02 + :end-before: DOCEND 02 + :dedent: 12 - ... - - // Constraints on the signers. - check.using("There must be two signers.", command.getSigners().size() == 2); - check.using("The borrower and lender must be signers.", command.getSigners().containsAll( - ImmutableList.of(borrower.getOwningKey(), lender.getOwningKey()))); + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUContract.java + :language: java + :start-after: DOCSTART 02 + :end-before: DOCEND 02 + :dedent: 12 Progress so far --------------- diff --git a/docs/source/tut-two-party-flow.rst b/docs/source/tut-two-party-flow.rst index b19630eb32..4d5866b2c8 100644 --- a/docs/source/tut-two-party-flow.rst +++ b/docs/source/tut-two-party-flow.rst @@ -21,76 +21,35 @@ by invoking a built-in flow called ``FinalityFlow`` as a subflow. We're going to We also need to add the borrower's public key to the transaction's command, making the borrower one of the required signers on the transaction. -In ``IOUFlow.java``/``IOUFlow.kt``, update ``IOUFlow.call`` as follows: +In ``IOUFlow.java``/``IOUFlow.kt``, change the imports block to the following: .. container:: codeset - .. code-block:: kotlin + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt + :language: kotlin + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - ... + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java + :language: java + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - // We create the transaction components. - val outputState = IOUState(iouValue, ourIdentity, otherParty) - val outputContract = IOUContract::class.jvmName - val outputContractAndState = StateAndContract(outputState, outputContract) - val cmd = Command(IOUContract.Create(), listOf(ourIdentity.owningKey, otherParty.owningKey)) +And update ``IOUFlow.call`` by changing the code following the creation of the ``TransactionBuilder`` as follows: - // We add the items to the builder. - txBuilder.withItems(outputContractAndState, cmd) +.. container:: codeset - // Verifying the transaction. - txBuilder.verify(serviceHub) + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flow.kt + :language: kotlin + :start-after: DOCSTART 02 + :end-before: DOCEND 02 + :dedent: 8 - // Signing the transaction. - val signedTx = serviceHub.signInitialTransaction(txBuilder) - - // Creating a session with the other party. - val otherpartySession = initiateFlow(otherParty) - - // Obtaining the counterparty's signature. - val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx, listOf(otherpartySession), CollectSignaturesFlow.tracker())) - - // Finalising the transaction. - subFlow(FinalityFlow(fullySignedTx)) - - .. code-block:: java - - ... - - import com.google.common.collect.ImmutableList; - import java.security.PublicKey; - import java.util.Collections; - import java.util.List; - - ... - - // We create the transaction components. - IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty); - String outputContract = IOUContract.class.getName(); - StateAndContract outputContractAndState = new StateAndContract(outputState, outputContract); - List requiredSigners = ImmutableList.of(getOurIdentity().getOwningKey(), otherParty.getOwningKey()); - Command cmd = new Command<>(new IOUContract.Create(), requiredSigners); - - // We add the items to the builder. - txBuilder.withItems(outputContractAndState, cmd); - - // Verifying the transaction. - txBuilder.verify(getServiceHub()); - - // Signing the transaction. - final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); - - // Creating a session with the other party. - FlowSession otherpartySession = initiateFlow(otherParty); - - // Obtaining the counterparty's signature. - SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow( - signedTx, ImmutableList.of(otherpartySession), CollectSignaturesFlow.tracker())); - - // Finalising the transaction. - subFlow(new FinalityFlow(signedTx)); - - return null; + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java + :language: java + :start-after: DOCSTART 02 + :end-before: DOCEND 02 + :dedent: 8 To make the borrower a required signer, we simply add the borrower's public key to the list of signers on the command. @@ -116,81 +75,15 @@ In a new ``IOUFlowResponder.java`` file in Java, or within the ``App.kt`` file i .. container:: codeset - .. code-block:: kotlin + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/tutorial/twoparty/flowResponder.kt + :language: kotlin + :start-after: DOCSTART 01 + :end-before: DOCEND 01 - ... - - import net.corda.core.transactions.SignedTransaction - - ... - - @InitiatedBy(IOUFlow::class) - class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic() { - @Suspendable - override fun call() { - val signTransactionFlow = object : SignTransactionFlow(otherPartySession, SignTransactionFlow.tracker()) { - override fun checkTransaction(stx: SignedTransaction) = requireThat { - val output = stx.tx.outputs.single().data - "This must be an IOU transaction." using (output is IOUState) - val iou = output as IOUState - "The IOU's value can't be too high." using (iou.value < 100) - } - } - - subFlow(signTransactionFlow) - } - } - - .. code-block:: java - - package com.template.flow; - - import co.paralleluniverse.fibers.Suspendable; - import com.template.state.IOUState; - import net.corda.core.contracts.ContractState; - import net.corda.core.flows.FlowException; - 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.core.transactions.SignedTransaction; - import net.corda.core.utilities.ProgressTracker; - - import static net.corda.core.contracts.ContractsDSL.requireThat; - - @InitiatedBy(IOUFlow.class) - public class IOUFlowResponder extends FlowLogic { - private final FlowSession otherPartySession; - - public IOUFlowResponder(FlowSession otherPartySession) { - this.otherPartySession = otherPartySession; - } - - @Suspendable - @Override - public Void call() throws FlowException { - class SignTxFlow extends SignTransactionFlow { - private signTxFlow(FlowSession otherPartySession, ProgressTracker progressTracker) { - super(otherPartySession, progressTracker); - } - - @Override - protected void checkTransaction(SignedTransaction stx) { - requireThat(require -> { - ContractState output = stx.getTx().getOutputs().get(0).getData(); - require.using("This must be an IOU transaction.", output instanceof IOUState); - IOUState iou = (IOUState) output; - require.using("The IOU's value can't be too high.", iou.getValue() < 100); - return null; - }); - } - } - - subFlow(new SignTxFlow(otherPartySession, SignTransactionFlow.Companion.tracker())); - - return null; - } - } + .. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlowResponder.java + :language: java + :start-after: DOCSTART 01 + :end-before: DOCEND 01 As with the ``IOUFlow``, our ``IOUFlowResponder`` flow is a ``FlowLogic`` subclass where we've overridden ``FlowLogic.call``. diff --git a/docs/source/tut-two-party-introduction.rst b/docs/source/tut-two-party-introduction.rst index 95ad302e2c..66f4f14acc 100644 --- a/docs/source/tut-two-party-introduction.rst +++ b/docs/source/tut-two-party-introduction.rst @@ -20,4 +20,4 @@ IOU onto the ledger. We'll need to make two changes: signature (as well as the lender's) to become valid ledger updates * The ``IOUFlow`` will need to be updated to allow for the gathering of the borrower's signature -We'll start by updating the contract. +We'll start by updating the contract. \ No newline at end of file From d8b81f755a89f663b90c2aaef0ab832138129a56 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Mon, 16 Oct 2017 15:15:02 +0100 Subject: [PATCH 183/185] Makes docs fixes flagged by various internals. --- docs/source/node-services.rst | 6 ++---- docs/source/resources/node-architecture.png | Bin 371367 -> 289224 bytes docs/source/upgrade-notes.rst | 2 ++ 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/node-services.rst b/docs/source/node-services.rst index d62edd2548..b63dbfc942 100644 --- a/docs/source/node-services.rst +++ b/docs/source/node-services.rst @@ -209,10 +209,8 @@ NodeAttachmentService The ``NodeAttachmentService`` provides an implementation of the ``AttachmentStorage`` interface exposed on the ``ServiceHub`` allowing transactions to add documents, copies of the contract code and binary -data to transactions. The data is persisted to the local file system -inside the attachments subfolder of the node workspace. The service is -also interfaced to by the web server, which allows files to be uploaded -via an HTTP post request. +data to transactions. The service is also interfaced to by the web server, +which allows files to be uploaded via an HTTP post request. Flow framework and event scheduling services -------------------------------------------- diff --git a/docs/source/resources/node-architecture.png b/docs/source/resources/node-architecture.png index cfc8b22cf7716e73509815af419d96dab7e67fa1..7004cda149e3924fa4831db8366d201a022eabbb 100644 GIT binary patch literal 289224 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