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 f4db3bf103..228a2d587c 100644 --- a/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt +++ b/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt @@ -10,55 +10,60 @@ 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() + ) + ) + ) + ) } /** @@ -66,58 +71,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-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 33574c27ee..566df14eaa 100644 --- a/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt +++ b/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt @@ -26,15 +26,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/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 39a14c4d94..4c35473fe7 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -399,18 +399,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 @@ -510,29 +498,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 833db70277..190700e236 100644 --- a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt +++ b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt @@ -10,6 +10,7 @@ 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 @@ -115,4 +116,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 8a8212838b..523ec4c721 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -11,6 +11,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 @@ -19,9 +20,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 @@ -84,7 +87,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) @@ -105,11 +108,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 @@ -119,14 +129,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 + ) } } @@ -179,12 +202,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) @@ -293,10 +313,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. */ @@ -312,7 +332,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/flows/AttachmentTests.kt b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt index eb3d17eb65..fc188c1509 100644 --- a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt @@ -15,8 +15,8 @@ import 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 @@ -26,6 +26,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 @@ -130,13 +131,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 981b217a53..d7362b0355 100644 --- a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -15,8 +15,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 40d8624644..9a6deff59a 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -13,8 +13,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 @@ -90,9 +90,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 c30e47ae73..96e1d0cd65 100644 --- a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt @@ -12,8 +12,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 9e1f1cb81b..d407967182 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt @@ -12,7 +12,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/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 09af78383f..d95219cbca 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,8 +6,13 @@ 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 + 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. @@ -195,7 +200,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/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index 91806630da..9c0e79e1ce 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -105,7 +105,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 c1cf994177..6897e5dd5a 100644 --- a/docs/source/example-code/build.gradle +++ b/docs/source/example-code/build.gradle @@ -99,6 +99,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { adminAddress "localhost:10013" } webPort 10004 + extraConfig = ['h2Settings.address' : 'localhost:10014'] cordapps = [] } node { @@ -109,6 +110,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/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt index 05958da0da..475ae02e4a 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 @@ -52,7 +52,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 48cda189f1..b48bd94963 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 @@ -46,7 +46,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/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index 4b50e62a87..a98cd6c077 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.". 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 c9847f5345..3e761393e1 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. .. _standalone_database_config_examples_ref: diff --git a/experimental/flow-worker/src/main/kotlin/net/corda/flowworker/FlowWorkerServiceHub.kt b/experimental/flow-worker/src/main/kotlin/net/corda/flowworker/FlowWorkerServiceHub.kt index 13da729582..edc4a1cdd2 100644 --- a/experimental/flow-worker/src/main/kotlin/net/corda/flowworker/FlowWorkerServiceHub.kt +++ b/experimental/flow-worker/src/main/kotlin/net/corda/flowworker/FlowWorkerServiceHub.kt @@ -242,7 +242,7 @@ class FlowWorkerServiceHub(override val configuration: NodeConfiguration, overri servicesForResolution.start(networkParameters) persistentNetworkMapCache.start(networkParameters.notaries) - database.hikariStart(configuration.dataSourceProperties, configuration.database, schemaService) + database.startHikariPool(configuration.dataSourceProperties, configuration.database, schemaService) identityService.start(trustRoot, listOf(myInfo.legalIdentitiesAndCerts.first().certificate, nodeCa)) database.transaction { 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 d173537418..3269a4def1 100644 --- a/node/src/integration-test/kotlin/net/corda/node/AddressBindingFailureTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/AddressBindingFailureTests.kt @@ -42,7 +42,7 @@ class AddressBindingFailureTests: IntegrationTest() { @Test fun `H2 address`() { assumeTrue(!IntegrationTest.isRemoteDatabaseMode()) // Enterprise only - disable test where running against remote database - assertBindExceptionForOverrides { address -> mapOf("h2Settings" to mapOf("address" to address.toString())) } + 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/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt index 2cbe11f96a..091ca80a27 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 @@ -202,7 +202,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 788d367754..77e144dcb4 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -230,7 +230,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 } @@ -254,10 +254,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() } @@ -347,12 +344,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) @@ -692,6 +689,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 } @@ -772,7 +772,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } val props = configuration.dataSourceProperties if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.") - database.hikariStart(props, configuration.database, schemaService) + database.startHikariPool(props, configuration.database, schemaService) // Now log the vendor string as this will also cause a connection to be tested eagerly. logVendorString(database, log) } @@ -941,7 +941,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 @@ -1038,7 +1038,7 @@ fun configureDatabase(hikariProperties: Properties, wellKnownPartyFromAnonymous: (AbstractParty) -> Party?, schemaService: SchemaService = NodeSchemaService()): CordaPersistence = createCordaPersistence(databaseConfig, wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous, schemaService) - .apply { hikariStart(hikariProperties, databaseConfig, schemaService) } + .apply { startHikariPool(hikariProperties, databaseConfig, schemaService) } fun createCordaPersistence(databaseConfig: DatabaseConfig, wellKnownPartyFromX500Name: (CordaX500Name) -> Party?, @@ -1053,7 +1053,7 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig, return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, attributeConverters) } -fun CordaPersistence.hikariStart(hikariProperties: Properties, databaseConfig: DatabaseConfig, schemaService: SchemaService) { +fun CordaPersistence.startHikariPool(hikariProperties: Properties, databaseConfig: DatabaseConfig, schemaService: SchemaService) { try { val dataSource = DataSourceFactory.createDataSource(hikariProperties) val jdbcUrl = hikariProperties.getProperty("dataSource.url", "") 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 e301a22d44..91371e1a7a 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -28,26 +28,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 @@ -85,11 +71,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> { @@ -206,7 +191,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/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 652f41e209..a3e30beb7f 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -64,6 +64,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 @@ -71,11 +72,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 {} @@ -358,13 +361,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/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index f6ff539cd4..d3fc126f31 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -39,11 +39,11 @@ 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.DatabaseMigrationException import net.corda.nodeapi.internal.persistence.oracleJdbcDriverSerialFilter 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 @@ -337,6 +337,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() @@ -352,7 +354,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/internal/cordapp/CordappProviderImpl.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt index 78ad7fbe9f..b5ee3bfaff 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 @@ -24,6 +24,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 @@ -99,7 +100,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 2f9bd1f63c..ac94064148 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 @@ -31,6 +31,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 @@ -115,6 +116,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/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt index f927fd181b..b8b807da47 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 @@ -80,9 +80,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 { 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 302246cd97..b30518a5c1 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 @@ -22,12 +22,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.* @@ -60,10 +56,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. @@ -238,10 +236,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)!! @@ -268,6 +265,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) } @@ -292,7 +301,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") @@ -305,10 +320,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/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 76520eaf88..62b2160981 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 @@ -95,10 +95,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) } } } @@ -541,10 +541,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) } } } @@ -553,14 +553,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 } } 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/persistence/NodeAttachmentStorageTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt similarity index 78% 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 81afefc16d..86390a05f4 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 @@ -10,26 +10,34 @@ 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 @@ -40,7 +48,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 @@ -195,7 +203,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) } @@ -208,7 +216,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) } @@ -243,23 +251,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/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 632f399c4f..23a27ad96d 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 @@ -63,47 +63,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() } @@ -484,45 +478,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`() { 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 04889c3ab9..c75527f462 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 @@ -12,12 +12,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.* @@ -37,6 +39,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 @@ -125,7 +128,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 @@ -263,6 +267,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 { diff --git a/samples/attachment-demo/build.gradle b/samples/attachment-demo/build.gradle index a1de8648b7..67d17faa05 100644 --- a/samples/attachment-demo/build.gradle +++ b/samples/attachment-demo/build.gradle @@ -63,6 +63,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" @@ -73,6 +74,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" @@ -84,6 +86,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 bcd0b9d397..dece57396b 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 @@ -54,10 +54,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") @@ -77,6 +79,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 7374e50db1..6ec41fe419 100644 --- a/samples/cordapp-configuration/build.gradle +++ b/samples/cordapp-configuration/build.gradle @@ -32,10 +32,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 @@ -46,10 +47,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 @@ -60,5 +62,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 23319f6ea5..cdaf961058 100644 --- a/samples/irs-demo/cordapp/build.gradle +++ b/samples/irs-demo/cordapp/build.gradle @@ -80,6 +80,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" @@ -91,6 +92,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" @@ -102,6 +104,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" @@ -114,6 +117,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 f289e8e5c7..3ae92d1c0a 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 @@ -45,6 +45,7 @@ class BFTNotaryCordform : CordformDefinition() { } rpcUsers(notaryDemoUser) devMode(true) + extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0")) } node { name(BOB_NAME) @@ -54,11 +55,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 ce27373833..98df126ffb 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 @@ -33,6 +33,7 @@ class CustomNotaryCordform : CordformDefinition() { } rpcUsers(notaryDemoUser) devMode(true) + extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0")) } node { name(BOB_NAME) @@ -42,6 +43,7 @@ class CustomNotaryCordform : CordformDefinition() { adminAddress("localhost:10106") } devMode(true) + extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0")) } node { name(DUMMY_NOTARY_NAME) @@ -52,6 +54,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 ee598290f5..793ae6cc07 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 @@ -45,6 +45,7 @@ class RaftNotaryCordform : CordformDefinition() { } rpcUsers(notaryDemoUser) devMode(true) + extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0")) } node { name(BOB_NAME) @@ -54,11 +55,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 2f09466812..d8298966a6 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 @@ -39,6 +39,7 @@ class SingleNotaryCordform : CordformDefinition() { } rpcUsers(notaryDemoUser) devMode(true) + extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0")) } node { name(BOB_NAME) @@ -48,6 +49,7 @@ class SingleNotaryCordform : CordformDefinition() { adminAddress("localhost:10106") } devMode(true) + extraConfig = mapOf("h2Settings" to mapOf("address" to "localhost:0")) } node { name(DUMMY_NOTARY_NAME) @@ -58,6 +60,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 be55e7a211..29bae9965d 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -104,7 +104,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { extraConfig = [ custom: [ jvmArgs: ["-Xmx1g"] - ] + ], + 'h2Settings.address' : 'localhost:10038' ] } node { @@ -122,7 +123,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { extraConfig = [ custom: [ jvmArgs: ["-Xmx1g"] - ] + ], + 'h2Settings.address' : 'localhost:10039' ] } node { @@ -140,7 +142,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { extraConfig = [ custom: [ jvmArgs: ["-Xmx1g"] - ] + ], + 'h2Settings.address' : 'localhost:10040' ] } node { @@ -158,7 +161,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 01a04d9464..b36024c4dc 100644 --- a/samples/trader-demo/build.gradle +++ b/samples/trader-demo/build.gradle @@ -64,6 +64,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 { @@ -75,6 +76,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" @@ -85,6 +87,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" @@ -95,6 +98,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { address "localhost:10012" adminAddress "localhost:10013" } + extraConfig = ['h2Settings.address' : 'localhost:10017'] } } 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 3dca5276b9..9407f5b076 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 @@ -138,6 +138,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. */ @@ -421,7 +433,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))) } 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 229003b468..6b910619eb 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 @@ -157,7 +157,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. @@ -347,7 +347,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( @@ -562,6 +562,8 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe fun waitQuiescent() { busyLatch.await() } + + override fun close() = stopNodes() } abstract class MessagingServiceSpy { 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 diff --git a/testing/test-utils/build.gradle b/testing/test-utils/build.gradle index 3dbc48c9b6..41113fd0bc 100644 --- a/testing/test-utils/build.gradle +++ b/testing/test-utils/build.gradle @@ -34,6 +34,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 69114d58dd..c9fe66ec00 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 @@ -35,6 +35,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 /** @@ -118,6 +119,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/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index 44480b3a2b..04a4397a42 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 @@ -298,7 +298,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 33f1abc9ba..787ea98899 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 @@ -13,12 +13,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 @@ -60,7 +59,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()) } } } 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