From d42b9f51ac31d2479a86836fe5797a95093fd6fd Mon Sep 17 00:00:00 2001 From: Jiachuan Li Date: Tue, 31 Jul 2018 22:36:25 +0800 Subject: [PATCH 1/9] Miner change to mention notary node dosen't need web server, and also change the total terminal windows number from 8 to 5 (#3012) * There is a duplication of the kotlin introduce part, which is the same as the description of the "Note" section right below. So remove this duplicated part but keep the "Note". * A miner change about the build node folder structure, first is Notary node doesn't need one web server, second is in V3.0, there are 5 windows totally but not 8. * Miner updates for the format of changes, as instructed by Joel on https://github.com/corda/corda/pull/3012 --- docs/source/hello-world-running.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index d079d6d0ee..1aeb15eb58 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -70,7 +70,7 @@ the three node folders. Each node folder has the following structure: . |____corda.jar // The runnable node - |____corda-webserver.jar // The node's webserver + |____corda-webserver.jar // The node's webserver (The notary doesn't need a web server) |____node.conf // The node's configuration file |____cordapps |____java/kotlin-source-0.1.jar // Our IOU CorDapp @@ -85,7 +85,7 @@ Let's start the nodes by running the following commands from the root of the pro // On Mac build/nodes/runnodes -This will start a terminal window for each node, and an additional terminal window for each node's webserver - eight +This will start a terminal window for each node, and an additional terminal window for each node's webserver - five terminal windows in all. Give each node a moment to start - you'll know it's ready when its terminal windows displays the message, "Welcome to the Corda interactive shell.". From 994fe0dbdc6214e44e363c66a7a164832913c952 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Tue, 31 Jul 2018 16:07:35 +0100 Subject: [PATCH 2/9] CORDA-1845: Check for min plaform version of 4 when building transactions with reference states (#3705) Also includes some minor cleanup brought up in a previous PR. --- .../net/corda/core/internal/CordaUtils.kt | 57 +++++++++++++ .../net/corda/core/internal/InternalUtils.kt | 28 ------ .../net/corda/core/node/NetworkParameters.kt | 9 +- .../core/transactions/TransactionBuilder.kt | 49 ++++++++--- .../transactions/TransactionBuilderTest.kt | 85 +++++++++++++++++++ docs/source/changelog.rst | 3 +- .../net/corda/node/internal/AbstractNode.kt | 13 ++- .../net/corda/node/internal/NodeStartup.kt | 6 +- .../network/PersistentNetworkMapCache.kt | 4 +- 9 files changed, 198 insertions(+), 56 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt create mode 100644 core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt diff --git a/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt b/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt new file mode 100644 index 0000000000..35ea7158ef --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt @@ -0,0 +1,57 @@ +package net.corda.core.internal + +import net.corda.core.DeleteForDJVM +import net.corda.core.cordapp.Cordapp +import net.corda.core.cordapp.CordappConfig +import net.corda.core.cordapp.CordappContext +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FlowLogic +import net.corda.core.node.ServicesForResolution +import net.corda.core.node.ZoneVersionTooLowException +import net.corda.core.serialization.SerializationContext +import net.corda.core.transactions.LedgerTransaction +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.transactions.WireTransaction +import org.slf4j.MDC + +// *Internal* Corda-specific utilities + +fun ServicesForResolution.ensureMinimumPlatformVersion(requiredMinPlatformVersion: Int, feature: String) { + val currentMinPlatformVersion = networkParameters.minimumPlatformVersion + if (currentMinPlatformVersion < requiredMinPlatformVersion) { + throw ZoneVersionTooLowException( + "$feature requires all nodes on the Corda compatibility zone to be running at least platform version " + + "$requiredMinPlatformVersion. The current zone is only enforcing a minimum platform version of " + + "$currentMinPlatformVersion. Please contact your zone operator." + ) + } +} + +/** Provide access to internal method for AttachmentClassLoaderTests */ +@DeleteForDJVM +fun TransactionBuilder.toWireTransaction(services: ServicesForResolution, serializationContext: SerializationContext): WireTransaction { + return toWireTransactionWithContext(services, serializationContext) +} + +/** Provide access to internal method for AttachmentClassLoaderTests */ +@DeleteForDJVM +fun TransactionBuilder.toLedgerTransaction(services: ServicesForResolution, serializationContext: SerializationContext): LedgerTransaction { + return toLedgerTransactionWithContext(services, serializationContext) +} + +fun createCordappContext(cordapp: Cordapp, attachmentId: SecureHash?, classLoader: ClassLoader, config: CordappConfig): CordappContext { + return CordappContext(cordapp, attachmentId, classLoader, config) +} + +/** Checks if this flow is an idempotent flow. */ +fun Class>.isIdempotentFlow(): Boolean { + return IdempotentFlow::class.java.isAssignableFrom(this) +} + +/** + * Ensures each log entry from the current thread will contain id of the transaction in the MDC. + */ +internal fun SignedTransaction.pushToLoggingContext() { + MDC.put("tx_id", id.toString()) +} 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 6a53f25755..9626cf88a0 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -388,18 +388,6 @@ fun uncheckedCast(obj: T) = obj as U fun Iterable>.toMultiMap(): Map> = this.groupBy({ it.first }) { it.second } -/** Provide access to internal method for AttachmentClassLoaderTests */ -@DeleteForDJVM -fun TransactionBuilder.toWireTransaction(services: ServicesForResolution, serializationContext: SerializationContext): WireTransaction { - return toWireTransactionWithContext(services, serializationContext) -} - -/** Provide access to internal method for AttachmentClassLoaderTests */ -@DeleteForDJVM -fun TransactionBuilder.toLedgerTransaction(services: ServicesForResolution, serializationContext: SerializationContext): LedgerTransaction { - return toLedgerTransactionWithContext(services, serializationContext) -} - /** Returns the location of this class. */ val Class<*>.location: URL get() = protectionDomain.codeSource.location @@ -499,29 +487,13 @@ fun SerializedBytes.sign(keyPair: KeyPair): SignedData = SignedD fun ByteBuffer.copyBytes(): ByteArray = ByteArray(remaining()).also { get(it) } -fun createCordappContext(cordapp: Cordapp, attachmentId: SecureHash?, classLoader: ClassLoader, config: CordappConfig): CordappContext { - return CordappContext(cordapp, attachmentId, classLoader, config) -} - val PublicKey.hash: SecureHash get() = encoded.sha256() -/** Checks if this flow is an idempotent flow. */ -fun Class>.isIdempotentFlow(): Boolean { - return IdempotentFlow::class.java.isAssignableFrom(this) -} - /** * Extension method for providing a sumBy method that processes and returns a Long */ fun Iterable.sumByLong(selector: (T) -> Long): Long = this.map { selector(it) }.sum() -/** - * Ensures each log entry from the current thread will contain id of the transaction in the MDC. - */ -internal fun SignedTransaction.pushToLoggingContext() { - MDC.put("tx_id", id.toString()) -} - fun SerializedBytes.checkPayloadIs(type: Class): UntrustworthyData { val payloadData: T = try { val serializer = SerializationDefaults.SERIALIZATION_FACTORY diff --git a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt index 1b8618500d..7127177c71 100644 --- a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt +++ b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt @@ -1,5 +1,6 @@ package net.corda.core.node +import net.corda.core.CordaRuntimeException import net.corda.core.KeepForDJVM import net.corda.core.identity.Party import net.corda.core.node.services.AttachmentId @@ -105,4 +106,10 @@ data class NetworkParameters( */ @KeepForDJVM @CordaSerializable -data class NotaryInfo(val identity: Party, val validating: Boolean) \ No newline at end of file +data class NotaryInfo(val identity: Party, val validating: Boolean) + +/** + * When a Corda feature cannot be used due to the node's compatibility zone not enforcing a high enough minimum platform + * version. + */ +class ZoneVersionTooLowException(message: String) : CordaRuntimeException(message) 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 0331d54dbc..831a6e098e 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -1,6 +1,7 @@ package net.corda.core.transactions import co.paralleluniverse.strands.Strand +import net.corda.core.CordaInternal import net.corda.core.DeleteForDJVM import net.corda.core.contracts.* import net.corda.core.cordapp.CordappProvider @@ -9,9 +10,11 @@ import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignatureMetadata import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachine +import net.corda.core.internal.ensureMinimumPlatformVersion import net.corda.core.node.NetworkParameters import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution +import net.corda.core.node.ZoneVersionTooLowException import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.SerializationContext @@ -74,7 +77,7 @@ open class TransactionBuilder @JvmOverloads constructor( for (t in items) { when (t) { is StateAndRef<*> -> addInputState(t) - is ReferencedStateAndRef<*> -> @Suppress("DEPRECATION") addReferenceState(t) // Will remove when feature finalised. + is ReferencedStateAndRef<*> -> addReferenceState(t) is SecureHash -> addAttachment(t) is TransactionState<*> -> addOutputState(t) is StateAndContract -> addOutputState(t.state, t.contract) @@ -95,11 +98,18 @@ open class TransactionBuilder @JvmOverloads constructor( * [HashAttachmentConstraint]. * * @returns A new [WireTransaction] that will be unaffected by further changes to this [TransactionBuilder]. + * + * @throws ZoneVersionTooLowException if there are reference states and the zone minimum platform version is less than 4. */ @Throws(MissingContractAttachments::class) fun toWireTransaction(services: ServicesForResolution): WireTransaction = toWireTransactionWithContext(services) + @CordaInternal internal fun toWireTransactionWithContext(services: ServicesForResolution, serializationContext: SerializationContext? = null): WireTransaction { + val referenceStates = referenceStates() + if (referenceStates.isNotEmpty()) { + services.ensureMinimumPlatformVersion(4, "Reference states") + } // Resolves the AutomaticHashConstraints to HashAttachmentConstraints or WhitelistedByZoneAttachmentConstraint based on a global parameter. // The AutomaticHashConstraint allows for less boiler plate when constructing transactions since for the typical case the named contract @@ -109,14 +119,27 @@ open class TransactionBuilder @JvmOverloads constructor( when { state.constraint !== AutomaticHashConstraint -> state useWhitelistedByZoneAttachmentConstraint(state.contract, services.networkParameters) -> state.copy(constraint = WhitelistedByZoneAttachmentConstraint) - else -> services.cordappProvider.getContractAttachmentID(state.contract)?.let { - state.copy(constraint = HashAttachmentConstraint(it)) - } ?: throw MissingContractAttachments(listOf(state)) + else -> { + services.cordappProvider.getContractAttachmentID(state.contract)?.let { + state.copy(constraint = HashAttachmentConstraint(it)) + } ?: throw MissingContractAttachments(listOf(state)) + } } } return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) { - WireTransaction(WireTransaction.createComponentGroups(inputStates(), resolvedOutputs, commands, attachments + makeContractAttachments(services.cordappProvider), notary, window, referenceStates()), privacySalt) + WireTransaction( + WireTransaction.createComponentGroups( + inputStates(), + resolvedOutputs, + commands, + attachments + makeContractAttachments(services.cordappProvider), + notary, + window, + referenceStates + ), + privacySalt + ) } } @@ -169,12 +192,9 @@ open class TransactionBuilder @JvmOverloads constructor( /** * Adds a reference input [StateRef] to the transaction. * - * This feature was added in version 4 of Corda, so will throw an exception for any Corda networks with a minimum - * platform version less than 4. - * - * @throws UncheckedVersionException + * Note: Reference states are only supported on Corda networks running a minimum platform version of 4. + * [toWireTransaction] will throw an [IllegalStateException] if called in such an environment. */ - @Deprecated(message = "Feature not yet released. Pending stabilisation.") open fun addReferenceState(referencedStateAndRef: ReferencedStateAndRef<*>): TransactionBuilder { val stateAndRef = referencedStateAndRef.stateAndRef referencesWithTransactionState.add(stateAndRef.state) @@ -283,10 +303,10 @@ open class TransactionBuilder @JvmOverloads constructor( return this } - /** Returns an immutable list of input [StateRefs]. */ + /** Returns an immutable list of input [StateRef]s. */ fun inputStates(): List = ArrayList(inputs) - /** Returns an immutable list of reference input [StateRefs]. */ + /** Returns an immutable list of reference input [StateRef]s. */ fun referenceStates(): List = ArrayList(references) /** Returns an immutable list of attachment hashes. */ @@ -302,7 +322,10 @@ open class TransactionBuilder @JvmOverloads constructor( * Sign the built transaction and return it. This is an internal function for use by the service hub, please use * [ServiceHub.signInitialTransaction] instead. */ - fun toSignedTransaction(keyManagementService: KeyManagementService, publicKey: PublicKey, signatureMetadata: SignatureMetadata, services: ServicesForResolution): SignedTransaction { + fun toSignedTransaction(keyManagementService: KeyManagementService, + publicKey: PublicKey, + signatureMetadata: SignatureMetadata, + services: ServicesForResolution): SignedTransaction { val wtx = toWireTransaction(services) val signableData = SignableData(wtx.id, signatureMetadata) val sig = keyManagementService.sign(signableData, publicKey) diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt new file mode 100644 index 0000000000..c24a485776 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt @@ -0,0 +1,85 @@ +package net.corda.core.transactions + +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.contracts.* +import net.corda.core.cordapp.CordappProvider +import net.corda.core.crypto.SecureHash +import net.corda.core.node.ServicesForResolution +import net.corda.core.node.ZoneVersionTooLowException +import net.corda.testing.common.internal.testNetworkParameters +import net.corda.testing.contracts.DummyContract +import net.corda.testing.contracts.DummyState +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.core.DummyCommandData +import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.core.TestIdentity +import net.corda.testing.internal.rigorousMock +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class TransactionBuilderTest { + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() + + private val notary = TestIdentity(DUMMY_NOTARY_NAME).party + private val services = rigorousMock() + private val contractAttachmentId = SecureHash.randomSHA256() + + @Before + fun setup() { + val cordappProvider = rigorousMock() + doReturn(cordappProvider).whenever(services).cordappProvider + doReturn(contractAttachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID) + doReturn(testNetworkParameters()).whenever(services).networkParameters + } + + @Test + fun `bare minimum issuance tx`() { + val outputState = TransactionState( + data = DummyState(), + contract = DummyContract.PROGRAM_ID, + notary = notary, + constraint = HashAttachmentConstraint(contractAttachmentId) + ) + val builder = TransactionBuilder() + .addOutputState(outputState) + .addCommand(DummyCommandData, notary.owningKey) + val wtx = builder.toWireTransaction(services) + assertThat(wtx.outputs).containsOnly(outputState) + assertThat(wtx.commands).containsOnly(Command(DummyCommandData, notary.owningKey)) + } + + @Test + fun `automatic hash constraint`() { + val outputState = TransactionState(data = DummyState(), contract = DummyContract.PROGRAM_ID, notary = notary) + val builder = TransactionBuilder() + .addOutputState(outputState) + .addCommand(DummyCommandData, notary.owningKey) + val wtx = builder.toWireTransaction(services) + assertThat(wtx.outputs).containsOnly(outputState.copy(constraint = HashAttachmentConstraint(contractAttachmentId))) + } + + @Test + fun `reference states`() { + val referenceState = TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary) + val referenceStateRef = StateRef(SecureHash.randomSHA256(), 1) + val builder = TransactionBuilder(notary) + .addReferenceState(StateAndRef(referenceState, referenceStateRef).referenced()) + .addOutputState(TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary)) + .addCommand(DummyCommandData, notary.owningKey) + + doReturn(testNetworkParameters(minimumPlatformVersion = 3)).whenever(services).networkParameters + assertThatThrownBy { builder.toWireTransaction(services) } + .isInstanceOf(ZoneVersionTooLowException::class.java) + .hasMessageContaining("Reference states") + + doReturn(testNetworkParameters(minimumPlatformVersion = 4)).whenever(services).networkParameters + val wtx = builder.toWireTransaction(services) + assertThat(wtx.references).containsOnly(referenceStateRef) + } +} diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index f9e3ffacd8..e3499fb54b 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -188,7 +188,8 @@ Unreleased to in a transaction by the contracts of input and output states but whose contract is not executed as part of the transaction verification process and is not consumed when the transaction is committed to the ledger but is checked for "current-ness". In other words, the contract logic isn't run for the referencing transaction only. It's still a - normal state when it occurs in an input or output position. + normal state when it occurs in an input or output position. *This feature is only available on Corda networks running + with a minimum platform version of 4.* .. _changelog_v3.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 258755982f..617e3a2455 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -220,7 +220,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, private var _started: S? = null private fun T.tokenize(): T { - tokenizableServices?.add(this) ?: throw IllegalStateException("The tokenisable services list has already been finialised") + tokenizableServices?.add(this) ?: throw IllegalStateException("The tokenisable services list has already been finalised") return this } @@ -239,10 +239,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, private fun initKeyStore(): X509Certificate { if (configuration.devMode) { - log.warn("The Corda node is running in developer mode. This is not suitable for production usage.") configuration.configureWithDevSSLCertificate() - } else { - log.info("The Corda node is running in production mode. If this is a developer environment you can set 'devMode=true' in the node.conf file.") } return validateKeyStore() } @@ -317,12 +314,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } val (nodeInfo, signedNodeInfo) = nodeInfoAndSigned + services.start(nodeInfo, netParams) networkMapUpdater.start(trustRoot, signedNetParams.raw.hash, signedNodeInfo.raw.hash) startMessagingService(rpcOps, nodeInfo, myNotaryIdentity, netParams) // Do all of this in a database transaction so anything that might need a connection has one. return database.transaction { - services.start(nodeInfo, netParams) identityService.loadIdentities(nodeInfo.legalIdentitiesAndCerts) attachments.start() cordappProvider.start(netParams.whitelistedContractImplementations) @@ -731,7 +728,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, protected open fun startDatabase() { val props = configuration.dataSourceProperties if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.") - database.hikariStart(props) + database.startHikariPool(props) // Now log the vendor string as this will also cause a connection to be tested eagerly. logVendorString(database, log) } @@ -994,7 +991,7 @@ fun configureDatabase(hikariProperties: Properties, wellKnownPartyFromAnonymous: (AbstractParty) -> Party?, schemaService: SchemaService = NodeSchemaService()): CordaPersistence { val persistence = createCordaPersistence(databaseConfig, wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous, schemaService, hikariProperties) - persistence.hikariStart(hikariProperties) + persistence.startHikariPool(hikariProperties) return persistence } @@ -1013,7 +1010,7 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig, return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, jdbcUrl, attributeConverters) } -fun CordaPersistence.hikariStart(hikariProperties: Properties) { +fun CordaPersistence.startHikariPool(hikariProperties: Properties) { try { start(DataSourceFactory.createDataSource(hikariProperties)) } catch (ex: Exception) { 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 ac8d29d96b..fddaeb47eb 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -27,9 +27,9 @@ import net.corda.node.utilities.registration.UnableToRegisterNodeWithDoormanExce import net.corda.node.utilities.saveToKeyStore import net.corda.node.utilities.saveToTrustStore import net.corda.nodeapi.internal.addShutdownHook -import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException +import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException import net.corda.tools.shell.InteractiveShell import org.fusesource.jansi.Ansi import org.fusesource.jansi.AnsiConsole @@ -319,6 +319,8 @@ open class NodeStartup(val args: Array) { Emoji.renderIfSupported { Node.printWarning("This node is running in developer mode! ${Emoji.developer} This is not safe for production deployment.") } + } else { + logger.info("The Corda node is running in production mode. If this is a developer environment you can set 'devMode=true' in the node.conf file.") } val nodeInfo = node.start() @@ -332,7 +334,7 @@ open class NodeStartup(val args: Array) { if (conf.shouldStartLocalShell()) { node.startupComplete.then { try { - InteractiveShell.runLocalShell({ node.stop() }) + InteractiveShell.runLocalShell(node::stop) } catch (e: Throwable) { logger.error("Shell failed to start", e) } 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 1016e34cde..0fe84c9d3a 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 @@ -70,9 +70,7 @@ class NetworkMapCacheImpl( } } -/** - * Extremely simple in-memory cache of the network map. - */ +/** Database-based network map cache. */ @ThreadSafe open class PersistentNetworkMapCache(private val database: CordaPersistence) : SingletonSerializeAsToken(), NetworkMapCacheBaseInternal { companion object { From dfafdbcb9f2e48be988c99e30f8f4a1b6bedc523 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Tue, 31 Jul 2018 17:01:39 +0100 Subject: [PATCH 3/9] Added checks against the use of the special uploader tokens (#3724) "app", "rpc", "p2p" and "unknown" have security implications (see isUploaderTrusted method) and thus they are not allowed to be used in the uploader field when importing attachments via the public API. --- .../common/TransactionVerificationRequest.kt | 7 +- .../corda/core/internal/AbstractAttachment.kt | 7 +- docs/source/changelog.rst | 3 + .../net/corda/node/internal/AbstractNode.kt | 2 +- .../corda/node/internal/CordaRPCOpsImpl.kt | 29 ++----- .../internal/cordapp/CordappProviderImpl.kt | 9 +- .../node/services/api/ServiceHubInternal.kt | 2 + .../persistence/AttachmentStorageInternal.kt | 13 +++ .../persistence/NodeAttachmentService.kt | 47 ++++++---- ...geTest.kt => NodeAttachmentServiceTest.kt} | 85 +++++++++++++++---- .../node/internal/InternalMockNetwork.kt | 4 +- .../kotlin/net/corda/testing/dsl/TestDSL.kt | 2 +- .../testing/internal/MockCordappProvider.kt | 5 +- 13 files changed, 148 insertions(+), 67 deletions(-) create mode 100644 node/src/main/kotlin/net/corda/node/services/persistence/AttachmentStorageInternal.kt rename node/src/test/kotlin/net/corda/node/services/persistence/{NodeAttachmentStorageTest.kt => NodeAttachmentServiceTest.kt} (77%) diff --git a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/TransactionVerificationRequest.kt b/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/TransactionVerificationRequest.kt index 247e15c464..f2c825ba61 100644 --- a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/TransactionVerificationRequest.kt +++ b/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/TransactionVerificationRequest.kt @@ -3,7 +3,7 @@ package net.corda.deterministic.common import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.ContractClassName -import net.corda.core.internal.TEST_UPLOADER +import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize @@ -18,8 +18,9 @@ class TransactionVerificationRequest(val wtxToVerify: SerializedBytes() } - val attachmentMap = attachments.mapNotNull { it as? MockContractAttachment } - .associateBy(Attachment::id) { ContractAttachment(it, it.contract, uploader=TEST_UPLOADER) } + val attachmentMap = attachments + .mapNotNull { it as? MockContractAttachment } + .associateBy(Attachment::id) { ContractAttachment(it, it.contract, uploader = DEPLOYED_CORDAPP_UPLOADER) } val contractAttachmentMap = emptyMap() @Suppress("DEPRECATION") return wtxToVerify.deserialize().toLedgerTransaction( diff --git a/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt b/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt index b86890eb70..94a1789ba5 100644 --- a/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt +++ b/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt @@ -16,15 +16,14 @@ import java.security.CodeSigner import java.security.cert.X509Certificate import java.util.jar.JarInputStream -// Possible attachment uploaders const val DEPLOYED_CORDAPP_UPLOADER = "app" const val RPC_UPLOADER = "rpc" -const val TEST_UPLOADER = "test" const val P2P_UPLOADER = "p2p" const val UNKNOWN_UPLOADER = "unknown" -fun isUploaderTrusted(uploader: String?) = - uploader?.let { it in listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER, TEST_UPLOADER) } ?: false +private val TRUSTED_UPLOADERS = listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER) + +fun isUploaderTrusted(uploader: String?): Boolean = uploader in TRUSTED_UPLOADERS @KeepForDJVM abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment { diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index e3499fb54b..426b192264 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -8,6 +8,9 @@ Unreleased ---------- * Added ``registerResponderFlow`` method to ``StartedMockNode``, to support isolated testing of responder flow behaviour. +* "app", "rpc", "p2p" and "unknown" are no longer allowed as uploader values when importing attachments. These are used + internally in security sensitive code. + * Introduced ``TestCorDapp`` and utilities to support asymmetric setups for nodes through ``DriverDSL``, ``MockNetwork`` and ``MockServices``. * Change type of the `checkpoint_value` column. Please check the upgrade-notes on how to update your database. 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 617e3a2455..45f9385275 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -894,7 +894,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, override val transactionVerifierService: TransactionVerifierService get() = this@AbstractNode.transactionVerifierService override val contractUpgradeService: ContractUpgradeService get() = this@AbstractNode.contractUpgradeService override val auditService: AuditService get() = this@AbstractNode.auditService - override val attachments: AttachmentStorage get() = this@AbstractNode.attachments + override val attachments: AttachmentStorageInternal get() = this@AbstractNode.attachments override val networkService: MessagingService get() = network override val clock: Clock get() = platformClock override val configuration: NodeConfiguration get() = this@AbstractNode.configuration 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 fcc6c7eafe..f584a72818 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -18,26 +18,12 @@ import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.RPC_UPLOADER import net.corda.core.internal.STRUCTURAL_STEP_PREFIX import net.corda.core.internal.sign -import net.corda.core.messaging.CordaRPCOps -import net.corda.core.messaging.DataFeed -import net.corda.core.messaging.FlowHandle -import net.corda.core.messaging.FlowHandleImpl -import net.corda.core.messaging.FlowProgressHandle -import net.corda.core.messaging.FlowProgressHandleImpl -import net.corda.core.messaging.ParametersUpdateInfo -import net.corda.core.messaging.RPCReturnsObservables -import net.corda.core.messaging.StateMachineInfo -import net.corda.core.messaging.StateMachineTransactionMapping -import net.corda.core.messaging.StateMachineUpdate +import net.corda.core.messaging.* import net.corda.core.node.NodeInfo import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.Vault -import net.corda.core.node.services.vault.AttachmentQueryCriteria -import net.corda.core.node.services.vault.AttachmentSort -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.node.services.vault.* import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.getOrThrow @@ -74,11 +60,10 @@ internal class CordaRPCOpsImpl( } override fun acceptNewNetworkParameters(parametersHash: SecureHash) { - services.networkMapUpdater.acceptNewNetworkParameters( - parametersHash, - // TODO When multiple identities design will be better specified this should be signature from node operator. - { hash -> hash.serialize().sign { services.keyManagementService.sign(it.bytes, services.myInfo.legalIdentities[0].owningKey) } } - ) + // TODO When multiple identities design will be better specified this should be signature from node operator. + services.networkMapUpdater.acceptNewNetworkParameters(parametersHash) { hash -> + hash.serialize().sign { services.keyManagementService.sign(it.bytes, services.myInfo.legalIdentities[0].owningKey) } + } } override fun networkMapFeed(): DataFeed, NetworkMapCache.MapChange> { @@ -191,7 +176,7 @@ internal class CordaRPCOpsImpl( } override fun uploadAttachment(jar: InputStream): SecureHash { - return services.attachments.importAttachment(jar, RPC_UPLOADER, null) + return services.attachments.privilegedImportAttachment(jar, RPC_UPLOADER, null) } override fun uploadAttachmentWithMetadata(jar: InputStream, uploader: String, filename: String): SecureHash { 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 454b1f8101..4241b6aea2 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 @@ -14,6 +14,7 @@ import net.corda.core.node.services.AttachmentStorage import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.contextLogger import net.corda.node.cordapp.CordappLoader +import net.corda.node.services.persistence.AttachmentStorageInternal import java.net.URL import java.util.concurrent.ConcurrentHashMap @@ -89,7 +90,13 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader, cordapps.filter { !it.contractClassNames.isEmpty() }.map { it.jarPath.openStream().use { stream -> try { - attachmentStorage.importAttachment(stream, DEPLOYED_CORDAPP_UPLOADER, null) + // We can't make attachmentStorage a AttachmentStorageInternal as that ends up requiring + // MockAttachmentStorage to implement it. + if (attachmentStorage is AttachmentStorageInternal) { + attachmentStorage.privilegedImportAttachment(stream, DEPLOYED_CORDAPP_UPLOADER, null) + } else { + attachmentStorage.importAttachment(stream, DEPLOYED_CORDAPP_UPLOADER, null) + } } catch (faee: java.nio.file.FileAlreadyExistsException) { AttachmentId.parse(faee.message!!) } 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 3fbb296485..bd85b33b2c 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.node.internal.cordapp.CordappProviderInternal import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService import net.corda.node.services.network.NetworkMapUpdater +import net.corda.node.services.persistence.AttachmentStorageInternal import net.corda.node.services.statemachine.ExternalEvent import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -105,6 +106,7 @@ interface ServiceHubInternal : ServiceHub { } } + override val attachments: AttachmentStorageInternal override val vaultService: VaultServiceInternal /** * A map of hash->tx where tx has been signature/contract validated and the states are known to be correct. diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/AttachmentStorageInternal.kt b/node/src/main/kotlin/net/corda/node/services/persistence/AttachmentStorageInternal.kt new file mode 100644 index 0000000000..0ca5cd2ffb --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/persistence/AttachmentStorageInternal.kt @@ -0,0 +1,13 @@ +package net.corda.node.services.persistence + +import net.corda.core.node.services.AttachmentId +import net.corda.core.node.services.AttachmentStorage +import java.io.InputStream + +interface AttachmentStorageInternal : AttachmentStorage { + /** + * This is the same as [importAttachment] expect there are no checks done on the uploader field. This API is internal + * and is only for the node. + */ + fun privilegedImportAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId +} diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index da0d32a6cc..93a8753616 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 @@ -12,12 +12,8 @@ import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.ContractClassName import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 -import net.corda.core.internal.AbstractAttachment -import net.corda.core.internal.UNKNOWN_UPLOADER -import net.corda.core.internal.VisibleForTesting -import net.corda.core.internal.readFully +import net.corda.core.internal.* import net.corda.core.node.services.AttachmentId -import net.corda.core.node.services.AttachmentStorage import net.corda.core.node.services.vault.AttachmentQueryCriteria import net.corda.core.node.services.vault.AttachmentSort import net.corda.core.serialization.* @@ -50,10 +46,12 @@ class NodeAttachmentService( private val database: CordaPersistence, attachmentContentCacheSize: Long = NodeConfiguration.defaultAttachmentContentCacheSize, attachmentCacheBound: Long = NodeConfiguration.defaultAttachmentCacheBound -) : AttachmentStorage, SingletonSerializeAsToken() { +) : AttachmentStorageInternal, SingletonSerializeAsToken() { companion object { private val log = contextLogger() + private val PRIVILEGED_UPLOADERS = listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER, P2P_UPLOADER, UNKNOWN_UPLOADER) + // Just iterate over the entries with verification enabled: should be good enough to catch mistakes. // Note that JarInputStream won't throw any kind of error at all if the file stream is in fact not // a ZIP! It'll just pretend it's an empty archive, which is kind of stupid but that's how it works. @@ -228,10 +226,9 @@ class NodeAttachmentService( } } - private val attachmentCache = NonInvalidatingCache>( - attachmentCacheBound, - { key -> Optional.ofNullable(createAttachment(key)) } - ) + private val attachmentCache = NonInvalidatingCache>(attachmentCacheBound) { key -> + Optional.ofNullable(createAttachment(key)) + } private fun createAttachment(key: SecureHash): Attachment? { val content = attachmentContentCache.get(key)!! @@ -258,6 +255,18 @@ class NodeAttachmentService( } override fun importAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId { + require(uploader !in PRIVILEGED_UPLOADERS) { "$uploader is a reserved uploader token" } + if (uploader.startsWith("$P2P_UPLOADER:")) { + // FetchAttachmentsFlow is in core and thus doesn't have access to AttachmentStorageInternal to call + // privilegedImportAttachment + require(Thread.currentThread().stackTrace.any { it.className == FetchAttachmentsFlow::class.java.name }) { + "$P2P_UPLOADER is a reserved uploader token prefix" + } + } + return import(jar, uploader, filename) + } + + override fun privilegedImportAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId { return import(jar, uploader, filename) } @@ -282,7 +291,13 @@ class NodeAttachmentService( if (!hasAttachment(id)) { checkIsAValidJAR(bytes.inputStream()) val session = currentDBSession() - val attachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = bytes, uploader = uploader, filename = filename, contractClassNames = contractClassNames) + val attachment = NodeAttachmentService.DBAttachment( + attId = id.toString(), + content = bytes, + uploader = uploader, + filename = filename, + contractClassNames = contractClassNames + ) session.save(attachment) attachmentCount.inc() log.info("Stored new attachment $id") @@ -295,10 +310,12 @@ class NodeAttachmentService( } @Suppress("OverridingDeprecatedMember") - override fun importOrGetAttachment(jar: InputStream): AttachmentId = try { - import(jar, UNKNOWN_UPLOADER, null) - } catch (faee: java.nio.file.FileAlreadyExistsException) { - AttachmentId.parse(faee.message!!) + override fun importOrGetAttachment(jar: InputStream): AttachmentId { + return try { + import(jar, UNKNOWN_UPLOADER, null) + } catch (faee: java.nio.file.FileAlreadyExistsException) { + AttachmentId.parse(faee.message!!) + } } override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List { diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt similarity index 77% rename from node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt rename to node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt index 9bf4cb828d..baee00a8f3 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt @@ -1,25 +1,33 @@ package net.corda.node.services.persistence +import co.paralleluniverse.fibers.Suspendable import com.codahale.metrics.MetricRegistry import com.google.common.jimfs.Configuration import com.google.common.jimfs.Jimfs import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 +import net.corda.core.flows.FlowLogic import net.corda.core.internal.* import net.corda.core.node.services.vault.AttachmentQueryCriteria import net.corda.core.node.services.vault.AttachmentSort import net.corda.core.node.services.vault.Builder import net.corda.core.node.services.vault.Sort +import net.corda.core.utilities.getOrThrow import net.corda.node.internal.configureDatabase import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.internal.LogHelper import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.node.internal.InternalMockNetwork +import net.corda.testing.node.internal.startFlow +import org.assertj.core.api.Assertions.assertThatIllegalArgumentException import org.junit.After import org.junit.Before import org.junit.Ignore import org.junit.Test +import java.io.ByteArrayOutputStream +import java.io.OutputStream import java.nio.charset.StandardCharsets import java.nio.file.FileAlreadyExistsException import java.nio.file.FileSystem @@ -30,7 +38,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNull -class NodeAttachmentStorageTest { +class NodeAttachmentServiceTest { // Use an in memory file system for testing attachment storage. private lateinit var fs: FileSystem private lateinit var database: CordaPersistence @@ -185,7 +193,7 @@ class NodeAttachmentStorageTest { @Ignore("We need to be able to restart nodes - make importing attachments idempotent?") @Test fun `duplicates not allowed`() { - val (testJar, _) = makeTestJar() + val (testJar) = makeTestJar() testJar.read { storage.importAttachment(it, "test", null) } @@ -198,7 +206,7 @@ class NodeAttachmentStorageTest { @Test fun `corrupt entry throws exception`() { - val (testJar, _) = makeTestJar() + val (testJar) = makeTestJar() val id = database.transaction { val id = testJar.read { storage.importAttachment(it, "test", null) } @@ -233,23 +241,68 @@ class NodeAttachmentStorageTest { } } + @Test + fun `using reserved uploader tokens`() { + val (testJar) = makeTestJar() + + fun assertImportFails(uploader: String) { + testJar.read { + assertThatIllegalArgumentException().isThrownBy { + storage.importAttachment(it, uploader, null) + }.withMessageContaining(uploader) + } + } + + database.transaction { + assertImportFails(DEPLOYED_CORDAPP_UPLOADER) + assertImportFails(P2P_UPLOADER) + assertImportFails(RPC_UPLOADER) + assertImportFails(UNKNOWN_UPLOADER) + } + + // Import an attachment similar to how net.corda.core.internal.FetchAttachmentsFlow does it. + InternalMockNetwork(threadPerNode = true).use { mockNet -> + val node = mockNet.createNode() + val result = node.services.startFlow(FetchAttachmentsFlow()).resultFuture + assertThatIllegalArgumentException().isThrownBy { + result.getOrThrow() + }.withMessageContaining(P2P_UPLOADER) + } + } + + // Not the real FetchAttachmentsFlow! + private class FetchAttachmentsFlow : FlowLogic() { + @Suspendable + override fun call() { + val baos = ByteArrayOutputStream() + makeTestJar(baos) + serviceHub.attachments.importAttachment(baos.toByteArray().inputStream(), "$P2P_UPLOADER:${ourIdentity.name}", null) + } + } + private var counter = 0 private fun makeTestJar(extraEntries: List> = emptyList()): Pair { counter++ val file = fs.getPath("$counter.jar") - file.write { - val jar = JarOutputStream(it) - jar.putNextEntry(JarEntry("test1.txt")) - jar.write("This is some useful content".toByteArray()) - jar.closeEntry() - jar.putNextEntry(JarEntry("test2.txt")) - jar.write("Some more useful content".toByteArray()) - extraEntries.forEach { - jar.putNextEntry(JarEntry(it.first)) - jar.write(it.second.toByteArray()) - } - jar.closeEntry() - } + makeTestJar(file.outputStream(), extraEntries) return Pair(file, file.readAll().sha256()) } + + private companion object { + private fun makeTestJar(output: OutputStream, extraEntries: List> = emptyList()) { + output.use { + val jar = JarOutputStream(it) + jar.putNextEntry(JarEntry("test1.txt")) + jar.write("This is some useful content".toByteArray()) + jar.closeEntry() + jar.putNextEntry(JarEntry("test2.txt")) + jar.write("Some more useful content".toByteArray()) + extraEntries.forEach { + jar.putNextEntry(JarEntry(it.first)) + jar.write(it.second.toByteArray()) + } + jar.closeEntry() + } + } + } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index 50b951ef60..739dccf3df 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -147,7 +147,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe val testDirectory: Path = Paths.get("build", getTimestampAsDirectoryName()), val networkParameters: NetworkParameters = testNetworkParameters(), val defaultFactory: (MockNodeArgs, CordappLoader?) -> MockNode = { args, cordappLoader -> cordappLoader?.let { MockNode(args, it) } ?: MockNode(args) }, - val cordappsForAllNodes: Set = emptySet()) { + val cordappsForAllNodes: Set = emptySet()) : AutoCloseable { init { // Apache SSHD for whatever reason registers a SFTP FileSystemProvider - which gets loaded by JimFS. // This SFTP support loads BouncyCastle, which we want to avoid. @@ -551,6 +551,8 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe fun waitQuiescent() { busyLatch.await() } + + override fun close() = stopNodes() } abstract class MessagingServiceSpy { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index 1e820f9de0..19f282dac1 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -288,7 +288,7 @@ data class TestLedgerDSLInterpreter private constructor( copy().dsl() override fun attachment(attachment: InputStream): SecureHash { - return services.attachments.importAttachment(attachment, UNKNOWN_UPLOADER, null) + return services.attachments.importAttachment(attachment, "TestDSL", null) } override fun verifies(): EnforceVerifyOrFail { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt index 07a8c4c61b..508ace7fb4 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt @@ -3,12 +3,11 @@ package net.corda.testing.internal import net.corda.core.contracts.ContractClassName import net.corda.core.cordapp.Cordapp import net.corda.core.crypto.SecureHash -import net.corda.core.internal.TEST_UPLOADER +import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER 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.cordapp.CordappLoader -import net.corda.node.internal.cordapp.JarScanningCordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.testing.services.MockAttachmentStorage import java.nio.file.Paths @@ -50,7 +49,7 @@ class MockCordappProvider( return if (!existingAttachment.isEmpty()) { existingAttachment.keys.first() } else { - attachments.importContractAttachment(contractClassNames, TEST_UPLOADER, data.inputStream()) + attachments.importContractAttachment(contractClassNames, DEPLOYED_CORDAPP_UPLOADER, data.inputStream()) } } } From b7f7dcc510fa1641f4771276dac6d513f4012c10 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Tue, 31 Jul 2018 17:10:07 +0100 Subject: [PATCH 4/9] CORDA-1861: Remove default value to make createUnstartedNode() unambiguous. (#3729) --- .../src/main/kotlin/net/corda/testing/node/MockNetwork.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt index 82730cefc1..e5ee8728c8 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt @@ -411,7 +411,7 @@ open class MockNetwork( forcedID: Int? = null, entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), configOverrides: (NodeConfiguration) -> Any? = {}, - additionalCordapps: Set = emptySet()): UnstartedMockNode { + additionalCordapps: Set): UnstartedMockNode { val parameters = MockNodeParameters(forcedID, legalName, entropyRoot, configOverrides, additionalCordapps) return UnstartedMockNode.create(internalMockNetwork.createUnstartedNode(InternalMockNodeParameters(parameters))) } From 8b501b1b8031ff76646d1da2820ec2058ceb13b0 Mon Sep 17 00:00:00 2001 From: josecoll Date: Tue, 31 Jul 2018 17:16:27 +0100 Subject: [PATCH 5/9] CORDA-1858 - Vault query fails to find a state if it extends from class (#3722) * Included Contract State parent classes in list of queryable types. * Added changelog entry. --- docs/source/changelog.rst | 2 + .../node/services/vault/NodeVaultService.kt | 36 +++++++++------- .../node/services/vault/VaultQueryTests.kt | 43 ++++++++++++++++++- 3 files changed, 65 insertions(+), 16 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 426b192264..e852b26f77 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,6 +6,8 @@ release, see :doc:`upgrade-notes`. Unreleased ---------- +* Vault query fix: support query by parent classes of Contract State classes (see https://github.com/corda/corda/issues/3714) + * Added ``registerResponderFlow`` method to ``StartedMockNode``, to support isolated testing of responder flow behaviour. * "app", "rpc", "p2p" and "unknown" are no longer allowed as uploader values when importing attachments. These are used 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 2062762711..17f905313d 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 @@ -85,10 +85,10 @@ class NodeVaultService( 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) + val contractTypes = deriveContractTypes(concreteType) + contractTypes.map { + val contractStateType = contractStateTypeMappings.getOrPut(it.name) { mutableSetOf() } + contractStateType.add(concreteType.name) } } } @@ -532,10 +532,10 @@ class NodeVaultService( null } concreteType?.let { - val contractInterfaces = deriveContractInterfaces(it) - contractInterfaces.map { - val contractInterface = contractStateTypeMappings.getOrPut(it.name) { mutableSetOf() } - contractInterface.add(it.name) + val contractTypes = deriveContractTypes(it) + contractTypes.map { + val contractStateType = contractStateTypeMappings.getOrPut(it.name) { mutableSetOf() } + contractStateType.add(it.name) } } } @@ -544,14 +544,20 @@ class NodeVaultService( } } - private fun deriveContractInterfaces(clazz: Class): Set> { - val myInterfaces: MutableSet> = mutableSetOf() - clazz.interfaces.forEach { - if (it != ContractState::class.java) { - myInterfaces.add(uncheckedCast(it)) - myInterfaces.addAll(deriveContractInterfaces(uncheckedCast(it))) + private fun deriveContractTypes(clazz: Class): Set> { + val myTypes : MutableSet> = mutableSetOf() + clazz.superclass?.let { + if (!it.isInstance(Any::class)) { + myTypes.add(uncheckedCast(it)) + myTypes.addAll(deriveContractTypes(uncheckedCast(it))) } } - return myInterfaces + clazz.interfaces.forEach { + if (it != ContractState::class.java) { + myTypes.add(uncheckedCast(it)) + myTypes.addAll(deriveContractTypes(uncheckedCast(it))) + } + } + return myTypes } } \ No newline at end of file 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 54e47d3f08..3f8f4ef7d2 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 @@ -2,12 +2,14 @@ package net.corda.node.services.vault import net.corda.core.contracts.* import net.corda.core.crypto.* +import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name 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.* import net.corda.core.node.services.vault.QueryCriteria.* +import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.* import net.corda.finance.* @@ -27,6 +29,7 @@ import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseTransaction import net.corda.testing.core.* import net.corda.testing.internal.TEST_TX_TIME +import net.corda.testing.internal.chooseIdentity import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.vault.* import net.corda.testing.node.MockServices @@ -115,7 +118,8 @@ open class VaultQueryTestRule : ExternalResource(), VaultQueryParties { "net.corda.finance.contracts", CashSchemaV1::class.packageName, DummyLinearStateSchemaV1::class.packageName, - SampleCashSchemaV3::class.packageName) + SampleCashSchemaV3::class.packageName, + VaultQueryTestsBase.MyContractClass::class.packageName) override lateinit var services: MockServices override lateinit var vaultFiller: VaultFiller @@ -253,6 +257,43 @@ abstract class VaultQueryTestsBase : VaultQueryParties { } } + @Test + fun `query by interface for a contract class extending a parent contract class`() { + database.transaction { + + // build custom contract and store in vault + val me = services.myInfo.chooseIdentity() + val state = MyState("myState", listOf(me)) + val stateAndContract = StateAndContract(state, MYCONTRACT_ID) + val utx = TransactionBuilder(notary = notaryServices.myInfo.singleIdentity()).withItems(stateAndContract).withItems(dummyCommand()) + services.recordTransactions(services.signInitialTransaction(utx)) + + // query vault by Child class + val criteria = VaultQueryCriteria() // default is UNCONSUMED + val queryByMyState = vaultService.queryBy(criteria) + assertThat(queryByMyState.states).hasSize(1) + + // query vault by Parent class + val queryByBaseState = vaultService.queryBy(criteria) + assertThat(queryByBaseState.states).hasSize(1) + + // query vault by extended Contract Interface + val queryByContract = vaultService.queryBy(criteria) + assertThat(queryByContract.states).hasSize(1) + } + } + + // Beware: do not use `MyContractClass::class.qualifiedName` as this returns a fully qualified name using "dot" notation for enclosed class + val MYCONTRACT_ID = "net.corda.node.services.vault.VaultQueryTestsBase\$MyContractClass" + + open class MyContractClass : Contract { + override fun verify(tx: LedgerTransaction) {} + } + + interface MyContractInterface : ContractState + open class BaseState(override val participants: List = emptyList()) : MyContractInterface + data class MyState(val name: String, override val participants: List = emptyList()) : BaseState(participants) + @Test fun `unconsumed states simple`() { database.transaction { From 0762a61aca127e93d7ce44afff36167b00a8eb36 Mon Sep 17 00:00:00 2001 From: Dan Newton Date: Wed, 1 Aug 2018 07:58:00 +0100 Subject: [PATCH 6/9] In AbstractNode check if an initiating flow already has a flow registered to it and throw exception if one has been (#3713) * In AbstractNode check if an initiating flow already has a flow registered to it and throw exception if one has been In AbstractNode.internalRegisterFlowFactory check if a flow factory already exists for a initiating flow If a factory does exist, throw an exception A few tests needed to be fixed due to this change * Reorder test setup to prevent flows from being registered multiple times * Use check instead of if and throws statement in AbstractNode when checking for initiating flow having multiple flows mapped to it Use check instead of if throws statement Change names of methods in tests Improve FlowRegistrationTest to better check if flow has been registered properly * tidy up FlowFrameworkTests and FlowRegistrationTest --- .../net/corda/docs/CustomVaultQueryTest.kt | 1 - .../WorkflowTransactionBuildTutorialTest.kt | 1 - .../services/messaging/MQSecurityTest.kt | 1 - .../net/corda/node/internal/AbstractNode.kt | 3 + .../node/internal/FlowRegistrationTest.kt | 72 ++++++++++++ .../statemachine/FlowFrameworkTests.kt | 103 ++++++++---------- 6 files changed, 120 insertions(+), 61 deletions(-) create mode 100644 node/src/test/kotlin/net/corda/node/internal/FlowRegistrationTest.kt 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 1ac8e1e909..53e92a35c5 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 @@ -42,7 +42,6 @@ class CustomVaultQueryTest { mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance", "net.corda.docs", "com.template")) nodeA = mockNet.createPartyNode() nodeB = mockNet.createPartyNode() - nodeA.registerInitiatedFlow(TopupIssuerFlow.TopupIssuer::class.java) notary = mockNet.defaultNotaryIdentity } 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 e7d28fb72e..3304f4707a 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 @@ -36,7 +36,6 @@ class WorkflowTransactionBuildTutorialTest { mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.docs")) aliceNode = mockNet.createPartyNode(ALICE_NAME) bobNode = mockNet.createPartyNode(BOB_NAME) - aliceNode.registerInitiatedFlow(RecordCompletionFlow::class.java) alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME) bob = bobNode.services.myInfo.identityFromX500Name(BOB_NAME) } 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 d42949d020..0214cd0f48 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 @@ -188,7 +188,6 @@ abstract class MQSecurityTest : NodeBasedTest() { protected fun startBobAndCommunicateWithAlice(): Party { val bob = startNode(BOB_NAME) - bob.registerInitiatedFlow(ReceiveFlow::class.java) val bobParty = bob.info.singleIdentity() // Perform a protocol exchange to force the peer queue to be created alice.services.startFlow(SendFlow(bobParty, 0)).resultFuture.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 45f9385275..343f25c772 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -659,6 +659,9 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } else { Observable.empty() } + check(initiatingFlowClass !in flowFactories.keys) { + "$initiatingFlowClass is attempting to register multiple initiated flows" + } flowFactories[initiatingFlowClass] = flowFactory return observable } diff --git a/node/src/test/kotlin/net/corda/node/internal/FlowRegistrationTest.kt b/node/src/test/kotlin/net/corda/node/internal/FlowRegistrationTest.kt new file mode 100644 index 0000000000..e6a9b5fd3e --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/internal/FlowRegistrationTest.kt @@ -0,0 +1,72 @@ +package net.corda.node.internal + +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.CordaX500Name +import net.corda.core.identity.Party +import net.corda.core.utilities.unwrap +import net.corda.testing.core.singleIdentity +import net.corda.testing.node.MockNetwork +import net.corda.testing.node.MockNodeParameters +import net.corda.testing.node.StartedMockNode +import org.assertj.core.api.Assertions.assertThatIllegalStateException +import org.junit.After +import org.junit.Before +import org.junit.Test +import kotlin.test.assertNotNull + +class FlowRegistrationTest { + + lateinit var mockNetwork: MockNetwork + lateinit var initiator: StartedMockNode + lateinit var responder: StartedMockNode + + @Before + fun setup() { + // no cordapps scanned so it can be tested in isolation + mockNetwork = MockNetwork(emptyList()) + initiator = mockNetwork.createNode(MockNodeParameters(legalName = CordaX500Name("initiator", "Reading", "GB"))) + responder = mockNetwork.createNode(MockNodeParameters(legalName = CordaX500Name("responder", "Reading", "GB"))) + mockNetwork.runNetwork() + } + + @After + fun tearDown() { + mockNetwork.stopNodes() + } + + @Test + fun `startup fails when two flows initiated by the same flow are registered`() { + // register the same flow twice to invoke the error without causing errors in other tests + responder.registerInitiatedFlow(Responder::class.java) + assertThatIllegalStateException().isThrownBy { responder.registerInitiatedFlow(Responder::class.java) } + } + + @Test + fun `a single initiated flow can be registered without error`() { + responder.registerInitiatedFlow(Responder::class.java) + val result = initiator.startFlow(Initiator(responder.info.singleIdentity())) + mockNetwork.runNetwork() + assertNotNull(result.get()) + } +} + +@InitiatingFlow +class Initiator(val party: Party) : FlowLogic() { + @Suspendable + override fun call(): String { + return initiateFlow(party).sendAndReceive("Hello there").unwrap { it } + } +} + +@InitiatedBy(Initiator::class) +private class Responder(val session: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + session.receive().unwrap { it } + session.send("What's up") + } +} \ No newline at end of file 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 0d6dc218d8..948aa59f5d 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 @@ -53,47 +53,41 @@ class FlowFrameworkTests { init { LogHelper.setLevel("+net.corda.flow") } + } - private lateinit var mockNet: InternalMockNetwork - private lateinit var aliceNode: TestStartedNode - private lateinit var bobNode: TestStartedNode - private lateinit var alice: Party - private lateinit var bob: Party - private lateinit var notaryIdentity: Party - private val receivedSessionMessages = ArrayList() + private lateinit var mockNet: InternalMockNetwork + private lateinit var aliceNode: TestStartedNode + private lateinit var bobNode: TestStartedNode + private lateinit var alice: Party + private lateinit var bob: Party + private lateinit var notaryIdentity: Party + private val receivedSessionMessages = ArrayList() - @BeforeClass - @JvmStatic - fun beforeClass() { - mockNet = InternalMockNetwork( - cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"), - servicePeerAllocationStrategy = RoundRobin() - ) + @Before + fun setUpMockNet() { + mockNet = InternalMockNetwork( + cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"), + servicePeerAllocationStrategy = RoundRobin() + ) - aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME)) - bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME)) + aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME)) + bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME)) - // Extract identities - alice = aliceNode.info.singleIdentity() - bob = bobNode.info.singleIdentity() - notaryIdentity = mockNet.defaultNotaryIdentity + // Extract identities + alice = aliceNode.info.singleIdentity() + bob = bobNode.info.singleIdentity() + notaryIdentity = mockNet.defaultNotaryIdentity - receivedSessionMessagesObservable().forEach { receivedSessionMessages += it } - } - - private fun receivedSessionMessagesObservable(): Observable { - return mockNet.messagingNetwork.receivedMessages.toSessionTransfers() - } - - @AfterClass @JvmStatic - fun afterClass() { - mockNet.stopNodes() - } + receivedSessionMessagesObservable().forEach { receivedSessionMessages += it } + } + private fun receivedSessionMessagesObservable(): Observable { + return mockNet.messagingNetwork.receivedMessages.toSessionTransfers() } @After fun cleanUp() { + mockNet.stopNodes() receivedSessionMessages.clear() } @@ -474,45 +468,38 @@ class FlowFrameworkTripartyTests { private lateinit var charlie: Party private lateinit var notaryIdentity: Party private val receivedSessionMessages = ArrayList() + } - @BeforeClass - @JvmStatic - fun beforeClass() { - mockNet = InternalMockNetwork( - cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"), - servicePeerAllocationStrategy = RoundRobin() - ) + @Before + fun setUpGlobalMockNet() { + mockNet = InternalMockNetwork( + cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"), + servicePeerAllocationStrategy = RoundRobin() + ) - aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME)) - bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME)) - charlieNode = mockNet.createNode(InternalMockNodeParameters(legalName = CHARLIE_NAME)) + aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME)) + bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME)) + charlieNode = mockNet.createNode(InternalMockNodeParameters(legalName = CHARLIE_NAME)) - // Extract identities - alice = aliceNode.info.singleIdentity() - bob = bobNode.info.singleIdentity() - charlie = charlieNode.info.singleIdentity() - notaryIdentity = mockNet.defaultNotaryIdentity - - receivedSessionMessagesObservable().forEach { receivedSessionMessages += it } - } - - @AfterClass @JvmStatic - fun afterClass() { - mockNet.stopNodes() - } - - private fun receivedSessionMessagesObservable(): Observable { - return mockNet.messagingNetwork.receivedMessages.toSessionTransfers() - } + // Extract identities + alice = aliceNode.info.singleIdentity() + bob = bobNode.info.singleIdentity() + charlie = charlieNode.info.singleIdentity() + notaryIdentity = mockNet.defaultNotaryIdentity + receivedSessionMessagesObservable().forEach { receivedSessionMessages += it } } @After fun cleanUp() { + mockNet.stopNodes() receivedSessionMessages.clear() } + private fun receivedSessionMessagesObservable(): Observable { + return mockNet.messagingNetwork.receivedMessages.toSessionTransfers() + } @Test fun `sending to multiple parties`() { From 01d896394ad2600d9e05abc690b804cb85c84396 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Wed, 1 Aug 2018 08:35:39 +0100 Subject: [PATCH 7/9] CORDA-1862: Allow MockNetwork to create StartedMockNode from UnstartedMockNode. (#3731) * Allow MockNetwork to create StartedMockNode from UnstartedMockNode. * Reimplement by adding a `started` property to UnstartedMockNode. * Throw IllegalStateException instead of NoSuchElementException. * Add an isStarted property to UnstartedMockNode. --- .../net/corda/testing/node/MockNetwork.kt | 12 +++++ .../net/corda/testing/node/MockNetworkTest.kt | 47 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 testing/node-driver/src/test/kotlin/net/corda/testing/node/MockNetworkTest.kt diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt index e5ee8728c8..d81fc4c529 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt @@ -128,6 +128,18 @@ class UnstartedMockNode private constructor(private val node: InternalMockNetwor * @return A [StartedMockNode] object. */ fun start(): StartedMockNode = StartedMockNode.create(node.start()) + + /** + * A [StartedMockNode] object for this running node. + * @throws [IllegalStateException] if the node is not running yet. + */ + val started: StartedMockNode + get() = StartedMockNode.create(node.started ?: throw IllegalStateException("Node ID=$id is not running")) + + /** + * Whether this node has been started yet. + */ + val isStarted: Boolean get() = node.started != null } /** A class that represents a started mock node for testing. */ diff --git a/testing/node-driver/src/test/kotlin/net/corda/testing/node/MockNetworkTest.kt b/testing/node-driver/src/test/kotlin/net/corda/testing/node/MockNetworkTest.kt new file mode 100644 index 0000000000..e9aa07276d --- /dev/null +++ b/testing/node-driver/src/test/kotlin/net/corda/testing/node/MockNetworkTest.kt @@ -0,0 +1,47 @@ +package net.corda.testing.node + +import net.corda.testing.core.* +import org.assertj.core.api.Assertions.* +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import kotlin.test.assertFailsWith + +class MockNetworkTest { + private companion object { + private const val NODE_ID = 101 + } + private lateinit var mockNetwork: MockNetwork + + @Before + fun setup() { + mockNetwork = MockNetwork(cordappPackages = emptyList()) + } + + @After + fun done() { + mockNetwork.stopNodes() + } + + @Test + fun `with a started node`() { + val unstarted = mockNetwork.createUnstartedNode(DUMMY_BANK_A_NAME, forcedID = NODE_ID) + assertFalse(unstarted.isStarted) + + mockNetwork.startNodes() + assertTrue(unstarted.isStarted) + + val started = unstarted.started + assertEquals(NODE_ID, started.id) + assertEquals(DUMMY_BANK_A_NAME, started.info.identityFromX500Name(DUMMY_BANK_A_NAME).name) + assertFailsWith { started.info.identityFromX500Name(DUMMY_BANK_B_NAME) } + } + + @Test + fun `with an unstarted node`() { + val unstarted = mockNetwork.createUnstartedNode(DUMMY_BANK_A_NAME, forcedID = NODE_ID) + val ex = assertFailsWith { unstarted.started } + assertThat(ex).hasMessage("Node ID=$NODE_ID is not running") + } +} \ No newline at end of file From 7182542724048ccb660a2b70bc2acd75b4019bef Mon Sep 17 00:00:00 2001 From: Dominic Fox <40790090+distributedleetravis@users.noreply.github.com> Date: Wed, 1 Aug 2018 10:18:54 +0100 Subject: [PATCH 8/9] Matchers (#3716) * Move common matches to testing, add some missing ones * Clarify test logic * Move common matches to testing, add some missing ones * Clarify test logic * Rename 'randomise' * endregion * Fix broken unit test --- .../confidential/SwapIdentitiesFlowTests.kt | 208 +++++++++++------- .../net/corda/core/flows/AttachmentTests.kt | 9 +- .../core/flows/CollectSignaturesFlowTests.kt | 4 +- .../core/flows/ContractUpgradeFlowRPCTest.kt | 4 +- .../core/flows/ContractUpgradeFlowTest.kt | 10 +- .../net/corda/core/flows/FinalityFlowTests.kt | 4 +- .../corda/core/flows/ReceiveAllFlowTests.kt | 2 +- .../core/flows/matchers/flow/FlowMatchers.kt | 36 --- .../core/flows/matchers/rpc/RpcMatchers.kt | 31 --- .../corda/core/flows/mixins/WithMockNet.kt | 9 +- .../node/internal/InternalMockNetwork.kt | 2 +- testing/test-utils/build.gradle | 1 + .../net/corda/testing/core/TestUtils.kt | 13 ++ .../testing/internal/matchers/Matchers.kt | 99 +++++++++ .../internal/matchers/flow/FlowMatchers.kt | 38 ++++ .../matchers/future}/FutureMatchers.kt | 19 +- .../internal/matchers/rpc/RpcMatchers.kt | 36 +++ .../corda/testing/internal/MatcherTests.kt | 58 +++++ 18 files changed, 403 insertions(+), 180 deletions(-) delete mode 100644 core/src/test/kotlin/net/corda/core/flows/matchers/flow/FlowMatchers.kt delete mode 100644 core/src/test/kotlin/net/corda/core/flows/matchers/rpc/RpcMatchers.kt create mode 100644 testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/Matchers.kt create mode 100644 testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/flow/FlowMatchers.kt rename {core/src/test/kotlin/net/corda/core/flows/matchers => testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/future}/FutureMatchers.kt (75%) create mode 100644 testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/rpc/RpcMatchers.kt create mode 100644 testing/test-utils/src/test/kotlin/net/corda/testing/internal/MatcherTests.kt 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 5b1541b597..7023242a07 100644 --- a/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt +++ b/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt @@ -1,54 +1,59 @@ package net.corda.confidential +import com.natpryce.hamkrest.MatchResult +import com.natpryce.hamkrest.Matcher +import com.natpryce.hamkrest.equalTo import net.corda.core.identity.* -import net.corda.core.utilities.getOrThrow import net.corda.testing.core.* +import net.corda.testing.internal.matchers.allOf +import net.corda.testing.internal.matchers.flow.willReturn import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.startFlow -import org.junit.Before import org.junit.Test import kotlin.test.* +import com.natpryce.hamkrest.assertion.assert +import net.corda.core.crypto.DigitalSignature +import net.corda.testing.internal.matchers.hasOnlyEntries +import net.corda.testing.node.internal.TestStartedNode +import org.junit.AfterClass +import java.security.PublicKey class SwapIdentitiesFlowTests { - private lateinit var mockNet: InternalMockNetwork + companion object { + private val mockNet = InternalMockNetwork(networkSendManuallyPumped = false, threadPerNode = true) - @Before - fun setup() { - // We run this in parallel threads to help catch any race conditions that may exist. - mockNet = InternalMockNetwork(networkSendManuallyPumped = false, threadPerNode = true) + @AfterClass + @JvmStatic + fun tearDown() = mockNet.stopNodes() } + private val aliceNode = mockNet.createPartyNode(makeUnique(ALICE_NAME)) + private val bobNode = mockNet.createPartyNode(makeUnique(BOB_NAME)) + private val charlieNode = mockNet.createPartyNode(makeUnique(CHARLIE_NAME)) + private val alice = aliceNode.info.singleIdentity() + private val bob = bobNode.info.singleIdentity() + @Test fun `issue key`() { - // Set up values we'll need - val aliceNode = mockNet.createPartyNode(ALICE_NAME) - val bobNode = mockNet.createPartyNode(BOB_NAME) - val alice = aliceNode.info.singleIdentity() - val bob = bobNode.services.myInfo.singleIdentity() - - // Run the flows - val requesterFlow = aliceNode.services.startFlow(SwapIdentitiesFlow(bob)).resultFuture - - // Get the results - val actual: Map = requesterFlow.getOrThrow().toMap() - assertEquals(2, actual.size) - // Verify that the generated anonymous identities do not match the well known identities - val aliceAnonymousIdentity = actual[alice] ?: throw IllegalStateException() - val bobAnonymousIdentity = actual[bob] ?: throw IllegalStateException() - assertNotEquals(alice, aliceAnonymousIdentity) - assertNotEquals(bob, bobAnonymousIdentity) - - // Verify that the anonymous identities look sane - assertEquals(alice.name, aliceNode.database.transaction { aliceNode.services.identityService.wellKnownPartyFromAnonymous(aliceAnonymousIdentity)!!.name }) - assertEquals(bob.name, bobNode.database.transaction { bobNode.services.identityService.wellKnownPartyFromAnonymous(bobAnonymousIdentity)!!.name }) - - // Verify that the nodes have the right anonymous identities - assertTrue { aliceAnonymousIdentity.owningKey in aliceNode.services.keyManagementService.keys } - assertTrue { bobAnonymousIdentity.owningKey in bobNode.services.keyManagementService.keys } - assertFalse { aliceAnonymousIdentity.owningKey in bobNode.services.keyManagementService.keys } - assertFalse { bobAnonymousIdentity.owningKey in aliceNode.services.keyManagementService.keys } - - mockNet.stopNodes() + assert.that( + aliceNode.services.startFlow(SwapIdentitiesFlow(bob)), + willReturn( + hasOnlyEntries( + alice to allOf( + !equalTo(alice), + aliceNode.resolvesToWellKnownParty(alice), + aliceNode.holdsOwningKey(), + !bobNode.holdsOwningKey() + ), + bob to allOf( + !equalTo(bob), + bobNode.resolvesToWellKnownParty(bob), + bobNode.holdsOwningKey(), + !aliceNode.holdsOwningKey() + ) + ) + ) + ) } /** @@ -56,58 +61,101 @@ class SwapIdentitiesFlowTests { */ @Test fun `verifies identity name`() { - // Set up values we'll need - val aliceNode = mockNet.createPartyNode(ALICE_NAME) - val bobNode = mockNet.createPartyNode(BOB_NAME) - val charlieNode = mockNet.createPartyNode(CHARLIE_NAME) - val bob: Party = bobNode.services.myInfo.singleIdentity() - val notBob = charlieNode.database.transaction { - charlieNode.services.keyManagementService.freshKeyAndCert(charlieNode.services.myInfo.singleIdentityAndCert(), false) + val notBob = charlieNode.issueFreshKeyAndCert() + val signature = charlieNode.signSwapIdentitiesFlowData(notBob, notBob.owningKey) + assertFailsWith( + "Certificate subject must match counterparty's well known identity.") { + aliceNode.validateSwapIdentitiesFlow(bob, notBob, signature) } - val sigData = SwapIdentitiesFlow.buildDataToSign(notBob) - val signature = charlieNode.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`() { - // Set up values we'll need - val aliceNode = mockNet.createPartyNode(ALICE_NAME) - val bobNode = mockNet.createPartyNode(BOB_NAME) - val alice: PartyAndCertificate = aliceNode.info.singleIdentityAndCert() - val bob: PartyAndCertificate = bobNode.info.singleIdentityAndCert() - // Check that the right name but wrong key is rejected - val evilBobNode = mockNet.createPartyNode(BOB_NAME) + fun `verification rejects signature if name is right but key is wrong`() { + val evilBobNode = mockNet.createPartyNode(bobNode.info.singleIdentity().name) val evilBob = evilBobNode.info.singleIdentityAndCert() - evilBobNode.database.transaction { - val anonymousEvilBob = evilBobNode.services.keyManagementService.freshKeyAndCert(evilBob, false) - val sigData = SwapIdentitiesFlow.buildDataToSign(evilBob) - val signature = evilBobNode.services.keyManagementService.sign(sigData, anonymousEvilBob.owningKey) - assertFailsWith("Signature does not match the given identity and nonce") { - SwapIdentitiesFlow.validateAndRegisterIdentity(aliceNode.services.identityService, bob.party, anonymousEvilBob, signature.withoutKey()) - } - } - // Check that the right signing key, but wrong identity is rejected - val anonymousAlice: PartyAndCertificate = aliceNode.database.transaction { - aliceNode.services.keyManagementService.freshKeyAndCert(alice, false) - } - bobNode.database.transaction { - bobNode.services.keyManagementService.freshKeyAndCert(bob, 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.party, anonymousBob, signature.withoutKey()) - } - } + val anonymousEvilBob = evilBobNode.issueFreshKeyAndCert() + val signature = evilBobNode.signSwapIdentitiesFlowData(evilBob, anonymousEvilBob.owningKey) - mockNet.stopNodes() + assertFailsWith( + "Signature does not match the given identity and nonce") { + aliceNode.validateSwapIdentitiesFlow(bob, anonymousEvilBob, signature) + } } + + @Test + fun `verification rejects signature if key is right but name is wrong`() { + val anonymousAlice = aliceNode.issueFreshKeyAndCert() + val anonymousBob = bobNode.issueFreshKeyAndCert() + val signature = bobNode.signSwapIdentitiesFlowData(anonymousAlice, anonymousBob.owningKey) + + assertFailsWith( + "Signature does not match the given identity and nonce.") { + aliceNode.validateSwapIdentitiesFlow(bob, anonymousBob, signature) + } + } + + //region Operations + private fun TestStartedNode.issueFreshKeyAndCert() = database.transaction { + services.keyManagementService.freshKeyAndCert(services.myInfo.singleIdentityAndCert(), false) + } + + private fun TestStartedNode.signSwapIdentitiesFlowData(party: PartyAndCertificate, owningKey: PublicKey) = + services.keyManagementService.sign( + SwapIdentitiesFlow.buildDataToSign(party), + owningKey) + + private fun TestStartedNode.validateSwapIdentitiesFlow( + party: Party, + counterparty: PartyAndCertificate, + signature: DigitalSignature.WithKey) = + SwapIdentitiesFlow.validateAndRegisterIdentity( + services.identityService, + party, + counterparty, + signature.withoutKey() + ) + //endregion + + //region Matchers + private fun TestStartedNode.resolvesToWellKnownParty(party: Party) = object : Matcher { + override val description = """ + is resolved by "${this@resolvesToWellKnownParty.info.singleIdentity().name}" to well-known party "${party.name}" + """.trimIndent() + + override fun invoke(actual: AnonymousParty): MatchResult { + val resolvedName = services.identityService.wellKnownPartyFromAnonymous(actual)!!.name + return if (resolvedName == party.name) { + MatchResult.Match + } else { + MatchResult.Mismatch("was resolved to $resolvedName") + } + } + } + + private data class HoldsOwningKeyMatcher(val node: TestStartedNode, val negated: Boolean = false) : Matcher { + private fun sayNotIf(negation: Boolean) = if (negation) { "not " } else { "" } + + override val description = + "has an owning key which is ${sayNotIf(negated)}held by ${node.info.singleIdentity().name}" + + override fun invoke(actual: AnonymousParty) = + if (negated != actual.owningKey in node.services.keyManagementService.keys) { + MatchResult.Match + } else { + MatchResult.Mismatch(""" + had an owning key which was ${sayNotIf(!negated)}held by ${node.info.singleIdentity().name} + """.trimIndent()) + } + + override fun not(): Matcher { + return copy(negated=!negated) + } + } + + private fun TestStartedNode.holdsOwningKey() = HoldsOwningKeyMatcher(this) + //endregion + } 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 81299f7ada..3a19824b7b 100644 --- a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt @@ -5,8 +5,8 @@ import com.natpryce.hamkrest.* import com.natpryce.hamkrest.assertion.assert import net.corda.core.contracts.Attachment import net.corda.core.crypto.SecureHash -import net.corda.core.flows.matchers.flow.willReturn -import net.corda.core.flows.matchers.flow.willThrow +import net.corda.testing.internal.matchers.flow.willReturn +import net.corda.testing.internal.matchers.flow.willThrow import net.corda.core.flows.mixins.WithMockNet import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party @@ -16,6 +16,7 @@ import net.corda.core.internal.hash import net.corda.node.services.persistence.NodeAttachmentService import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.makeUnique import net.corda.testing.core.singleIdentity import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNodeParameters @@ -120,13 +121,13 @@ class AttachmentTests : WithMockNet { //region Generators override fun makeNode(name: CordaX500Name) = - mockNet.createPartyNode(randomise(name)).apply { + mockNet.createPartyNode(makeUnique(name)).apply { registerInitiatedFlow(FetchAttachmentsResponse::class.java) } // Makes a node that doesn't do sanity checking at load time. private fun makeBadNode(name: CordaX500Name) = mockNet.createNode( - InternalMockNodeParameters(legalName = randomise(name)), + InternalMockNodeParameters(legalName = makeUnique(name)), nodeFactory = { args, _ -> object : InternalMockNetwork.MockNode(args) { override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false } 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 96d9b77f5a..c8bf1fad7a 100644 --- a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -5,8 +5,8 @@ import com.natpryce.hamkrest.assertion.assert import net.corda.core.contracts.Command import net.corda.core.contracts.StateAndContract import net.corda.core.contracts.requireThat -import net.corda.core.flows.matchers.flow.willReturn -import net.corda.core.flows.matchers.flow.willThrow +import net.corda.testing.internal.matchers.flow.willReturn +import net.corda.testing.internal.matchers.flow.willThrow import net.corda.core.flows.mixins.WithContracts import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowRPCTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowRPCTest.kt index f185bf33ec..585e269de1 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowRPCTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowRPCTest.kt @@ -8,8 +8,8 @@ import com.natpryce.hamkrest.isA import net.corda.core.CordaRuntimeException import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef -import net.corda.core.flows.matchers.rpc.willReturn -import net.corda.core.flows.matchers.rpc.willThrow +import net.corda.testing.internal.matchers.rpc.willReturn +import net.corda.testing.internal.matchers.rpc.willThrow import net.corda.core.flows.mixins.WithContracts import net.corda.core.flows.mixins.WithFinality import net.corda.core.messaging.CordaRPCOps 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 9f4bbc7bf1..ead8da3ae4 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -3,8 +3,8 @@ package net.corda.core.flows import com.natpryce.hamkrest.* import com.natpryce.hamkrest.assertion.assert import net.corda.core.contracts.* -import net.corda.core.flows.matchers.flow.willReturn -import net.corda.core.flows.matchers.flow.willThrow +import net.corda.testing.internal.matchers.flow.willReturn +import net.corda.testing.internal.matchers.flow.willThrow import net.corda.core.flows.mixins.WithContracts import net.corda.core.flows.mixins.WithFinality import net.corda.core.identity.AbstractParty @@ -80,9 +80,9 @@ class ContractUpgradeFlowTest : WithContracts, WithFinality { // Party A initiates contract upgrade flow, expected to succeed this time. assert.that( aliceNode.initiateDummyContractUpgrade(atx), - willReturn( - aliceNode.hasDummyContractUpgradeTransaction() - and bobNode.hasDummyContractUpgradeTransaction())) + willReturn( + aliceNode.hasDummyContractUpgradeTransaction() + and bobNode.hasDummyContractUpgradeTransaction())) } private fun TestStartedNode.issueCash(amount: Amount = Amount(1000, USD)) = 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 17e2a5c6ec..08d6741580 100644 --- a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt @@ -2,8 +2,8 @@ package net.corda.core.flows import com.natpryce.hamkrest.and import com.natpryce.hamkrest.assertion.assert -import net.corda.core.flows.matchers.flow.willReturn -import net.corda.core.flows.matchers.flow.willThrow +import net.corda.testing.internal.matchers.flow.willReturn +import net.corda.testing.internal.matchers.flow.willThrow import net.corda.core.flows.mixins.WithFinality import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction diff --git a/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt index 2001f26b9c..c546a06c20 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt @@ -2,7 +2,7 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import com.natpryce.hamkrest.assertion.assert -import net.corda.core.flows.matchers.flow.willReturn +import net.corda.testing.internal.matchers.flow.willReturn import net.corda.core.flows.mixins.WithMockNet import net.corda.core.identity.Party import net.corda.core.utilities.UntrustworthyData diff --git a/core/src/test/kotlin/net/corda/core/flows/matchers/flow/FlowMatchers.kt b/core/src/test/kotlin/net/corda/core/flows/matchers/flow/FlowMatchers.kt deleted file mode 100644 index 7ebea9131b..0000000000 --- a/core/src/test/kotlin/net/corda/core/flows/matchers/flow/FlowMatchers.kt +++ /dev/null @@ -1,36 +0,0 @@ -package net.corda.core.flows.matchers.flow - -import com.natpryce.hamkrest.Matcher -import com.natpryce.hamkrest.equalTo -import com.natpryce.hamkrest.has -import net.corda.core.flows.matchers.willReturn -import net.corda.core.flows.matchers.willThrow -import net.corda.core.internal.FlowStateMachine - -/** - * Matches a Flow that succeeds with a result matched by the given matcher - */ -fun willReturn() = has(FlowStateMachine::resultFuture, willReturn()) - -fun willReturn(expected: T): Matcher> = net.corda.core.flows.matchers.flow.willReturn(equalTo(expected)) - -/** - * Matches a Flow that succeeds with a result matched by the given matcher - */ -fun willReturn(successMatcher: Matcher) = has( - FlowStateMachine::resultFuture, - willReturn(successMatcher)) - -/** - * Matches a Flow that fails, with an exception matched by the given matcher. - */ -inline fun willThrow(failureMatcher: Matcher) = has( - FlowStateMachine<*>::resultFuture, - willThrow(failureMatcher)) - -/** - * Matches a Flow that fails, with an exception of the specified type. - */ -inline fun willThrow() = has( - FlowStateMachine<*>::resultFuture, - willThrow()) \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/flows/matchers/rpc/RpcMatchers.kt b/core/src/test/kotlin/net/corda/core/flows/matchers/rpc/RpcMatchers.kt deleted file mode 100644 index dcc365d301..0000000000 --- a/core/src/test/kotlin/net/corda/core/flows/matchers/rpc/RpcMatchers.kt +++ /dev/null @@ -1,31 +0,0 @@ -package net.corda.core.flows.matchers.rpc - -import com.natpryce.hamkrest.Matcher -import com.natpryce.hamkrest.has -import net.corda.core.flows.matchers.willReturn -import net.corda.core.flows.matchers.willThrow -import net.corda.core.messaging.FlowHandle - -/** - * Matches a flow handle that succeeds with a result matched by the given matcher - */ -fun willReturn() = has(FlowHandle::returnValue, willReturn()) - -/** - * Matches a flow handle that succeeds with a result matched by the given matcher - */ -fun willReturn(successMatcher: Matcher) = has(FlowHandle::returnValue, willReturn(successMatcher)) - -/** - * Matches a flow handle that fails, with an exception matched by the given matcher. - */ -inline fun willThrow(failureMatcher: Matcher) = has( - FlowHandle<*>::returnValue, - willThrow(failureMatcher)) - -/** - * Matches a flow handle that fails, with an exception of the specified type. - */ -inline fun willThrow() = has( - FlowHandle<*>::returnValue, - willThrow()) \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/flows/mixins/WithMockNet.kt b/core/src/test/kotlin/net/corda/core/flows/mixins/WithMockNet.kt index 0901da704d..02c70ecc3f 100644 --- a/core/src/test/kotlin/net/corda/core/flows/mixins/WithMockNet.kt +++ b/core/src/test/kotlin/net/corda/core/flows/mixins/WithMockNet.kt @@ -9,10 +9,10 @@ import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.FlowStateMachine import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder +import net.corda.testing.core.makeUnique import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.TestStartedNode import net.corda.testing.node.internal.startFlow -import java.util.* import kotlin.reflect.KClass /** @@ -25,12 +25,7 @@ interface WithMockNet { /** * Create a node using a randomised version of the given name */ - fun makeNode(name: CordaX500Name) = mockNet.createPartyNode(randomise(name)) - - /** - * Randomise a party name to avoid clashes with other tests - */ - fun randomise(name: CordaX500Name) = name.copy(commonName = "${name.commonName}_${UUID.randomUUID()}") + fun makeNode(name: CordaX500Name) = mockNet.createPartyNode(makeUnique(name)) /** * Run the mock network before proceeding diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index 739dccf3df..91d36fe238 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -337,7 +337,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe } } - override val started: TestStartedNode? get() = uncheckedCast(super.started) + override val started: TestStartedNode? get() = super.started override fun createStartedNode(nodeInfo: NodeInfo, rpcOps: CordaRPCOps, notaryService: NotaryService?): TestStartedNode { return TestStartedNodeImpl( diff --git a/testing/test-utils/build.gradle b/testing/test-utils/build.gradle index 98beb3372a..36c44a076b 100644 --- a/testing/test-utils/build.gradle +++ b/testing/test-utils/build.gradle @@ -24,6 +24,7 @@ dependencies { compile 'com.nhaarman:mockito-kotlin:1.5.0' compile "org.mockito:mockito-core:$mockito_version" compile "org.assertj:assertj-core:$assertj_version" + compile "com.natpryce:hamkrest:$hamkrest_version" // Guava: Google test library (collections test suite) compile "com.google.guava:guava-testlib:$guava_version" diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestUtils.kt index 9b092d5f45..3de6c0a16b 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestUtils.kt @@ -25,6 +25,7 @@ import java.math.BigInteger import java.security.KeyPair import java.security.PublicKey import java.security.cert.X509Certificate +import java.util.* import java.util.concurrent.atomic.AtomicInteger /** @@ -108,6 +109,18 @@ fun getTestPartyAndCertificate(name: CordaX500Name, publicKey: PublicKey): Party return getTestPartyAndCertificate(Party(name, publicKey)) } + +private val count = AtomicInteger(0) +/** + * Randomise a party name to avoid clashes with other tests + */ +fun makeUnique(name: CordaX500Name) = name.copy(commonName = + if (name.commonName == null) { + count.incrementAndGet().toString() + } else { + "${ name.commonName }_${ count.incrementAndGet() }" + }) + /** * A class that encapsulates a test identity containing a [CordaX500Name] and a [KeyPair], alongside a range * of utility methods for use during testing. diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/Matchers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/Matchers.kt new file mode 100644 index 0000000000..e65c1b1cdb --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/Matchers.kt @@ -0,0 +1,99 @@ +package net.corda.testing.internal.matchers + +import com.natpryce.hamkrest.* + +internal fun indent(description: String) = description.lineSequence().map { "\t$it" }.joinToString("\n") + +fun hasEntrySetSize(expected: Int) = object : Matcher> { + override val description = "is a map of size $expected" + override fun invoke(actual: Map<*, *>) = + if (actual.size == expected) { + MatchResult.Match + } else { + MatchResult.Mismatch("was a map of size ${actual.size}") + } +} + +fun Matcher.redescribe(redescriber: (String) -> String) = object : Matcher { + override val description = redescriber(this@redescribe.description) + override fun invoke(actual: T) = this@redescribe(actual) +} + +fun Matcher.redescribeMismatch(redescriber: (String) -> String) = object : Matcher { + override val description = this@redescribeMismatch.description + override fun invoke(actual: T) = this@redescribeMismatch(actual).modifyMismatchDescription(redescriber) +} + +fun MatchResult.modifyMismatchDescription(modify: (String) -> String) = when(this) { + is MatchResult.Match -> MatchResult.Match + is MatchResult.Mismatch -> MatchResult.Mismatch(modify(this.description)) +} + +fun Matcher.extrude(projection: (O) -> I) = object : Matcher { + override val description = this@extrude.description + override fun invoke(actual: O) = this@extrude(projection(actual)) +} + +internal fun hasAnEntry(key: K, valueMatcher: Matcher) = object : Matcher> { + override val description = "$key: ${valueMatcher.description}" + override fun invoke(actual: Map): MatchResult = + actual[key]?.let { valueMatcher(it) }?.let { when(it) { + is MatchResult.Match -> it + is MatchResult.Mismatch -> MatchResult.Mismatch("$key: ${it.description}") + }} ?: MatchResult.Mismatch("$key was not present") +} + +fun hasEntry(key: K, valueMatcher: Matcher) = + hasAnEntry(key, valueMatcher).redescribe { "Is a map containing the entry:\n${indent(it)}"} + +fun hasOnlyEntries(vararg entryMatchers: Pair>) = hasOnlyEntries(entryMatchers.toList()) + +fun hasOnlyEntries(entryMatchers: Collection>>) = + allOf( + hasEntrySetSize(entryMatchers.size), + hasEntries(entryMatchers) + ) + +fun hasEntries(vararg entryMatchers: Pair>) = hasEntries(entryMatchers.toList()) + +fun hasEntries(entryMatchers: Collection>>) = object : Matcher> { + override val description = + "is a map containing the entries:\n" + + entryMatchers.asSequence() + .joinToString("\n") { indent("${it.first}: ${it.second.description}") } + + override fun invoke(actual: Map): MatchResult { + val mismatches = entryMatchers.map { hasAnEntry(it.first, it.second)(actual) } + .filterIsInstance() + + return if (mismatches.isEmpty()) { + MatchResult.Match + } else { + MatchResult.Mismatch( + "had entries which did not meet criteria:\n" + + mismatches.joinToString("\n") { indent(it.description) }) + } + } +} + +fun allOf(vararg matchers: Matcher) = allOf(matchers.toList()) + +fun allOf(matchers: Collection>) = object : Matcher { + override val description = + "meets all of the criteria:\n" + + matchers.asSequence() + .joinToString("\n") { indent(it.description) } + + override fun invoke(actual: T) : MatchResult { + val mismatches = matchers.map { it(actual) } + .filterIsInstance() + + return if (mismatches.isEmpty()) { + MatchResult.Match + } else { + MatchResult.Mismatch( + "did not meet criteria:\n" + + mismatches.joinToString("\n") { indent(it.description) }) + } + } +} \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/flow/FlowMatchers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/flow/FlowMatchers.kt new file mode 100644 index 0000000000..2e66073fc9 --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/flow/FlowMatchers.kt @@ -0,0 +1,38 @@ +package net.corda.testing.internal.matchers.flow + +import com.natpryce.hamkrest.Matcher +import com.natpryce.hamkrest.equalTo +import net.corda.core.internal.FlowStateMachine +import net.corda.testing.internal.matchers.* + +/** + * Matches a Flow that succeeds with a result matched by the given matcher + */ +fun willReturn(): Matcher> = net.corda.testing.internal.matchers.future.willReturn() + .extrude(FlowStateMachine::resultFuture) + .redescribe { "is a flow that will return" } + +fun willReturn(expected: T): Matcher> = willReturn(equalTo(expected)) + +/** + * Matches a Flow that succeeds with a result matched by the given matcher + */ +fun willReturn(successMatcher: Matcher) = net.corda.testing.internal.matchers.future.willReturn(successMatcher) + .extrude(FlowStateMachine::resultFuture) + .redescribe { "is a flow that will return with a value that ${successMatcher.description}" } + +/** + * Matches a Flow that fails, with an exception matched by the given matcher. + */ +inline fun willThrow(failureMatcher: Matcher) = + net.corda.testing.internal.matchers.future.willThrow(failureMatcher) + .extrude(FlowStateMachine<*>::resultFuture) + .redescribe { "is a flow that will fail, throwing an exception that ${failureMatcher.description}" } + +/** + * Matches a Flow that fails, with an exception of the specified type. + */ +inline fun willThrow() = + net.corda.testing.internal.matchers.future.willThrow() + .extrude(FlowStateMachine<*>::resultFuture) + .redescribe { "is a flow that will fail with an exception of type ${E::class.java.simpleName}" } \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/flows/matchers/FutureMatchers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/future/FutureMatchers.kt similarity index 75% rename from core/src/test/kotlin/net/corda/core/flows/matchers/FutureMatchers.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/future/FutureMatchers.kt index dd0edc4747..e1e42108c8 100644 --- a/core/src/test/kotlin/net/corda/core/flows/matchers/FutureMatchers.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/future/FutureMatchers.kt @@ -1,9 +1,10 @@ -package net.corda.core.flows.matchers +package net.corda.testing.internal.matchers.future import com.natpryce.hamkrest.MatchResult import com.natpryce.hamkrest.Matcher import com.natpryce.hamkrest.equalTo import net.corda.core.utilities.getOrThrow +import net.corda.testing.internal.matchers.modifyMismatchDescription import java.util.concurrent.Future /** @@ -16,7 +17,7 @@ fun willReturn() = object : Matcher> { actual.getOrThrow() MatchResult.Match } catch (e: Exception) { - MatchResult.Mismatch("Failed with $e") + MatchResult.Mismatch("failed with $e") } } @@ -29,9 +30,9 @@ fun willReturn(successMatcher: Matcher) = object : Matcher> override val description: String = "is a future that will succeed with a value that ${successMatcher.description}" override fun invoke(actual: Future): MatchResult = try { - successMatcher(actual.getOrThrow()) + successMatcher(actual.getOrThrow()).modifyMismatchDescription { "succeeded with value that $it" } } catch (e: Exception) { - MatchResult.Mismatch("Failed with $e") + MatchResult.Mismatch("failed with $e") } } @@ -44,11 +45,11 @@ inline fun willThrow(failureMatcher: Matcher) = object override fun invoke(actual: Future<*>): MatchResult = try { actual.getOrThrow() - MatchResult.Mismatch("Succeeded") + MatchResult.Mismatch("succeeded") } catch (e: Exception) { when(e) { - is E -> failureMatcher(e) - else -> MatchResult.Mismatch("Failure class was ${e.javaClass}") + is E -> failureMatcher(e).modifyMismatchDescription { "failed with ${E::class.java.simpleName} that $it" } + else -> MatchResult.Mismatch("failed with ${e.javaClass}") } } } @@ -62,11 +63,11 @@ inline fun willThrow() = object : Matcher> { override fun invoke(actual: Future<*>): MatchResult = try { actual.getOrThrow() - MatchResult.Mismatch("Succeeded") + MatchResult.Mismatch("succeeded") } catch (e: Exception) { when(e) { is E -> MatchResult.Match - else -> MatchResult.Mismatch("Failure class was ${e.javaClass}") + else -> MatchResult.Mismatch("failed with ${e.javaClass}") } } } \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/rpc/RpcMatchers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/rpc/RpcMatchers.kt new file mode 100644 index 0000000000..a5fb84997a --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/matchers/rpc/RpcMatchers.kt @@ -0,0 +1,36 @@ +package net.corda.testing.internal.matchers.rpc + +import com.natpryce.hamkrest.Matcher +import net.corda.core.messaging.FlowHandle +import net.corda.testing.internal.matchers.extrude +import net.corda.testing.internal.matchers.redescribe + +/** + * Matches a flow handle that succeeds with a result matched by the given matcher + */ +fun willReturn() = net.corda.testing.internal.matchers.future.willReturn() + .extrude(FlowHandle::returnValue) + .redescribe { "is an RPG flow handle that will return" } + +/** + * Matches a flow handle that succeeds with a result matched by the given matcher + */ +fun willReturn(successMatcher: Matcher) = net.corda.testing.internal.matchers.future.willReturn(successMatcher) + .extrude(FlowHandle::returnValue) + .redescribe { "is an RPG flow handle that will return a value that ${successMatcher.description}" } + +/** + * Matches a flow handle that fails, with an exception matched by the given matcher. + */ +inline fun willThrow(failureMatcher: Matcher) = + net.corda.testing.internal.matchers.future.willThrow(failureMatcher) + .extrude(FlowHandle<*>::returnValue) + .redescribe { "is an RPG flow handle that will fail with an exception that ${failureMatcher.description}" } + +/** + * Matches a flow handle that fails, with an exception of the specified type. + */ +inline fun willThrow() = + net.corda.testing.internal.matchers.future.willThrow() + .extrude(FlowHandle<*>::returnValue) + .redescribe { "is an RPG flow handle that will fail with an exception of type ${E::class.java.simpleName}" } \ No newline at end of file diff --git a/testing/test-utils/src/test/kotlin/net/corda/testing/internal/MatcherTests.kt b/testing/test-utils/src/test/kotlin/net/corda/testing/internal/MatcherTests.kt new file mode 100644 index 0000000000..b36a5fb6e9 --- /dev/null +++ b/testing/test-utils/src/test/kotlin/net/corda/testing/internal/MatcherTests.kt @@ -0,0 +1,58 @@ +package net.corda.testing.internal + +import com.natpryce.hamkrest.MatchResult +import com.natpryce.hamkrest.equalTo +import net.corda.testing.internal.matchers.hasEntries +import org.junit.Test +import kotlin.test.assertEquals + +class MatcherTests { + @Test + fun `nested items indent`() { + val nestedMap = mapOf( + "a" to mapOf( + "apple" to "vegetable", + "aardvark" to "animal", + "anthracite" to "mineral"), + "b" to mapOf( + "broccoli" to "mineral", + "bison" to "animal", + "bauxite" to "vegetable") + ) + + val matcher = hasEntries( + "a" to hasEntries( + "aardvark" to equalTo("animal"), + "anthracite" to equalTo("mineral") + ), + "b" to hasEntries( + "bison" to equalTo("animal"), + "bauxite" to equalTo("mineral") + ) + ) + + println(matcher.description) + println((matcher(nestedMap) as MatchResult.Mismatch).description) + + assertEquals( + """ + is a map containing the entries: + a: is a map containing the entries: + aardvark: is equal to "animal" + anthracite: is equal to "mineral" + b: is a map containing the entries: + bison: is equal to "animal" + bauxite: is equal to "mineral" + """.trimIndent().replace(" ", "\t"), + matcher.description) + + assertEquals( + """ + had entries which did not meet criteria: + b: had entries which did not meet criteria: + bauxite: was: "vegetable" + """.trimIndent().replace(" ", "\t"), + (matcher(nestedMap) as MatchResult.Mismatch).description + ) + } +} \ No newline at end of file From c23167f08e30d2891dbff807da8df3b32157cc73 Mon Sep 17 00:00:00 2001 From: szymonsztuka Date: Wed, 1 Aug 2018 11:50:42 +0100 Subject: [PATCH 9/9] ENT-2188 fix H2 insecure default configuration (#3692) Set the "h2.allowedClasses" system property, require database password when exposing H2 server on non-localhost address, samples start H2 server by default (reintroduces the behaviour before h2Settings.address configuration option was added) --- docs/source/corda-configuration-file.rst | 2 +- docs/source/example-code/build.gradle | 2 + docs/source/node-database-access-h2.rst | 6 +- docs/source/node-database.rst | 22 ++- .../corda/node/AddressBindingFailureTests.kt | 2 +- .../corda/node/persistence/H2SecurityTests.kt | 136 ++++++++++++++++++ .../kotlin/net/corda/node/internal/Node.kt | 12 +- samples/attachment-demo/build.gradle | 3 + .../net/corda/bank/BankOfCordaCordform.kt | 5 +- samples/cordapp-configuration/build.gradle | 7 +- samples/irs-demo/cordapp/build.gradle | 4 + samples/network-verifier/build.gradle | 4 +- samples/notary-demo/README.md | 2 +- .../net/corda/notarydemo/BFTNotaryCordform.kt | 3 + .../corda/notarydemo/CustomNotaryCordform.kt | 3 + .../corda/notarydemo/RaftNotaryCordform.kt | 3 + .../corda/notarydemo/SingleNotaryCordform.kt | 3 + samples/simm-valuation-demo/build.gradle | 12 +- samples/trader-demo/build.gradle | 4 + 19 files changed, 220 insertions(+), 15 deletions(-) create mode 100644 node/src/integration-test/kotlin/net/corda/node/persistence/H2SecurityTests.kt diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index a8f680774c..241b4cf2d1 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -86,7 +86,7 @@ absolute path to the node's base directory. :h2Port: Deprecated. Use ``h2Settings`` instead. -:h2Settings: Sets the H2 JDBC server port. See :doc:`node-database-access-h2`. +:h2Settings: Sets the H2 JDBC server host and port. See :doc:`node-database-access-h2`. For non-localhost address the database passowrd needs to be set in ``dataSourceProperties``. :messagingServerAddress: The address of the ArtemisMQ broker instance. If not provided the node will run one locally. diff --git a/docs/source/example-code/build.gradle b/docs/source/example-code/build.gradle index 54c5ad53de..9a2d0ed5a4 100644 --- a/docs/source/example-code/build.gradle +++ b/docs/source/example-code/build.gradle @@ -86,6 +86,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { adminAddress "localhost:10013" } webPort 10004 + extraConfig = ['h2Settings.address' : 'localhost:10014'] cordapps = [] } node { @@ -96,6 +97,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { adminAddress "localhost:10016" } webPort 10007 + extraConfig = ['h2Settings.address' : 'localhost:10017'] cordapps = [] rpcUsers = [ ['username' : "user", diff --git a/docs/source/node-database-access-h2.rst b/docs/source/node-database-access-h2.rst index 220f24c4ac..e68ae41aeb 100644 --- a/docs/source/node-database-access-h2.rst +++ b/docs/source/node-database-access-h2.rst @@ -32,13 +32,17 @@ If you want H2 to auto-select a port (mimicking the old ``h2Port`` behaviour), y address: "localhost:0" } -If remote access is required, the address can be changed to ``0.0.0.0``. However it is recommended to change the default username and password before doing so. +If remote access is required, the address can be changed to ``0.0.0.0``. +The node requires a database password to be set when the database is exposed on the network interface to listen on. .. sourcecode:: groovy h2Settings { address: "0.0.0.0:12345" } + dataSourceProperties { + dataSource.password : "strongpassword" + } The previous ``h2Port`` syntax is now deprecated. ``h2Port`` will continue to work but the database will only be accessible on localhost. diff --git a/docs/source/node-database.rst b/docs/source/node-database.rst index cbb44c9548..3e1cf88439 100644 --- a/docs/source/node-database.rst +++ b/docs/source/node-database.rst @@ -3,7 +3,24 @@ Node database Default in-memory database -------------------------- -By default, nodes store their data in an H2 database. You can connect directly to a running node's database to see its +By default, nodes store their data in an H2 database. +The database (a file persistence.mv.db) is created at the first node startup with the administrator user 'sa' and a blank password. +The user name and password can be changed in node configuration: + +.. sourcecode:: groovy + + dataSourceProperties = { + dataSource.user = [USER] + dataSource.password = [PASSWORD] + } + +Note, changing user/password for the existing node in node.conf will not update them in the H2 database, +you need to login to the database first to create new user or change the user password. +The database password is required only when the H2 database is exposed on non-localhost address (which is disabled by default). +The node requires the user with administrator permissions in order to creates tables upon the first startup +or after deplying new CorDapps with own tables. + +You can connect directly to a running node's database to see its stored states, transactions and attachments as follows: * Enable the H2 database access in the node configuration using the following syntax: @@ -35,7 +52,8 @@ interface for you to query them using SQL. The default behaviour is to expose the H2 database on localhost. This can be overridden in the node configuration using ``h2Settings.address`` and specifying the address of the network interface to listen on, -or simply using ``0.0.0.0:0`` to listen on all interfaces. +or simply using ``0.0.0.0:0`` to listen on all interfaces. The node requires a database password to be set when +the database is exposed on the network interface to listen on. PostgreSQL ---------- diff --git a/node/src/integration-test/kotlin/net/corda/node/AddressBindingFailureTests.kt b/node/src/integration-test/kotlin/net/corda/node/AddressBindingFailureTests.kt index 425e7a4e53..8b366f33a9 100644 --- a/node/src/integration-test/kotlin/net/corda/node/AddressBindingFailureTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/AddressBindingFailureTests.kt @@ -28,7 +28,7 @@ class AddressBindingFailureTests { fun `rpc admin address`() = assertBindExceptionForOverrides { address -> mapOf("rpcSettings" to mapOf("adminAddress" to address.toString())) } @Test - fun `H2 address`() = assertBindExceptionForOverrides { address -> mapOf("h2Settings" to mapOf("address" to address.toString())) } + fun `H2 address`() = assertBindExceptionForOverrides { address -> mapOf("h2Settings" to mapOf("address" to address.toString()), "dataSourceProperties.dataSource.password" to "password") } private fun assertBindExceptionForOverrides(overrides: (NetworkHostAndPort) -> Map) { diff --git a/node/src/integration-test/kotlin/net/corda/node/persistence/H2SecurityTests.kt b/node/src/integration-test/kotlin/net/corda/node/persistence/H2SecurityTests.kt new file mode 100644 index 0000000000..57dad79ea5 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/persistence/H2SecurityTests.kt @@ -0,0 +1,136 @@ +package net.corda.node.persistence + +import co.paralleluniverse.fibers.Suspendable +import net.corda.client.rpc.CordaRPCClient +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.getOrThrow +import net.corda.node.services.Permissions +import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.PortAllocation +import net.corda.testing.driver.driver +import net.corda.testing.node.User +import org.junit.Test +import java.net.InetAddress +import java.sql.DriverManager +import kotlin.test.assertFailsWith +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class H2SecurityTests { + companion object { + private val port = PortAllocation.Incremental(21_000) + private fun getFreePort() = port.nextPort() + private const val h2AddressKey = "h2Settings.address" + private const val dbPasswordKey = "dataSourceProperties.dataSource.password" + } + + @Test + fun `h2 server starts when h2Settings are set`() { + driver(DriverParameters(inMemoryDB = false, startNodesInProcess = isQuasarAgentSpecified(), notarySpecs = emptyList())) { + val port = getFreePort() + startNode(customOverrides = mapOf(h2AddressKey to "localhost:$port")).getOrThrow() + DriverManager.getConnection("jdbc:h2:tcp://localhost:$port/node", "sa", "").use { + assertTrue(it.createStatement().executeQuery("SELECT 1").next()) + } + } + } + + @Test + fun `h2 server on the host name requires non-default database password`() { + driver(DriverParameters(inMemoryDB = false, startNodesInProcess = isQuasarAgentSpecified(), notarySpecs = emptyList())) { + assertFailsWith(CouldNotCreateDataSourceException::class) { + startNode(customOverrides = mapOf(h2AddressKey to "${InetAddress.getLocalHost().hostName}:${getFreePort()}")).getOrThrow() + } + } + } + + @Test + fun `h2 server on the external host IP requires non-default database password`() { + driver(DriverParameters(inMemoryDB = false, startNodesInProcess = isQuasarAgentSpecified(), notarySpecs = emptyList())) { + assertFailsWith(CouldNotCreateDataSourceException::class) { + startNode(customOverrides = mapOf(h2AddressKey to "${InetAddress.getLocalHost().hostAddress}:${getFreePort()}")).getOrThrow() + } + } + } + + @Test + fun `h2 server on host name requires non-blank database password`() { + driver(DriverParameters(inMemoryDB = false, startNodesInProcess = isQuasarAgentSpecified(), notarySpecs = emptyList())) { + assertFailsWith(CouldNotCreateDataSourceException::class) { + startNode(customOverrides = mapOf(h2AddressKey to "${InetAddress.getLocalHost().hostName}:${getFreePort()}", + dbPasswordKey to " ")).getOrThrow() + } + } + } + + @Test + fun `h2 server on external host IP requires non-blank database password`() { + driver(DriverParameters(inMemoryDB = false, startNodesInProcess = isQuasarAgentSpecified(), notarySpecs = emptyList())) { + assertFailsWith(CouldNotCreateDataSourceException::class) { + startNode(customOverrides = mapOf(h2AddressKey to "${InetAddress.getLocalHost().hostAddress}:${getFreePort()}", + dbPasswordKey to " ")).getOrThrow() + } + } + } + + @Test + fun `h2 server on localhost runs with the default database password`() { + driver(DriverParameters(inMemoryDB = false, startNodesInProcess = false, notarySpecs = emptyList())) { + startNode(customOverrides = mapOf(h2AddressKey to "localhost:${getFreePort()}")).getOrThrow() + } + } + + @Test + fun `h2 server to loopback IP runs with the default database password`() { + driver(DriverParameters(inMemoryDB = false, startNodesInProcess = isQuasarAgentSpecified(), notarySpecs = emptyList())) { + startNode(customOverrides = mapOf(h2AddressKey to "127.0.0.1:${getFreePort()}")).getOrThrow() + } + } + + @Test + fun `remote code execution via h2 server is disabled`() { + driver(DriverParameters(inMemoryDB = false, startNodesInProcess = false, notarySpecs = emptyList())) { + val port = getFreePort() + startNode(customOverrides = mapOf(h2AddressKey to "localhost:$port", dbPasswordKey to "x")).getOrThrow() + DriverManager.getConnection("jdbc:h2:tcp://localhost:$port/node", "sa", "x").use { + assertFailsWith(org.h2.jdbc.JdbcSQLException::class) { + it.createStatement().execute("CREATE ALIAS SET_PROPERTY FOR \"java.lang.System.setProperty\"") + it.createStatement().execute("CALL SET_PROPERTY('abc', '1')") + } + } + assertNull(System.getProperty("abc")) + } + } + + @Test + fun `malicious flow tries to enable remote code execution via h2 server`() { + val user = User("mark", "dadada", setOf(Permissions.startFlow())) + driver(DriverParameters(inMemoryDB = false, startNodesInProcess = false, notarySpecs = emptyList())) { + val port = getFreePort() + val nodeHandle = startNode(rpcUsers = listOf(user), customOverrides = mapOf(h2AddressKey to "localhost:$port", + dbPasswordKey to "x")).getOrThrow() + CordaRPCClient(nodeHandle.rpcAddress).start(user.username, user.password).use { + it.proxy.startFlow(::MaliciousFlow).returnValue.getOrThrow() + } + DriverManager.getConnection("jdbc:h2:tcp://localhost:$port/node", "sa", "x").use { + assertFailsWith(org.h2.jdbc.JdbcSQLException::class) { + it.createStatement().execute("CREATE ALIAS SET_PROPERTY FOR \"java.lang.System.setProperty\"") + it.createStatement().execute("CALL SET_PROPERTY('abc', '1')") + } + } + assertNull(System.getProperty("abc")) + } + } + + @StartableByRPC + class MaliciousFlow : FlowLogic() { + @Suspendable + override fun call(): Boolean { + System.clearProperty("h2.allowedClasses") + return true + } + } +} \ No newline at end of file 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 e1f4ae7f81..6e5cd7abfb 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -54,6 +54,7 @@ import net.corda.nodeapi.internal.bridging.BridgeControlListener import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.serialization.internal.* +import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException import org.h2.jdbc.JdbcSQLException import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -61,11 +62,13 @@ import rx.Observable import rx.Scheduler import rx.schedulers.Schedulers import java.net.BindException +import java.net.InetAddress import java.nio.file.Path import java.time.Clock import java.util.concurrent.atomic.AtomicInteger import javax.management.ObjectName import kotlin.system.exitProcess +import java.nio.file.Paths class NodeWithInfo(val node: Node, val info: NodeInfo) { val services: StartedNodeServices = object : StartedNodeServices, ServiceHubInternal by node.services, FlowStarter by node.flowStarter {} @@ -331,13 +334,20 @@ open class Node(configuration: NodeConfiguration, if (databaseUrl != null && databaseUrl.startsWith(h2Prefix)) { val effectiveH2Settings = configuration.effectiveH2Settings - + //forbid execution of arbitrary code via SQL except those classes required by H2 itself + System.setProperty("h2.allowedClasses", "org.h2.mvstore.db.MVTableEngine,org.locationtech.jts.geom.Geometry,org.h2.server.TcpServer") if (effectiveH2Settings?.address != null) { + if (!InetAddress.getByName(effectiveH2Settings.address.host).isLoopbackAddress + && configuration.dataSourceProperties.getProperty("dataSource.password").isBlank()) { + throw CouldNotCreateDataSourceException("Database password is required for H2 server listening on ${InetAddress.getByName(effectiveH2Settings.address.host)}.") + } val databaseName = databaseUrl.removePrefix(h2Prefix).substringBefore(';') + val baseDir = Paths.get(databaseName).parent.toString() val server = org.h2.tools.Server.createTcpServer( "-tcpPort", effectiveH2Settings.address.port.toString(), "-tcpAllowOthers", "-tcpDaemon", + "-baseDir", baseDir, "-key", "node", databaseName) // override interface that createTcpServer listens on (which is always 0.0.0.0) System.setProperty("h2.bindAddress", effectiveH2Settings.address.host) diff --git a/samples/attachment-demo/build.gradle b/samples/attachment-demo/build.gradle index 50e4a29d5c..71fbfe48d7 100644 --- a/samples/attachment-demo/build.gradle +++ b/samples/attachment-demo/build.gradle @@ -53,6 +53,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { address "localhost:10003" adminAddress "localhost:10004" } + extraConfig = ['h2Settings.address' : 'localhost:10012'] } node { name "O=Bank A,L=London,C=GB" @@ -63,6 +64,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { address "localhost:10006" adminAddress "localhost:10007" } + extraConfig = ['h2Settings.address' : 'localhost:10013'] } node { name "O=Bank B,L=New York,C=US" @@ -74,6 +76,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { webPort 10010 cordapps = [] rpcUsers = ext.rpcUsers + extraConfig = ['h2Settings.address' : 'localhost:10014'] } } diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt index c84434e94a..06beee5f5f 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt @@ -44,10 +44,12 @@ class BankOfCordaCordform : CordformDefinition() { adminAddress("localhost:10004") } devMode(true) + extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:10016")) } node { name(BOC_NAME) - extraConfig = mapOf("custom" to mapOf("issuableCurrencies" to listOf("USD"))) + extraConfig = mapOf("custom" to mapOf("issuableCurrencies" to listOf("USD")), + "h2Settings" to mapOf("address" to "localhost:10017")) p2pPort(10005) rpcSettings { address("localhost:$BOC_RPC_PORT") @@ -67,6 +69,7 @@ class BankOfCordaCordform : CordformDefinition() { webPort(10010) rpcUsers(User(BIGCORP_RPC_USER, BIGCORP_RPC_PWD, setOf(all()))) devMode(true) + extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:10018")) } } diff --git a/samples/cordapp-configuration/build.gradle b/samples/cordapp-configuration/build.gradle index 37edc09147..a84712f171 100644 --- a/samples/cordapp-configuration/build.gradle +++ b/samples/cordapp-configuration/build.gradle @@ -22,10 +22,11 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { port 10003 adminPort 10004 } + extraConfig = ['h2Settings.address' : 'localhost:10005'] } node { name "O=Bank A,L=London,C=GB" - p2pPort 10005 + p2pPort 10006 cordapps = [] rpcUsers = ext.rpcUsers // This configures the default cordapp for this node @@ -36,10 +37,11 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { port 10007 adminPort 10008 } + extraConfig = ['h2Settings.address' : 'localhost:10009'] } node { name "O=Bank B,L=New York,C=US" - p2pPort 10009 + p2pPort 10010 cordapps = [] rpcUsers = ext.rpcUsers // This configures the default cordapp for this node @@ -50,5 +52,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { port 10011 adminPort 10012 } + extraConfig = ['h2Settings.address' : 'localhost:10013'] } } \ No newline at end of file diff --git a/samples/irs-demo/cordapp/build.gradle b/samples/irs-demo/cordapp/build.gradle index 5e735c8a72..fba2b8f523 100644 --- a/samples/irs-demo/cordapp/build.gradle +++ b/samples/irs-demo/cordapp/build.gradle @@ -70,6 +70,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { cordapps = ["${project(":finance").group}:finance:$corda_release_version"] rpcUsers = rpcUsersList useTestClock true + extraConfig = ['h2Settings.address' : 'localhost:10024'] } node { name "O=Bank A,L=London,C=GB" @@ -81,6 +82,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { cordapps = ["${project(":finance").group}:finance:$corda_release_version"] rpcUsers = rpcUsersList useTestClock true + extraConfig = ['h2Settings.address' : 'localhost:10027'] } node { name "O=Bank B,L=New York,C=US" @@ -92,6 +94,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { cordapps = ["${project.group}:finance:$corda_release_version"] rpcUsers = rpcUsersList useTestClock true + extraConfig = ['h2Settings.address' : 'localhost:10030'] } node { name "O=Regulator,L=Moscow,C=RU" @@ -104,6 +107,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { cordapps = ["${project(":finance").group}:finance:$corda_release_version"] rpcUsers = rpcUsersList useTestClock true + extraConfig = ['h2Settings.address' : 'localhost:10033'] } } diff --git a/samples/network-verifier/build.gradle b/samples/network-verifier/build.gradle index f64efcc692..0245d6027f 100644 --- a/samples/network-verifier/build.gradle +++ b/samples/network-verifier/build.gradle @@ -24,7 +24,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { port 10003 adminPort 10004 } - h2Port 20004 + extraConfig = ['h2Settings.address' : 'localhost:20004'] } node { name "O=Bank A,L=London,C=GB" @@ -35,6 +35,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { port 10007 adminPort 10008 } + extraConfig = ['h2Settings.address' : 'localhost:0'] } node { name "O=Bank B,L=New York,C=US" @@ -45,5 +46,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { port 10011 adminPort 10012 } + extraConfig = ['h2Settings.address' : 'localhost:0'] } } \ No newline at end of file diff --git a/samples/notary-demo/README.md b/samples/notary-demo/README.md index e0bba709ef..569c56351b 100644 --- a/samples/notary-demo/README.md +++ b/samples/notary-demo/README.md @@ -52,7 +52,7 @@ by using the H2 web console: 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`` + ``Database connection url is : jdbc:h2:tcp://localhost:56736/node`` 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 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 0e5df1260d..04e045c115 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 @@ -35,6 +35,7 @@ class BFTNotaryCordform : CordformDefinition() { } rpcUsers(notaryDemoUser) devMode(true) + extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0")) } node { name(BOB_NAME) @@ -44,11 +45,13 @@ class BFTNotaryCordform : CordformDefinition() { adminAddress("localhost:10106") } devMode(true) + extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0")) } val clusterAddresses = (0 until clusterSize).map { NetworkHostAndPort("localhost", 11000 + it * 10) } fun notaryNode(replicaId: Int, configure: CordformNode.() -> Unit) = node { name(notaryNames[replicaId]) notary(NotaryConfig(validating = false, serviceLegalName = clusterName, bftSMaRt = BFTSMaRtConfiguration(replicaId, clusterAddresses))) + extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0")) configure() } notaryNode(0) { 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 index 05fded3f50..21e87c5650 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt @@ -23,6 +23,7 @@ class CustomNotaryCordform : CordformDefinition() { } rpcUsers(notaryDemoUser) devMode(true) + extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0")) } node { name(BOB_NAME) @@ -32,6 +33,7 @@ class CustomNotaryCordform : CordformDefinition() { adminAddress("localhost:10106") } devMode(true) + extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0")) } node { name(DUMMY_NOTARY_NAME) @@ -42,6 +44,7 @@ class CustomNotaryCordform : CordformDefinition() { } notary(NotaryConfig(validating = true, custom = true)) devMode(true) + extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost: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 d6083c42e6..6de371c3e6 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 @@ -35,6 +35,7 @@ class RaftNotaryCordform : CordformDefinition() { } rpcUsers(notaryDemoUser) devMode(true) + extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0")) } node { name(BOB_NAME) @@ -44,11 +45,13 @@ class RaftNotaryCordform : CordformDefinition() { adminAddress("localhost:10106") } devMode(true) + extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0")) } fun notaryNode(index: Int, nodePort: Int, clusterPort: Int? = null, configure: CordformNode.() -> Unit) = node { name(notaryNames[index]) val clusterAddresses = if (clusterPort != null) listOf(NetworkHostAndPort("localhost", clusterPort)) else emptyList() notary(NotaryConfig(validating = true, serviceLegalName = clusterName, raft = RaftConfig(NetworkHostAndPort("localhost", nodePort), clusterAddresses))) + extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0")) configure() devMode(true) } 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 8358ed7e8c..b2ebaac841 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 @@ -29,6 +29,7 @@ class SingleNotaryCordform : CordformDefinition() { } rpcUsers(notaryDemoUser) devMode(true) + extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0")) } node { name(BOB_NAME) @@ -38,6 +39,7 @@ class SingleNotaryCordform : CordformDefinition() { adminAddress("localhost:10106") } devMode(true) + extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0")) } node { name(DUMMY_NOTARY_NAME) @@ -48,6 +50,7 @@ class SingleNotaryCordform : CordformDefinition() { } notary(NotaryConfig(validating = true)) devMode(true) + extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0")) } } diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index 576460d0e6..d4d0b98caa 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -80,7 +80,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { extraConfig = [ custom: [ jvmArgs: ["-Xmx1g"] - ] + ], + 'h2Settings.address' : 'localhost:10038' ] } node { @@ -98,7 +99,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { extraConfig = [ custom: [ jvmArgs: ["-Xmx1g"] - ] + ], + 'h2Settings.address' : 'localhost:10039' ] } node { @@ -116,7 +118,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { extraConfig = [ custom: [ jvmArgs: ["-Xmx1g"] - ] + ], + 'h2Settings.address' : 'localhost:10040' ] } node { @@ -134,7 +137,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { extraConfig = [ custom: [ jvmArgs: ["-Xmx1g"] - ] + ], + 'h2Settings.address' : 'localhost:10041' ] } } diff --git a/samples/trader-demo/build.gradle b/samples/trader-demo/build.gradle index 7cebb99ed6..afd027fdbc 100644 --- a/samples/trader-demo/build.gradle +++ b/samples/trader-demo/build.gradle @@ -54,6 +54,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { address "localhost:10003" adminAddress "localhost:10004" } + extraConfig = ['h2Settings.address' : 'localhost:10014'] cordapps = ["$project.group:finance:$corda_release_version"] } node { @@ -65,6 +66,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { address "localhost:10006" adminAddress "localhost:10007" } + extraConfig = ['h2Settings.address' : 'localhost:10015'] } node { name "O=Bank B,L=New York,C=US" @@ -75,6 +77,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { address "localhost:10009" adminAddress "localhost:10010" } + extraConfig = ['h2Settings.address' : 'localhost:10016'] } node { name "O=BankOfCorda,L=New York,C=US" @@ -85,6 +88,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { address "localhost:10012" adminAddress "localhost:10013" } + extraConfig = ['h2Settings.address' : 'localhost:10017'] } }