From 469ffe473fe480b631b0e5ed8da1fd24ac210e89 Mon Sep 17 00:00:00 2001 From: Tudor Malene Date: Wed, 24 Oct 2018 17:36:14 +0100 Subject: [PATCH 01/24] Fix merge Fix merge Fix merge --- .../core/transactions/LedgerTransaction.kt | 26 ++----------------- .../core/transactions/TransactionBuilder.kt | 6 ++--- .../cordapp/JarScanningCordappLoader.kt | 1 + .../node/services/vault/VaultQueryTests.kt | 19 +++++++------- .../kotlin/net/corda/testing/dsl/TestDSL.kt | 4 --- .../testing/internal/vault/VaultFiller.kt | 14 +++++----- 6 files changed, 23 insertions(+), 47 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 880f78b0b2..d0ee49a524 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -10,6 +10,7 @@ import net.corda.core.internal.castIfPossible import net.corda.core.internal.uncheckedCast import net.corda.core.node.NetworkParameters import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.Try import net.corda.core.utilities.loggerFor import java.util.* import java.util.function.Predicate @@ -56,6 +57,7 @@ data class LedgerTransaction @JvmOverloads constructor( } private companion object { + val logger = loggerFor() private fun contractClassFor(className: ContractClassName, classLoader: ClassLoader?): Try> { return Try.on { (classLoader ?: this::class.java.classLoader) @@ -141,30 +143,6 @@ data class LedgerTransaction @JvmOverloads constructor( } } - /** - * Verify that for each contract the network wide package owner is respected. - * - * TODO - revisit once transaction contains network parameters. - */ - private fun validatePackageOwnership(contractAttachmentsByContract: Map) { - // This should never happen once we have network parameters in the transaction. - if (networkParameters == null) { - return - } - - val contractsAndOwners = allStates.mapNotNull { transactionState -> - val contractClassName = transactionState.contract - networkParameters.getOwnerOf(contractClassName)?.let { contractClassName to it } - }.toMap() - - contractsAndOwners.forEach { contract, owner -> - val attachment = contractAttachmentsByContract[contract]!! - if (!owner.isFulfilledBy(attachment.signers)) { - throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(this.id, id, contract) - } - } - } - /** * Enforces the validity of the actual constraints. * * Constraints should be one of the valid supported ones. 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 8475afaf52..5f747f2df3 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -181,7 +181,7 @@ open class TransactionBuilder @JvmOverloads constructor( val resolvedStates: List> = contractAttachmentsAndResolvedOutputStates.mapNotNull { it.second }.flatten() // The output states need to preserve the order in which they were added. - val resolvedOutputStatesInTheOriginalOrder: List> = outputStates().map { os -> resolvedStates.find { rs -> rs.data == os.data }!! } + val resolvedOutputStatesInTheOriginalOrder: List> = outputStates().map { os -> resolvedStates.find { rs -> rs.data == os.data && rs.encumbrance == os.encumbrance}!! } val attachments: Collection = contractAttachmentsAndResolvedOutputStates.map { it.first } + refStateContractAttachments @@ -454,7 +454,7 @@ open class TransactionBuilder @JvmOverloads constructor( state: ContractState, contract: ContractClassName = requireNotNull(state.requiredContractClassName) { //TODO: add link to docsite page, when there is one. -""" + """ Unable to infer Contract class name because state class ${state::class.java.name} is not annotated with @BelongsToContract, and does not have an enclosing class which implements Contract. Either annotate ${state::class.java.name} with @BelongsToContract, or supply an explicit contract parameter to addOutputState(). @@ -470,7 +470,7 @@ with @BelongsToContract, or supply an explicit contract parameter to addOutputSt state: ContractState, contract: ContractClassName = requireNotNull(state.requiredContractClassName) { //TODO: add link to docsite page, when there is one. -""" + """ Unable to infer Contract class name because state class ${state::class.java.name} is not annotated with @BelongsToContract, and does not have an enclosing class which implements Contract. Either annotate ${state::class.java.name} with @BelongsToContract, or supply an explicit contract parameter to addOutputState(). diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt index 3827957bec..3fbe766a6e 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt @@ -2,6 +2,7 @@ package net.corda.node.internal.cordapp import io.github.classgraph.ClassGraph import io.github.classgraph.ScanResult +import net.corda.core.contracts.warnContractWithoutConstraintPropagation import net.corda.core.cordapp.Cordapp import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 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 7f55504ee9..70dbfe79fe 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 @@ -7,9 +7,9 @@ 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.ConstraintInfo.Type.* import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.* -import net.corda.core.node.services.Vault.ConstraintInfo.Type.* import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.* @@ -481,7 +481,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { vaultFiller.fillWithSomeTestLinearStates(1, constraint = AlwaysAcceptAttachmentConstraint).states.first().state.constraint vaultFiller.fillWithSomeTestLinearStates(1, constraint = WhitelistedByZoneAttachmentConstraint).states.first().state.constraint // hash constraint - val linearStateHash = vaultFiller.fillWithSomeTestLinearStates(1, constraint = HashAttachmentConstraint(SecureHash.randomSHA256())) + val linearStateHash = vaultFiller.fillWithSomeTestLinearStates(1, constraint = AutomaticPlaceholderConstraint) // defaults to the HashConstraint val constraintHash = linearStateHash.states.first().state.constraint as HashAttachmentConstraint // signature constraint (single key) val linearStateSignature = vaultFiller.fillWithSomeTestLinearStates(1, constraint = SignatureAttachmentConstraint(alice.publicKey)) @@ -504,7 +504,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { val constraintTypeCriteria2 = VaultQueryCriteria(constraintTypes = setOf(HASH)) val constraintResults2 = vaultService.queryBy(constraintTypeCriteria2) assertThat(constraintResults2.states).hasSize(2) - assertThat(constraintResults2.states.map { it.state.constraint }).containsOnlyOnce(constraintHash) + assertThat(constraintResults2.states.map { it.state.constraint }.toSet()).isEqualTo(setOf(constraintHash)) // search for states with [Vault.ConstraintInfo.Type] either HASH or CZ_WHITELISED // DOCSTART VaultQueryExample30 @@ -536,7 +536,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { val alwaysAcceptConstraint = vaultFiller.fillWithSomeTestLinearStates(1, constraint = AlwaysAcceptAttachmentConstraint).states.first().state.constraint vaultFiller.fillWithSomeTestLinearStates(1, constraint = WhitelistedByZoneAttachmentConstraint) // hash constraint - val linearStateHash = vaultFiller.fillWithSomeTestLinearStates(1, constraint = HashAttachmentConstraint(SecureHash.randomSHA256())) + val linearStateHash = vaultFiller.fillWithSomeTestLinearStates(1, constraint = AutomaticPlaceholderConstraint) // defaults to the hash constraint. val constraintHash = linearStateHash.states.first().state.constraint as HashAttachmentConstraint // signature constraint (single key) val linearStateSignature = vaultFiller.fillWithSomeTestLinearStates(1, constraint = SignatureAttachmentConstraint(alice.publicKey)) @@ -559,7 +559,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { // search for states for a specific HashAttachmentConstraint val constraintsCriteria2 = VaultQueryCriteria(constraints = setOf(Vault.ConstraintInfo(constraintHash))) val constraintResults2 = vaultService.queryBy(constraintsCriteria2) - assertThat(constraintResults2.states).hasSize(1) + assertThat(constraintResults2.states).hasSize(2) assertThat(constraintResults2.states.first().state.constraint).isEqualTo(constraintHash) // search for states with a specific SignatureAttachmentConstraint constraint @@ -574,7 +574,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { Vault.ConstraintInfo(constraintSignatureCompositeKey), Vault.ConstraintInfo(constraintHash))) val constraintResults = vaultService.queryBy(constraintCriteria) // DOCEND VaultQueryExample31 - assertThat(constraintResults.states).hasSize(3) + assertThat(constraintResults.states).hasSize(4) assertThat(constraintResults.states.map { it.state.constraint }).containsAll(listOf(constraintHash, constraintSignature, constraintSignatureCompositeKey)) // exercise enriched query @@ -1474,8 +1474,9 @@ abstract class VaultQueryTestsBase : VaultQueryParties { vaultFiller.fillWithSomeTestLinearStates(1, linearNumber = it.toLong(), linearString = it.toString()) } val max = builder { DummyLinearStateSchemaV1.PersistentDummyLinearState::linearTimestamp.max( - groupByColumns = listOf(DummyLinearStateSchemaV1.PersistentDummyLinearState::linearNumber) - ) + groupByColumns = listOf(DummyLinearStateSchemaV1.PersistentDummyLinearState::linearNumber), + orderBy = Sort.Direction.ASC + ) } val maxCriteria = VaultCustomQueryCriteria(max) val pageSpec = PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE) @@ -2336,7 +2337,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { database.transaction { vaultFiller.fillWithSomeTestLinearStates(1, constraint = WhitelistedByZoneAttachmentConstraint) vaultFiller.fillWithSomeTestLinearStates(1, constraint = SignatureAttachmentConstraint(alice.publicKey)) - vaultFiller.fillWithSomeTestLinearStates(1, constraint = HashAttachmentConstraint( SecureHash.randomSHA256())) + vaultFiller.fillWithSomeTestLinearStates(1, constraint = AutomaticPlaceholderConstraint) // this defaults to the HashConstraint vaultFiller.fillWithSomeTestLinearStates(1, constraint = AlwaysAcceptAttachmentConstraint) // Base criteria 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 9cc8cff843..c98a026365 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 @@ -155,10 +155,6 @@ data class TestTransactionDSLInterpreter private constructor( attachment((services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage, attachmentId, signers)) } - override fun _attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List){ - attachment((services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage, attachmentId, signers)) - } - } data class TestLedgerDSLInterpreter private constructor( diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt index eb46690305..baba1b0cfe 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt @@ -103,7 +103,7 @@ class VaultFiller @JvmOverloads constructor( linearNumber: Long = 0L, linearBoolean: Boolean = false, linearTimestamp: Instant = now(), - constraint: AttachmentConstraint = AutomaticHashConstraint): Vault { + constraint: AttachmentConstraint = AutomaticPlaceholderConstraint): Vault { val myKey: PublicKey = services.myInfo.chooseIdentity().owningKey val me = AnonymousParty(myKey) val issuerKey = defaultNotary.keyPair @@ -134,12 +134,12 @@ class VaultFiller @JvmOverloads constructor( @JvmOverloads fun fillWithSomeTestLinearAndDealStates(numberToCreate: Int, - externalId: String? = null, - participants: List = emptyList(), - linearString: String = "", - linearNumber: Long = 0L, - linearBoolean: Boolean = false, - linearTimestamp: Instant = now()): Vault { + externalId: String? = null, + participants: List = emptyList(), + linearString: String = "", + linearNumber: Long = 0L, + linearBoolean: Boolean = false, + linearTimestamp: Instant = now()): Vault { val myKey: PublicKey = services.myInfo.chooseIdentity().owningKey val me = AnonymousParty(myKey) val issuerKey = defaultNotary.keyPair From 4c25250fc84ff9d5f3a08aa76160169d44e1094d Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Fri, 9 Nov 2018 12:45:43 +0000 Subject: [PATCH 02/24] [CORDA-2130] Encumbered states should always be assigned to the same notary (#4158) --- .../TransactionVerificationException.kt | 10 ++++ .../core/transactions/LedgerTransaction.kt | 36 ++++++++++++-- .../TransactionEncumbranceTests.kt | 47 +++++++++++++++++-- 3 files changed, 85 insertions(+), 8 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt index a6392a01a9..970c35ad9a 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt @@ -136,6 +136,16 @@ sealed class TransactionVerificationException(val txId: SecureHash, message: Str "is not satisfied. Encumbered states should also be referenced as an encumbrance of another state to form " + "a full cycle. Offending indices $nonMatching", null) + /** + * All encumbered states should be assigned to the same notary. This is due to the fact that multi-notary + * transactions are not supported and thus two encumbered states with different notaries cannot be consumed + * in the same transaction. + */ + @KeepForDJVM + class TransactionNotaryMismatchEncumbranceException(txId: SecureHash, encumberedIndex: Int, encumbranceIndex: Int, encumberedNotary: Party, encumbranceNotary: Party) + : TransactionVerificationException(txId, "Encumbered output states assigned to different notaries found. " + + "Output state with index $encumberedIndex is assigned to notary [$encumberedNotary], while its encumbrance with index $encumbranceIndex is assigned to notary [$encumbranceNotary]", null) + /** Whether the inputs or outputs list contains an encumbrance issue, see [TransactionMissingEncumbranceException]. */ @CordaSerializable @KeepForDJVM diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 64ccb45cbf..873615def6 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -14,6 +14,7 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.Try import java.util.* import java.util.function.Predicate +import kotlin.collections.HashSet /** * A LedgerTransaction is derived from a [WireTransaction]. It is the result of doing the following operations: @@ -232,10 +233,39 @@ data class LedgerTransaction @JvmOverloads constructor( // Check that in the outputs, // a) an encumbered state does not refer to itself as the encumbrance // b) the number of outputs can contain the encumbrance - // c) the bi-directionality (full cycle) property is satisfied. + // c) the bi-directionality (full cycle) property is satisfied + // d) encumbered output states are assigned to the same notary. val statesAndEncumbrance = outputs.withIndex().filter { it.value.encumbrance != null }.map { Pair(it.index, it.value.encumbrance!!) } if (!statesAndEncumbrance.isEmpty()) { - checkOutputEncumbrances(statesAndEncumbrance) + checkBidirectionalOutputEncumbrances(statesAndEncumbrance) + checkNotariesOutputEncumbrance(statesAndEncumbrance) + } + } + + // Method to check if all encumbered states are assigned to the same notary Party. + // This method should be invoked after [checkBidirectionalOutputEncumbrances], because it assumes that the + // bi-directionality property is already satisfied. + private fun checkNotariesOutputEncumbrance(statesAndEncumbrance: List>) { + // We only check for transactions in which notary is null (i.e., issuing transactions). + // Note that if a notary is defined for a transaction, we already check if all outputs are assigned + // to the same notary (transaction's notary) in [checkNoNotaryChange()]. + if (notary == null) { + // indicesAlreadyChecked is used to bypass already checked indices and to avoid cycles. + val indicesAlreadyChecked = HashSet() + statesAndEncumbrance.forEach { + checkNotary(it.first, indicesAlreadyChecked) + } + } + } + + private tailrec fun checkNotary(index: Int, indicesAlreadyChecked: HashSet) { + if (indicesAlreadyChecked.add(index)) { + val encumbranceIndex = outputs[index].encumbrance!! + if (outputs[index].notary != outputs[encumbranceIndex].notary) { + throw TransactionVerificationException.TransactionNotaryMismatchEncumbranceException(id, index, encumbranceIndex, outputs[index].notary, outputs[encumbranceIndex].notary) + } else { + checkNotary(encumbranceIndex, indicesAlreadyChecked) + } } } @@ -273,7 +303,7 @@ data class LedgerTransaction @JvmOverloads constructor( // b -> c and c -> b // c -> a b -> a // and form a full cycle, meaning that the bi-directionality property is satisfied. - private fun checkOutputEncumbrances(statesAndEncumbrance: List>) { + private fun checkBidirectionalOutputEncumbrances(statesAndEncumbrance: List>) { // [Set] of "from" (encumbered states). val encumberedSet = mutableSetOf() // [Set] of "to" (encumbrance states). diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt index 6cbd8e3483..a48c962ebe 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt @@ -2,10 +2,7 @@ package net.corda.core.transactions import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever -import net.corda.core.contracts.Contract -import net.corda.core.contracts.ContractState -import net.corda.core.contracts.TransactionVerificationException -import net.corda.core.contracts.requireThat +import net.corda.core.contracts.* import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.finance.DOLLARS @@ -18,6 +15,7 @@ import net.corda.testing.core.TestIdentity import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices import net.corda.testing.node.ledger +import org.assertj.core.api.AssertionsForClassTypes import org.junit.Rule import org.junit.Test import java.time.Instant @@ -33,6 +31,7 @@ class TransactionEncumbranceTests { private companion object { val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party + val DUMMY_NOTARY2 = TestIdentity(DUMMY_NOTARY_NAME.copy(organisation = "${DUMMY_NOTARY_NAME.organisation}2"), 30).party val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).party val MEGA_CORP get() = megaCorp.party @@ -77,7 +76,7 @@ class TransactionEncumbranceTests { } @Test - fun `states can be bi-directionally encumbered`() { + fun `states must be bi-directionally encumbered`() { // Basic encumbrance example for encumbrance index links 0 -> 1 and 1 -> 0 ledgerServices.ledger(DUMMY_NOTARY) { transaction { @@ -316,4 +315,42 @@ class TransactionEncumbranceTests { } } } + + @Test + fun `encumbered states cannot be assigned to different notaries`() { + // Single encumbrance with different notaries. + assertFailsWith { + TransactionBuilder() + .addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint) + .addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 0, AutomaticHashConstraint) + .addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey) + .toLedgerTransaction(ledgerServices) + } + + // More complex encumbrance (full cycle of size 4) where one of the encumbered states is assigned to a different notary. + // 0 -> 1, 1 -> 3, 3 -> 2, 2 -> 0 + // We expect that state at index 3 cannot be encumbered with the state at index 2, due to mismatched notaries. + AssertionsForClassTypes.assertThatExceptionOfType(TransactionVerificationException.TransactionNotaryMismatchEncumbranceException::class.java).isThrownBy { + TransactionBuilder() + .addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint) + .addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 3, AutomaticHashConstraint) + .addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 0, AutomaticHashConstraint) + .addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 2, AutomaticHashConstraint) + .addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey) + .toLedgerTransaction(ledgerServices) + }.withMessageContaining("index 3 is assigned to notary [O=Notary Service, L=Zurich, C=CH], while its encumbrance with index 2 is assigned to notary [O=Notary Service2, L=Zurich, C=CH]") + + // Two different encumbrance chains, where only one fails due to mismatched notary. + // 0 -> 1, 1 -> 0, 2 -> 3, 3 -> 2 where encumbered states with indices 2 and 3, respectively, are assigned + // to different notaries. + AssertionsForClassTypes.assertThatExceptionOfType(TransactionVerificationException.TransactionNotaryMismatchEncumbranceException::class.java).isThrownBy { + TransactionBuilder() + .addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint) + .addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 0, AutomaticHashConstraint) + .addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 3, AutomaticHashConstraint) + .addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 2, AutomaticHashConstraint) + .addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey) + .toLedgerTransaction(ledgerServices) + }.withMessageContaining("index 2 is assigned to notary [O=Notary Service, L=Zurich, C=CH], while its encumbrance with index 3 is assigned to notary [O=Notary Service2, L=Zurich, C=CH]") + } } From 2f644039b511bfc7f0d3e3931fb671b365415635 Mon Sep 17 00:00:00 2001 From: Michal Kit Date: Fri, 9 Nov 2018 13:45:23 +0000 Subject: [PATCH 03/24] CORDA-2205 Fixing CRL issuer store lookup (#4205) --- .../utilities/registration/NetworkRegistrationHelper.kt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt index 06b429b194..6c636d9e74 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt @@ -347,12 +347,7 @@ class NodeRegistrationHelper( if (principalMatchesCertificatePrincipal(tlsCertCrlIssuer, rootCert)) { return rootCert } - val trustStore = config.p2pSslOptions.trustStore.getOptional() - return if (trustStore != null) { - findMatchingCertificate(tlsCertCrlIssuer, trustStore.value) - } else { - null - } + return findMatchingCertificate(tlsCertCrlIssuer, rootTrustStore) } override fun isTlsCrlIssuerCertRequired(): Boolean { From 336185de23eed9eb257bca1308fba80acf93235b Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Fri, 9 Nov 2018 14:00:40 +0000 Subject: [PATCH 04/24] CORDA-2190: Improve notary service flow exception handling (#4200) * CORDA-2190: Improve notary service flow exception handling - Refactored notary flows to reduce validation code duplication - Improved notarisation error handling to provide more helpful responses to the client --- .../net/corda/core/flows/NotaryError.kt | 1 + .../core/internal/notary/NotaryServiceFlow.kt | 97 ++++++++++++------- .../corda/core/internal/notary/NotaryUtils.kt | 37 +++---- .../notary/SinglePartyNotaryService.kt | 1 + .../core/transactions/SignedTransaction.kt | 7 +- .../transactions/NonValidatingNotaryFlow.kt | 37 +++---- .../transactions/ValidatingNotaryFlow.kt | 33 +++---- .../net/corda/node/services/TimedFlowTests.kt | 10 +- .../corda/notarydemo/MyCustomNotaryService.kt | 41 +++----- 9 files changed, 123 insertions(+), 141 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryError.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryError.kt index 91a8075b6d..6a41d8f80e 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryError.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryError.kt @@ -49,6 +49,7 @@ sealed class NotaryError { } /** Occurs when the transaction sent for notarisation is assigned to a different notary identity. */ + @Deprecated("Deprecated since platform version 4. This object is no longer used, [TransactionInvalid] will be reported in case of notary mismatch") object WrongNotary : NotaryError() /** Occurs when the notarisation request signature does not verify for the provided transaction. */ diff --git a/core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt index bcd2e8d0a4..701846fad1 100644 --- a/core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt @@ -23,61 +23,84 @@ abstract class NotaryServiceFlow(val otherSideSession: FlowSession, val service: private const val maxAllowedInputsAndReferences = 10_000 } + private var transactionId: SecureHash? = null + @Suspendable override fun call(): Void? { check(serviceHub.myInfo.legalIdentities.any { serviceHub.networkMapCache.isNotary(it) }) { "We are not a notary on the network" } val requestPayload = otherSideSession.receive().unwrap { it } - var txId: SecureHash? = null + try { - val parts = validateRequest(requestPayload) - txId = parts.id - checkNotary(parts.notary) + val tx: TransactionParts = validateRequest(requestPayload) + val request = NotarisationRequest(tx.inputs, tx.id) + validateRequestSignature(request, requestPayload.requestSignature) + + verifyTransaction(requestPayload) + service.commitInputStates( - parts.inputs, - txId, + tx.inputs, + tx.id, otherSideSession.counterparty, requestPayload.requestSignature, - parts.timestamp, - parts.references - ) - signTransactionAndSendResponse(txId) + tx.timeWindow, + tx.references) + } catch (e: NotaryInternalException) { - throw NotaryException(e.error, txId) + logError(e.error) + // Any exception that's not a NotaryInternalException is assumed to be an unexpected internal error + // that is not relayed back to the client. + throw NotaryException(e.error, transactionId) } + + signTransactionAndSendResponse(transactionId!!) return null } - /** Checks whether the number of input states is too large. */ - protected fun checkInputs(inputs: List) { - if (inputs.size > maxAllowedInputsAndReferences) { - val error = NotaryError.TransactionInvalid( - IllegalArgumentException("A transaction cannot have more than $maxAllowedInputsAndReferences " + - "inputs or references, received: ${inputs.size}") - ) + private fun validateRequest(requestPayload: NotarisationPayload): TransactionParts { + try { + val transaction = extractParts(requestPayload) + transactionId = transaction.id + checkNotary(transaction.notary) + checkInputs(transaction.inputs + transaction.references) + return transaction + } catch (e: Exception) { + val error = NotaryError.TransactionInvalid(e) throw NotaryInternalException(error) } } - /** - * Implement custom logic to perform transaction verification based on validity and privacy requirements. - */ + /** Extract the common transaction components required for notarisation. */ + protected abstract fun extractParts(requestPayload: NotarisationPayload): TransactionParts + + /** Check if transaction is intended to be signed by this notary. */ @Suspendable - protected abstract fun validateRequest(requestPayload: NotarisationPayload): TransactionParts + private fun checkNotary(notary: Party?) { + require(notary?.owningKey == service.notaryIdentityKey) { + "The notary specified on the transaction: [$notary] does not match the notary service's identity: [${service.notaryIdentityKey}] " + } + } + + /** Checks whether the number of input states is too large. */ + private fun checkInputs(inputs: List) { + require(inputs.size < maxAllowedInputsAndReferences) { + "A transaction cannot have more than $maxAllowedInputsAndReferences " + + "inputs or references, received: ${inputs.size}" + } + } /** Verifies that the correct notarisation request was signed by the counterparty. */ - protected fun validateRequestSignature(request: NotarisationRequest, signature: NotarisationRequestSignature) { + private fun validateRequestSignature(request: NotarisationRequest, signature: NotarisationRequestSignature) { val requestingParty = otherSideSession.counterparty request.verifySignature(signature, requestingParty) } - /** Check if transaction is intended to be signed by this notary. */ + /** + * Override to implement custom logic to perform transaction verification based on validity and privacy requirements. + */ @Suspendable - protected fun checkNotary(notary: Party?) { - if (notary?.owningKey != service.notaryIdentityKey) { - throw NotaryInternalException(NotaryError.WrongNotary) - } + protected open fun verifyTransaction(requestPayload: NotarisationPayload) { } @Suspendable @@ -90,15 +113,23 @@ abstract class NotaryServiceFlow(val otherSideSession: FlowSession, val service: * The minimum amount of information needed to notarise a transaction. Note that this does not include * any sensitive transaction details. */ - protected data class TransactionParts @JvmOverloads constructor( + protected data class TransactionParts( val id: SecureHash, val inputs: List, - val timestamp: TimeWindow?, + val timeWindow: TimeWindow?, val notary: Party?, val references: List = emptyList() - ) { - fun copy(id: SecureHash, inputs: List, timestamp: TimeWindow?, notary: Party?): TransactionParts { - return TransactionParts(id, inputs, timestamp, notary, references) + ) + + private fun logError(error: NotaryError) { + val errorCause = when (error) { + is NotaryError.RequestSignatureInvalid -> error.cause + is NotaryError.TransactionInvalid -> error.cause + is NotaryError.General -> error.cause + else -> null + } + if (errorCause != null) { + logger.error("Error notarising transaction $transactionId", errorCause) } } } diff --git a/core/src/main/kotlin/net/corda/core/internal/notary/NotaryUtils.kt b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryUtils.kt index 008e2efe60..f4b80e4427 100644 --- a/core/src/main/kotlin/net/corda/core/internal/notary/NotaryUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryUtils.kt @@ -2,42 +2,31 @@ package net.corda.core.internal.notary import net.corda.core.contracts.StateRef import net.corda.core.contracts.TimeWindow -import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash import net.corda.core.crypto.isFulfilledBy import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.node.ServiceHub import net.corda.core.serialization.serialize -import java.security.InvalidKeyException -import java.security.SignatureException +import net.corda.core.utilities.toBase58String import java.time.Instant /** Verifies the signature against this notarisation request. Checks that the signature is issued by the right party. */ fun NotarisationRequest.verifySignature(requestSignature: NotarisationRequestSignature, intendedSigner: Party) { - val signature = requestSignature.digitalSignature - if (intendedSigner.owningKey != signature.by) { - val errorMessage = "Expected a signature by ${intendedSigner.owningKey}, but received by ${signature.by}}" - throw NotaryInternalException(NotaryError.RequestSignatureInvalid(IllegalArgumentException(errorMessage))) - } - // TODO: if requestSignature was generated over an old version of NotarisationRequest, we need to be able to - // reserialize it in that version to get the exact same bytes. Modify the serialization logic once that's - // available. - val expectedSignedBytes = this.serialize().bytes - verifyCorrectBytesSigned(signature, expectedSignedBytes) -} - -private fun verifyCorrectBytesSigned(signature: DigitalSignature.WithKey, bytes: ByteArray) { try { - signature.verify(bytes) - } catch (e: Exception) { - when (e) { - is InvalidKeyException, is SignatureException -> { - val error = NotaryError.RequestSignatureInvalid(e) - throw NotaryInternalException(error) - } - else -> throw e + val signature = requestSignature.digitalSignature + require(intendedSigner.owningKey == signature.by) { + "Expected a signature by ${intendedSigner.owningKey.toBase58String()}, but received by ${signature.by.toBase58String()}}" } + + // TODO: if requestSignature was generated over an old version of NotarisationRequest, we need to be able to + // reserialize it in that version to get the exact same bytes. Modify the serialization logic once that's + // available. + val expectedSignedBytes = this.serialize().bytes + signature.verify(expectedSignedBytes) + } catch (e: Exception) { + val error = NotaryError.RequestSignatureInvalid(e) + throw NotaryInternalException(error) } } diff --git a/core/src/main/kotlin/net/corda/core/internal/notary/SinglePartyNotaryService.kt b/core/src/main/kotlin/net/corda/core/internal/notary/SinglePartyNotaryService.kt index 56fe34a7ff..6239f03824 100644 --- a/core/src/main/kotlin/net/corda/core/internal/notary/SinglePartyNotaryService.kt +++ b/core/src/main/kotlin/net/corda/core/internal/notary/SinglePartyNotaryService.kt @@ -53,6 +53,7 @@ abstract class SinglePartyNotaryService : NotaryService() { references ) ) + if (result is UniquenessProvider.Result.Failure) { throw NotaryInternalException(result.error) } diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt index 601efcd8cf..1216477c91 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -265,8 +265,11 @@ data class SignedTransaction(val txBits: SerializedBytes, override fun toString(): String = "${javaClass.simpleName}(id=$id)" private companion object { - private fun missingSignatureMsg(missing: Set, descriptions: List, id: SecureHash): String = - "Missing signatures for $descriptions on transaction ${id.prefixChars()} for ${missing.joinToString()}" + private fun missingSignatureMsg(missing: Set, descriptions: List, id: SecureHash): String { + return "Missing signatures on transaction ${id.prefixChars()} for " + + "keys: ${missing.joinToString { it.toStringShort() }}, " + + "by signers: ${descriptions.joinToString()} " + } } @KeepForDJVM diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt index a1ce69da9d..fa05c15755 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt @@ -1,38 +1,25 @@ package net.corda.node.services.transactions -import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.ComponentGroupEnum import net.corda.core.flows.FlowSession import net.corda.core.flows.NotarisationPayload -import net.corda.core.flows.NotarisationRequest -import net.corda.core.internal.notary.SinglePartyNotaryService import net.corda.core.internal.notary.NotaryServiceFlow +import net.corda.core.internal.notary.SinglePartyNotaryService import net.corda.core.transactions.ContractUpgradeFilteredTransaction -import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.FilteredTransaction import net.corda.core.transactions.NotaryChangeWireTransaction +/** + * The received transaction is not checked for contract-validity, as that would require fully + * resolving it into a [TransactionForVerification], for which the caller would have to reveal the whole transaction + * history chain. + * As a result, the Notary _will commit invalid transactions_ as well, but as it also records the identity of + * the caller, it is possible to raise a dispute and verify the validity of the transaction and subsequently + * undo the commit of the input states (the exact mechanism still needs to be worked out). + */ class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: SinglePartyNotaryService) : NotaryServiceFlow(otherSideSession, service) { - /** - * The received transaction is not checked for contract-validity, as that would require fully - * resolving it into a [TransactionForVerification], for which the caller would have to reveal the whole transaction - * history chain. - * As a result, the Notary _will commit invalid transactions_ as well, but as it also records the identity of - * the caller, it is possible to raise a dispute and verify the validity of the transaction and subsequently - * undo the commit of the input states (the exact mechanism still needs to be worked out). - */ - @Suspendable - override fun validateRequest(requestPayload: NotarisationPayload): TransactionParts { - val transaction = requestPayload.coreTransaction - checkInputs(transaction.inputs + transaction.references) - val request = NotarisationRequest(transaction.inputs, transaction.id) - validateRequestSignature(request, requestPayload.requestSignature) - val parts = extractParts(transaction) - checkNotary(parts.notary) - return parts - } - - private fun extractParts(tx: CoreTransaction): TransactionParts { + override fun extractParts(requestPayload: NotarisationPayload): TransactionParts { + val tx = requestPayload.coreTransaction return when (tx) { is FilteredTransaction -> { tx.apply { @@ -43,7 +30,7 @@ class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: SinglePart } TransactionParts(tx.id, tx.inputs, tx.timeWindow, tx.notary, tx.references) } - is ContractUpgradeFilteredTransaction -> TransactionParts(tx.id, tx.inputs, null, tx.notary) + is ContractUpgradeFilteredTransaction, is NotaryChangeWireTransaction -> TransactionParts(tx.id, tx.inputs, null, tx.notary) else -> { throw IllegalArgumentException("Received unexpected transaction type: ${tx::class.java.simpleName}," + diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt index e4b470d0a4..94019849db 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt @@ -2,19 +2,16 @@ package net.corda.node.services.transactions import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.TimeWindow -import net.corda.core.contracts.TransactionVerificationException import net.corda.core.flows.FlowSession import net.corda.core.flows.NotarisationPayload -import net.corda.core.flows.NotarisationRequest import net.corda.core.flows.NotaryError import net.corda.core.internal.ResolveTransactionsFlow -import net.corda.core.internal.notary.SinglePartyNotaryService import net.corda.core.internal.notary.NotaryInternalException import net.corda.core.internal.notary.NotaryServiceFlow +import net.corda.core.internal.notary.SinglePartyNotaryService import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionWithSignatures import net.corda.core.transactions.WireTransaction -import java.security.SignatureException /** * A notary commit flow that makes sure a given transaction is valid before committing it. This does mean that the calling @@ -22,29 +19,25 @@ import java.security.SignatureException * has its input states "blocked" by a transaction from another party, and needs to establish whether that transaction was * indeed valid. */ -class ValidatingNotaryFlow(otherSideSession: FlowSession, service: SinglePartyNotaryService) : NotaryServiceFlow(otherSideSession, service) { +open class ValidatingNotaryFlow(otherSideSession: FlowSession, service: SinglePartyNotaryService) : NotaryServiceFlow(otherSideSession, service) { + override fun extractParts(requestPayload: NotarisationPayload): TransactionParts { + val stx = requestPayload.signedTransaction + val timeWindow: TimeWindow? = if (stx.coreTransaction is WireTransaction) stx.tx.timeWindow else null + return TransactionParts(stx.id, stx.inputs, timeWindow, stx.notary, stx.references) + } + /** * Fully resolves the received transaction and its dependencies, runs contract verification logic and checks that * the transaction in question has all required signatures apart from the notary's. */ @Suspendable - override fun validateRequest(requestPayload: NotarisationPayload): TransactionParts { + override fun verifyTransaction(requestPayload: NotarisationPayload) { try { val stx = requestPayload.signedTransaction - checkInputs(stx.inputs + stx.references) - validateRequestSignature(NotarisationRequest(stx.inputs, stx.id), requestPayload.requestSignature) - val notary = stx.notary - checkNotary(notary) resolveAndContractVerify(stx) verifySignatures(stx) - val timeWindow: TimeWindow? = if (stx.coreTransaction is WireTransaction) stx.tx.timeWindow else null - return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!, stx.references) } catch (e: Exception) { - throw when (e) { - is TransactionVerificationException, - is SignatureException -> NotaryInternalException(NotaryError.TransactionInvalid(e)) - else -> e - } + throw NotaryInternalException(NotaryError.TransactionInvalid(e)) } } @@ -60,10 +53,6 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: SinglePartyNo } private fun checkSignatures(tx: TransactionWithSignatures) { - try { - tx.verifySignaturesExcept(service.notaryIdentityKey) - } catch (e: SignatureException) { - throw NotaryInternalException(NotaryError.TransactionInvalid(e)) - } + tx.verifySignaturesExcept(service.notaryIdentityKey) } } diff --git a/node/src/test/kotlin/net/corda/node/services/TimedFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/TimedFlowTests.kt index 4b633a35c6..8ff8900f8f 100644 --- a/node/src/test/kotlin/net/corda/node/services/TimedFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/TimedFlowTests.kt @@ -13,7 +13,6 @@ import net.corda.core.internal.FlowIORequest import net.corda.core.internal.ResolveTransactionsFlow import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.internal.concurrent.openFuture -import net.corda.core.internal.notary.NotaryServiceFlow import net.corda.core.internal.notary.SinglePartyNotaryService import net.corda.core.internal.notary.UniquenessProvider import net.corda.core.node.NotaryInfo @@ -22,6 +21,7 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.seconds import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.transactions.ValidatingNotaryFlow import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.testing.common.internal.testNetworkParameters @@ -174,20 +174,21 @@ class TimedFlowTests { override val uniquenessProvider = object : UniquenessProvider { /** A dummy commit method that immediately returns a success message. */ override fun commit(states: List, txId: SecureHash, callerIdentity: Party, requestSignature: NotarisationRequestSignature, timeWindow: TimeWindow?, references: List): CordaFuture { - return openFuture(). apply { + return openFuture().apply { set(UniquenessProvider.Result.Success) } } } + override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic = TestNotaryFlow(otherPartySession, this) override fun start() {} override fun stop() {} } /** A notary flow that will yield without returning a response on the very first received request. */ - private class TestNotaryFlow(otherSide: FlowSession, service: TestNotaryService) : NotaryServiceFlow(otherSide, service) { + private class TestNotaryFlow(otherSide: FlowSession, service: TestNotaryService) : ValidatingNotaryFlow(otherSide, service) { @Suspendable - override fun validateRequest(requestPayload: NotarisationPayload): TransactionParts { + override fun verifyTransaction(requestPayload: NotarisationPayload) { val myIdentity = serviceHub.myInfo.legalIdentities.first() MDC.put("name", myIdentity.name.toString()) logger.info("Received a request from ${otherSideSession.counterparty.name}") @@ -201,7 +202,6 @@ class TimedFlowTests { } else { logger.info("Processing") } - return TransactionParts(stx.id, stx.inputs, stx.tx.timeWindow, stx.notary) } } } diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt index 4af9f4fb3e..53a98be07a 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt @@ -1,20 +1,19 @@ package net.corda.notarydemo import co.paralleluniverse.fibers.Suspendable -import net.corda.core.contracts.TimeWindow -import net.corda.core.contracts.TransactionVerificationException -import net.corda.core.flows.* +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.NotarisationPayload +import net.corda.core.flows.NotaryError import net.corda.core.internal.ResolveTransactionsFlow import net.corda.core.internal.notary.NotaryInternalException -import net.corda.core.internal.notary.NotaryServiceFlow import net.corda.core.internal.notary.SinglePartyNotaryService import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionWithSignatures -import net.corda.core.transactions.WireTransaction import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.transactions.PersistentUniquenessProvider +import net.corda.node.services.transactions.ValidatingNotaryFlow import java.security.PublicKey -import java.security.SignatureException /** * A custom notary service should provide a constructor that accepts two parameters of types [ServiceHubInternal] and [PublicKey]. @@ -35,28 +34,15 @@ class MyCustomValidatingNotaryService(override val services: ServiceHubInternal, @Suppress("UNUSED_PARAMETER") // START 2 -class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidatingNotaryService) : NotaryServiceFlow(otherSide, service) { - /** - * The received transaction is checked for contract-validity, for which the caller also has to to reveal the whole - * transaction dependency chain. - */ - @Suspendable - override fun validateRequest(requestPayload: NotarisationPayload): TransactionParts { +class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidatingNotaryService) : ValidatingNotaryFlow(otherSide, service) { + override fun verifyTransaction(requestPayload: NotarisationPayload) { try { val stx = requestPayload.signedTransaction - validateRequestSignature(NotarisationRequest(stx.inputs, stx.id), requestPayload.requestSignature) - val notary = stx.notary - checkNotary(notary) - verifySignatures(stx) resolveAndContractVerify(stx) - val timeWindow: TimeWindow? = if (stx.coreTransaction is WireTransaction) stx.tx.timeWindow else null - return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!, stx.references) + verifySignatures(stx) + customVerify(stx) } catch (e: Exception) { - throw when (e) { - is TransactionVerificationException, - is SignatureException -> NotaryInternalException(NotaryError.TransactionInvalid(e)) - else -> e - } + throw NotaryInternalException(NotaryError.TransactionInvalid(e)) } } @@ -64,7 +50,6 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating private fun resolveAndContractVerify(stx: SignedTransaction) { subFlow(ResolveTransactionsFlow(stx, otherSideSession)) stx.verify(serviceHub, false) - customVerify(stx) } private fun verifySignatures(stx: SignedTransaction) { @@ -73,11 +58,7 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating } private fun checkSignatures(tx: TransactionWithSignatures) { - try { - tx.verifySignaturesExcept(service.notaryIdentityKey) - } catch (e: SignatureException) { - throw NotaryInternalException(NotaryError.TransactionInvalid(e)) - } + tx.verifySignaturesExcept(service.notaryIdentityKey) } private fun customVerify(stx: SignedTransaction) { From 2893271ab4c64c6c81e1a22669bdae05d8f0b492 Mon Sep 17 00:00:00 2001 From: Roger Willis Date: Fri, 9 Nov 2018 15:34:10 +0000 Subject: [PATCH 05/24] removed dupe companion object. (#4207) --- .../net/corda/core/transactions/TransactionBuilder.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 a3156d9bcf..159a62750d 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -80,10 +80,6 @@ open class TransactionBuilder @JvmOverloads constructor( return t } - companion object { - val logger = contextLogger() - } - // DOCSTART 1 /** A more convenient way to add items to this transaction that calls the add* methods for you based on type */ fun withItems(vararg items: Any): TransactionBuilder { @@ -265,7 +261,7 @@ open class TransactionBuilder @JvmOverloads constructor( addReferenceState(resolvedStateAndRef.referenced()) } } else { - logger.warn("WARNING: You must pass in a ServiceHub reference to TransactionBuilder to resolve " + + log.warn("WARNING: You must pass in a ServiceHub reference to TransactionBuilder to resolve " + "state pointers outside of flows. If you are writing a unit test then pass in a " + "MockServices instance.") return From 30d90c50d1b02d4fd43588b016b6a93bd14a8ab3 Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Fri, 9 Nov 2018 12:03:50 +0000 Subject: [PATCH 06/24] Add cmd file to kill corda jobs on windows --- buildSrc/win_scripts/kill_corda_procs.cmd | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 buildSrc/win_scripts/kill_corda_procs.cmd diff --git a/buildSrc/win_scripts/kill_corda_procs.cmd b/buildSrc/win_scripts/kill_corda_procs.cmd new file mode 100644 index 0000000000..352ed89666 --- /dev/null +++ b/buildSrc/win_scripts/kill_corda_procs.cmd @@ -0,0 +1,14 @@ +@echo off + +REM Setlocal EnableDelayedExpansion +FOR /F "tokens=1,2 delims= " %%G IN ('jps -l') DO (call :sub %%H %%G) +goto :eof + +:sub + +IF %1==net.corda.webserver.WebServer taskkill /F /PID %2 +IF %1==net.corda.node.Corda taskkill /F /PID %2 +IF %1==corda.jar taskkill /F /PID %2 +IF %1==corda-webserver.jar taskkill /F /PID %2 +IF %1==org.gradle.launcher.daemon.bootstrap.GradleDaemon taskkill /F /PID %2 +goto :eof From 65b8cbe9b1cb3d7f565f71c2838f23722d8a739f Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Fri, 9 Nov 2018 16:40:06 +0000 Subject: [PATCH 07/24] Move cmd file to more suitable place --- {buildSrc/win_scripts => .ci}/kill_corda_procs.cmd | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {buildSrc/win_scripts => .ci}/kill_corda_procs.cmd (100%) diff --git a/buildSrc/win_scripts/kill_corda_procs.cmd b/.ci/kill_corda_procs.cmd similarity index 100% rename from buildSrc/win_scripts/kill_corda_procs.cmd rename to .ci/kill_corda_procs.cmd From 46842599709211881a1ce456aa65951c0c246968 Mon Sep 17 00:00:00 2001 From: Roger Willis Date: Fri, 9 Nov 2018 17:47:36 +0000 Subject: [PATCH 08/24] Expose JPA to flows (#4140) * First pass * Update test. * Address review comments. * Added docs and kdocs. * Clean-up. * Add extra test. * Changes to docsite. * Added try/catch block as recommended by Andras. * Removed try catch block. It's not required as the checkpoint serialiser deals with this. * Re-used existing DB session instead of creating a new session. * Entity manager auto flushes. * Added java friendly api. * Addressed review comments. --- .../kotlin/net/corda/core/node/ServiceHub.kt | 24 +++++ docs/source/api-persistence.rst | 98 +++++++++++++++++++ .../persistence/DatabaseTransaction.kt | 8 ++ .../persistence/RestrictedEntityManager.kt | 19 ++++ .../net/corda/node/internal/AbstractNode.kt | 10 ++ .../persistence/ExposeJpaToFlowsTests.kt | 89 +++++++++++++++++ .../net/corda/testing/node/MockServices.kt | 19 ++++ 7 files changed, 267 insertions(+) create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManager.kt create mode 100644 node/src/test/kotlin/net/corda/node/services/persistence/ExposeJpaToFlowsTests.kt diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index 8c304d81d2..23b403e2a4 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -18,6 +18,8 @@ import net.corda.core.transactions.TransactionBuilder import java.security.PublicKey import java.sql.Connection import java.time.Clock +import java.util.function.Consumer +import javax.persistence.EntityManager /** * Subset of node services that are used for loading transactions from the wire into fully resolved, looked up @@ -358,6 +360,28 @@ interface ServiceHub : ServicesForResolution { */ fun jdbcSession(): Connection + /** + * Exposes the Java Persistence API (JPA) to flows via a restricted [EntityManager]. This method can be used to + * persist and query entities which inherit from [MappedSchema]. This is particularly useful if off-ledger data + * needs to be kept in conjunction with on-ledger state data. + * + * NOTE: Suspendable flow operations such as send, receive, subFlow and sleep, cannot be called within the lambda. + * + * @param block a lambda function with access to an [EntityManager]. + */ + fun withEntityManager(block: EntityManager.() -> T): T + + /** + * Exposes the Java Persistence API (JPA) to flows via a restricted [EntityManager]. This method can be used to + * persist and query entities which inherit from [MappedSchema]. This is particularly useful if off-ledger data + * needs to be kept in conjunction with on-ledger state data. + * + * NOTE: Suspendable flow operations such as send, receive, subFlow and sleep, cannot be called within the lambda. + * + * @param block a lambda function with access to an [EntityManager]. + */ + fun withEntityManager(block: Consumer) + /** * Allows the registration of a callback that may inform services when the app is shutting down. * diff --git a/docs/source/api-persistence.rst b/docs/source/api-persistence.rst index ef0b1ead76..9238ec3219 100644 --- a/docs/source/api-persistence.rst +++ b/docs/source/api-persistence.rst @@ -155,3 +155,101 @@ which is then referenced within a custom flow: For examples on testing ``@CordaService`` implementations, see the oracle example :doc:`here ` +JPA Support +----------- +In addition to ``jdbcSession``, ``ServiceHub`` also exposes the Java Persistence API to flows via the ``withEntityManager`` +method. This method can be used to persist and query entities which inherit from ``MappedSchema``. This is particularly +useful if off-ledger data must be maintained in conjunction with on-ledger state data. + + .. note:: Your entity must be included as a mappedType in as part of a MappedSchema for it to be added to Hibernate + as a custom schema. See Samples below. + +The code snippet below defines a ``PersistentFoo`` type inside ``FooSchemaV1``. Note that ``PersistentFoo`` is added to +a list of mapped types which is passed to ``MappedSChema``. This is exactly how state schemas are defined, except that +the entity in this case should not subclass ``PersistentState`` (as it is not a state object). See examples: + +.. container:: codeset + + .. sourcecode:: java + + public class FooSchema {} + + @CordaSerializable + public class FooSchemaV1 extends MappedSchema { + FooSchemaV1() { + super(FooSchema.class, 1, ImmutableList.of(PersistentFoo.class)); + } + + @Entity + @Table(name = "foos") + class PersistentFoo implements Serializable { + @Id + @Column(name = "foo_id") + String fooId; + + @Column(name = "foo_data") + String fooData; + } + } + + .. sourcecode:: kotlin + + object FooSchema + + object FooSchemaV1 : MappedSchema(schemaFamily = FooSchema.javaClass, version = 1, mappedTypes = listOf(PersistentFoo::class.java)) { + @Entity + @Table(name = "foos") + class PersistentFoo(@Id @Column(name = "foo_id") var fooId: String, @Column(name = "foo_data") var fooData: String) : Serializable + } + +Instances of ``PersistentFoo`` can be persisted inside a flow as follows: + +.. container:: codeset + + .. sourcecode:: java + + PersistentFoo foo = new PersistentFoo(new UniqueIdentifier().getId().toString(), "Bar"); + node.getServices().withEntityManager(entityManager -> { + entityManager.persist(foo); + entityManager.flush(); + return null; + }); + + .. sourcecode:: kotlin + + val foo = FooSchemaV1.PersistentFoo(UniqueIdentifier().id.toString(), "Bar") + serviceHub.withEntityManager { + persist(foo) + } + +And retrieved via a query, as follows: + +.. container:: codeset + + .. sourcecode:: java + + node.getServices().withEntityManager((EntityManager entityManager) -> { + CriteriaQuery query = entityManager.getCriteriaBuilder().createQuery(PersistentFoo.class); + Root type = query.from(PersistentFoo.class); + query.select(type); + return entityManager.createQuery(query).getResultList(); + }); + + .. sourcecode:: kotlin + + val result: MutableList = services.withEntityManager { + val query = criteriaBuilder.createQuery(FooSchemaV1.PersistentFoo::class.java) + val type = query.from(FooSchemaV1.PersistentFoo::class.java) + query.select(type) + createQuery(query).resultList + } + +Please note that suspendable flow operations such as: + +* ``FlowSession.send`` +* ``FlowSession.receive`` +* ``FlowLogic.receiveAll`` +* ``FlowLogic.sleep`` +* ``FlowLogic.subFlow`` + +Cannot be used within the lambda function passed to ``withEntityManager``. diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt index 770477e173..6bd4f2d223 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt @@ -7,6 +7,7 @@ import org.hibernate.Transaction import rx.subjects.PublishSubject import java.sql.Connection import java.util.* +import javax.persistence.EntityManager fun currentDBSession(): Session = contextTransaction.session private val _contextTransaction = ThreadLocal() @@ -59,6 +60,12 @@ class DatabaseTransaction( session } + // Returns a delegate which overrides certain operations that we do not want CorDapp developers to call. + val restrictedEntityManager: RestrictedEntityManager by lazy { + val entityManager = session as EntityManager + RestrictedEntityManager(entityManager) + } + val session: Session by sessionDelegate private lateinit var hibernateTransaction: Transaction @@ -101,3 +108,4 @@ class DatabaseTransaction( boundary.filter { !it.success }.subscribe { callback() } } } + diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManager.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManager.kt new file mode 100644 index 0000000000..46df399b6c --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManager.kt @@ -0,0 +1,19 @@ +package net.corda.nodeapi.internal.persistence + +import javax.persistence.EntityManager + +/** + * A delegate of [EntityManager] which disallows some operations. + */ +class RestrictedEntityManager(private val delegate: EntityManager) : EntityManager by delegate { + + override fun close() { + throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") + } + + override fun clear() { + throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") + } + + // TODO: Figure out which other methods on EntityManager need to be blocked? +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index c60904c6a1..1add6142bf 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -105,6 +105,8 @@ import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.TimeUnit.MINUTES import java.util.concurrent.TimeUnit.SECONDS +import java.util.function.Consumer +import javax.persistence.EntityManager import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair /** @@ -954,6 +956,14 @@ abstract class AbstractNode(val configuration: NodeConfiguration, override fun jdbcSession(): Connection = database.createSession() + override fun withEntityManager(block: EntityManager.() -> T): T { + return block(contextTransaction.restrictedEntityManager) + } + + override fun withEntityManager(block: Consumer) { + block.accept(contextTransaction.restrictedEntityManager) + } + // allows services to register handlers to be informed when the node stop method is called override fun registerUnloadHandler(runOnStop: () -> Unit) { this@AbstractNode.runOnStop += runOnStop diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/ExposeJpaToFlowsTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/ExposeJpaToFlowsTests.kt new file mode 100644 index 0000000000..563dd63bcf --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/persistence/ExposeJpaToFlowsTests.kt @@ -0,0 +1,89 @@ +package net.corda.node.services.persistence + +import co.paralleluniverse.fibers.Suspendable +import com.esotericsoftware.kryo.KryoException +import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowLogic.Companion.sleep +import net.corda.core.identity.CordaX500Name +import net.corda.core.schemas.MappedSchema +import net.corda.core.serialization.CordaSerializable +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.testing.common.internal.testNetworkParameters +import net.corda.testing.core.TestIdentity +import net.corda.testing.node.MockNetwork +import net.corda.testing.node.MockServices +import net.corda.testing.node.makeTestIdentityService +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Test +import java.io.Serializable +import java.time.Duration +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Id +import javax.persistence.Table +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + + +class ExposeJpaToFlowsTests { + + object FooSchema + + object FooSchemaV1 : MappedSchema(schemaFamily = FooSchema.javaClass, version = 1, mappedTypes = listOf(PersistentFoo::class.java)) { + @Entity + @Table(name = "foos") + class PersistentFoo(@Id @Column(name = "foo_id") var fooId: String, @Column(name = "foo_data") var fooData: String) : Serializable + } + + val myself = TestIdentity(CordaX500Name("Me", "London", "GB")) + val cordapps = listOf("net.corda.node.services.persistence") + val databaseAndServices = MockServices.makeTestDatabaseAndMockServices( + cordappPackages = cordapps, + identityService = makeTestIdentityService(myself.identity), + initialIdentity = myself, + networkParameters = testNetworkParameters(minimumPlatformVersion = 4) + ) + + val services: MockServices = databaseAndServices.second + val database: CordaPersistence = databaseAndServices.first + + @Test + fun `can persist and query custom entities`() { + val foo = FooSchemaV1.PersistentFoo(UniqueIdentifier().id.toString(), "Bar") + + // Persist the foo. + val result: MutableList = database.transaction { + services.withEntityManager { + // Persist. + persist(foo) + // Query. + val query = criteriaBuilder.createQuery(FooSchemaV1.PersistentFoo::class.java) + val type = query.from(FooSchemaV1.PersistentFoo::class.java) + query.select(type) + createQuery(query).resultList + } + } + + assertEquals("Bar", result.single().fooData) + } + + @Test + fun `can't perform suspendable operations inside withEntityManager`() { + val mockNet = MockNetwork(cordapps) + val mockNode = mockNet.createNode() + assertFailsWith(KryoException::class) { + mockNode.startFlow(object : FlowLogic() { + @Suspendable + override fun call() { + serviceHub.withEntityManager { + val session = initiateFlow(myself.party) + session.send("Ooohhh eee oooh ah ah ting tang walla walla bing bang!") + } + } + }) + } + mockNet.stopNodes() + } +} \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index 9d7dfa7cf6..fe2c054673 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -29,6 +29,7 @@ import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.services.vault.NodeVaultService import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig +import net.corda.nodeapi.internal.persistence.contextTransaction import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.TestIdentity import net.corda.testing.internal.DEV_ROOT_CA @@ -40,6 +41,8 @@ import java.security.KeyPair import java.sql.Connection import java.time.Clock import java.util.* +import java.util.function.Consumer +import javax.persistence.EntityManager /** * Returns a simple [InMemoryIdentityService] containing the supplied [identities]. @@ -121,6 +124,14 @@ open class MockServices private constructor( } override fun jdbcSession(): Connection = database.createSession() + + override fun withEntityManager(block: EntityManager.() -> T): T { + return block(contextTransaction.restrictedEntityManager) + } + + override fun withEntityManager(block: Consumer) { + return block.accept(contextTransaction.restrictedEntityManager) + } } } return Pair(database, mockService) @@ -268,6 +279,14 @@ open class MockServices private constructor( override fun jdbcSession(): Connection = throw UnsupportedOperationException() + override fun withEntityManager(block: EntityManager.() -> T): T { + throw UnsupportedOperationException() + } + + override fun withEntityManager(block: Consumer) { + throw UnsupportedOperationException() + } + override fun registerUnloadHandler(runOnStop: () -> Unit) = throw UnsupportedOperationException() /** Add the given package name to the list of packages which will be scanned for cordapp contract verification code */ From 2caa0827465723dc78f87f3d094dbe07a8f15bee Mon Sep 17 00:00:00 2001 From: Matthew Nesbit Date: Fri, 9 Nov 2018 17:54:51 +0000 Subject: [PATCH 09/24] Some code paths for bridge control are now being acknowledged (#4206) --- .../net/corda/nodeapi/internal/bridging/BridgeControlListener.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt index 995ce01eba..c1fb05a20a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt @@ -55,6 +55,7 @@ class BridgeControlListener(val config: MutualSslConfiguration, } catch (ex: Exception) { log.error("Unable to process bridge control message", ex) } + msg.acknowledge() } val startupMessage = BridgeControl.BridgeToNodeSnapshotRequest(bridgeId).serialize(context = SerializationDefaults.P2P_CONTEXT).bytes val bridgeRequest = artemisSession.createMessage(false) From 81418ca7e7b64f939ef38a3e9b1077d49bb2b9b8 Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Mon, 12 Nov 2018 09:38:06 +0000 Subject: [PATCH 10/24] [CORDA-2200][CORDA-2202] More tests for BCCryptoService and CryptoServiceException (#4190) --- .../internal/crypto/ContentSignerBuilder.kt | 5 +- .../internal/cryptoservice/CryptoService.kt | 2 + .../node/services/config/NodeConfiguration.kt | 6 +- .../keys/cryptoservice/BCCryptoService.kt | 54 ++++++++---- .../cryptoservice/BCCryptoServiceTests.kt | 83 +++++++++++++++++++ 5 files changed, 131 insertions(+), 19 deletions(-) create mode 100644 node/src/test/kotlin/net/corda/node/services/keys/cryptoservice/BCCryptoServiceTests.kt diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/ContentSignerBuilder.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/ContentSignerBuilder.kt index 01ce3e5b51..37e99e9146 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/ContentSignerBuilder.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/ContentSignerBuilder.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.crypto +import net.corda.core.crypto.Crypto.SPHINCS256_SHA256 import net.corda.core.crypto.SignatureScheme import org.bouncycastle.asn1.x509.AlgorithmIdentifier import org.bouncycastle.operator.ContentSigner @@ -17,7 +18,9 @@ object ContentSignerBuilder { fun build(signatureScheme: SignatureScheme, privateKey: PrivateKey, provider: Provider, random: SecureRandom? = null): ContentSigner { val sigAlgId = signatureScheme.signatureOID val sig = Signature.getInstance(signatureScheme.signatureName, provider).apply { - if (random != null) { + // TODO special handling for Sphincs due to a known BouncyCastle's Sphincs bug we reported. + // It is fixed in BC 161b12, so consider updating the below if-statement after updating BouncyCastle. + if (random != null && signatureScheme != SPHINCS256_SHA256) { initSign(privateKey, random) } else { initSign(privateKey) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/CryptoService.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/CryptoService.kt index e24b53c39d..5c1b97674b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/CryptoService.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/CryptoService.kt @@ -38,3 +38,5 @@ interface CryptoService { */ fun getSigner(alias: String): ContentSigner } + +open class CryptoServiceException(message: String?, cause: Throwable? = null) : Exception(message, cause) diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index 27fea92b99..28f2fb68e4 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -107,8 +107,8 @@ interface NodeConfiguration { fun makeCryptoService(): CryptoService { return when(cryptoServiceName) { - SupportedCryptoServices.BC_SIMPLE -> BCCryptoService(this) - null -> BCCryptoService(this) // Pick default BCCryptoService when null. + // Pick default BCCryptoService when null. + SupportedCryptoServices.BC_SIMPLE, null -> BCCryptoService(this.myLegalName.x500Principal, this.signingCertificateStore) } } } @@ -117,7 +117,7 @@ data class FlowOverrideConfig(val overrides: List = listOf()) data class FlowOverride(val initiator: String, val responder: String) /** - * Currently registered JMX Reporters + * Currently registered JMX Reporters. */ enum class JmxReporterType { JOLOKIA, NEW_RELIC diff --git a/node/src/main/kotlin/net/corda/node/services/keys/cryptoservice/BCCryptoService.kt b/node/src/main/kotlin/net/corda/node/services/keys/cryptoservice/BCCryptoService.kt index 744b6a15d1..dc554037c9 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/cryptoservice/BCCryptoService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/cryptoservice/BCCryptoService.kt @@ -2,31 +2,39 @@ package net.corda.node.services.keys.cryptoservice import net.corda.core.crypto.Crypto import net.corda.core.crypto.newSecureRandom +import net.corda.core.crypto.sha256 import net.corda.node.services.config.NodeConfiguration import net.corda.nodeapi.internal.config.CertificateStore +import net.corda.nodeapi.internal.config.CertificateStoreSupplier import net.corda.nodeapi.internal.crypto.ContentSignerBuilder import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.cryptoservice.CryptoService +import net.corda.nodeapi.internal.cryptoservice.CryptoServiceException import org.bouncycastle.operator.ContentSigner import java.security.KeyPair import java.security.KeyStore import java.security.PublicKey +import javax.security.auth.x500.X500Principal /** * Basic implementation of a [CryptoService] that uses BouncyCastle for cryptographic operations * and a Java KeyStore in the form of [CertificateStore] to store private keys. * This service reuses the [NodeConfiguration.signingCertificateStore] to store keys. */ -class BCCryptoService(private val nodeConf: NodeConfiguration) : CryptoService { +class BCCryptoService(private val legalName: X500Principal, private val certificateStoreSupplier: CertificateStoreSupplier) : CryptoService { // TODO check if keyStore exists. // TODO make it private when E2ETestKeyManagementService does not require direct access to the private key. - internal var certificateStore: CertificateStore = nodeConf.signingCertificateStore.get(true) + internal var certificateStore: CertificateStore = certificateStoreSupplier.get(true) override fun generateKeyPair(alias: String, schemeNumberID: Int): PublicKey { - val keyPair = Crypto.generateKeyPair(Crypto.findSignatureScheme(schemeNumberID)) - importKey(alias, keyPair) - return keyPair.public + try { + val keyPair = Crypto.generateKeyPair(Crypto.findSignatureScheme(schemeNumberID)) + importKey(alias, keyPair) + return keyPair.public + } catch (e: Exception) { + throw CryptoServiceException("Cannot generate key for alias $alias and signature scheme with id $schemeNumberID", e) + } } override fun containsKey(alias: String): Boolean { @@ -34,17 +42,29 @@ class BCCryptoService(private val nodeConf: NodeConfiguration) : CryptoService { } override fun getPublicKey(alias: String): PublicKey { - return certificateStore.query { getPublicKey(alias) } + try { + return certificateStore.query { getPublicKey(alias) } + } catch (e: Exception) { + throw CryptoServiceException("Cannot get public key for alias $alias", e) + } } override fun sign(alias: String, data: ByteArray): ByteArray { - return Crypto.doSign(certificateStore.query { getPrivateKey(alias, certificateStore.entryPassword) } , data) + try { + return Crypto.doSign(certificateStore.query { getPrivateKey(alias, certificateStore.entryPassword) }, data) + } catch (e: Exception) { + throw CryptoServiceException("Cannot sign using the key with alias $alias. SHA256 of data to be signed: ${data.sha256()}", e) + } } override fun getSigner(alias: String): ContentSigner { - val privateKey = certificateStore.query { getPrivateKey(alias, certificateStore.entryPassword) } - val signatureScheme = Crypto.findSignatureScheme(privateKey) - return ContentSignerBuilder.build(signatureScheme, privateKey, Crypto.findProvider(signatureScheme.providerName), newSecureRandom()) + try { + val privateKey = certificateStore.query { getPrivateKey(alias, certificateStore.entryPassword) } + val signatureScheme = Crypto.findSignatureScheme(privateKey) + return ContentSignerBuilder.build(signatureScheme, privateKey, Crypto.findProvider(signatureScheme.providerName), newSecureRandom()) + } catch (e: Exception) { + throw CryptoServiceException("Cannot get Signer for key with alias $alias", e) + } } /** @@ -53,14 +73,18 @@ class BCCryptoService(private val nodeConf: NodeConfiguration) : CryptoService { * loaded [certificateStore] in memory with the contents of the corresponding [KeyStore] file. */ fun resyncKeystore() { - certificateStore = nodeConf.signingCertificateStore.get(true) + certificateStore = certificateStoreSupplier.get(true) } /** Import an already existing [KeyPair] to this [CryptoService]. */ fun importKey(alias: String, keyPair: KeyPair) { - // Store a self-signed certificate, as Keystore requires to store certificates instead of public keys. - // We could probably add a null cert, but we store a self-signed cert that will be used to retrieve the public key. - val cert = X509Utilities.createSelfSignedCACertificate(nodeConf.myLegalName.x500Principal, keyPair) - certificateStore.query { setPrivateKey(alias, keyPair.private, listOf(cert), certificateStore.entryPassword) } + try { + // Store a self-signed certificate, as Keystore requires to store certificates instead of public keys. + // We could probably add a null cert, but we store a self-signed cert that will be used to retrieve the public key. + val cert = X509Utilities.createSelfSignedCACertificate(legalName, keyPair) + certificateStore.query { setPrivateKey(alias, keyPair.private, listOf(cert), certificateStore.entryPassword) } + } catch (e: Exception) { + throw CryptoServiceException("Cannot import key with alias $alias", e) + } } } diff --git a/node/src/test/kotlin/net/corda/node/services/keys/cryptoservice/BCCryptoServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/keys/cryptoservice/BCCryptoServiceTests.kt new file mode 100644 index 0000000000..6bb9e5d2b9 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/keys/cryptoservice/BCCryptoServiceTests.kt @@ -0,0 +1,83 @@ +package net.corda.node.services.keys.cryptoservice + +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SignatureScheme +import net.corda.core.internal.div +import net.corda.core.utilities.days +import net.corda.nodeapi.internal.config.CertificateStoreSupplier +import net.corda.nodeapi.internal.crypto.CertificateType +import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.cryptoservice.CryptoServiceException +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.internal.stubs.CertificateStoreStubs +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.time.Duration +import javax.security.auth.x500.X500Principal +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class BCCryptoServiceTests { + + companion object { + val clearData = "data".toByteArray() + } + + @Rule + @JvmField + val temporaryFolder = TemporaryFolder() + private lateinit var signingCertificateStore: CertificateStoreSupplier + + @Before + fun setUp() { + val baseDirectory = temporaryFolder.root.toPath() + val certificatesDirectory = baseDirectory / "certificates" + signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory) + } + + @Test + fun `BCCryptoService generate key pair and sign both data and cert`() { + val cryptoService = BCCryptoService(ALICE_NAME.x500Principal, signingCertificateStore) + // Testing every supported scheme. + Crypto.supportedSignatureSchemes().filter { it != Crypto.COMPOSITE_KEY }.forEach { generateKeyAndSignForScheme(cryptoService, it) } + } + + private fun generateKeyAndSignForScheme(cryptoService: BCCryptoService, signatureScheme: SignatureScheme) { + val schemeNumberID = signatureScheme.schemeNumberID + val alias = "signature$schemeNumberID" + val pubKey = cryptoService.generateKeyPair(alias, schemeNumberID) + assertTrue { cryptoService.containsKey(alias) } + + val signatureData = cryptoService.sign(alias, clearData) + assertTrue(Crypto.doVerify(pubKey, signatureData, clearData)) + + // Test that getSigner can indeed sign a certificate. + val signer = cryptoService.getSigner(alias) + val x500Principal = X500Principal("CN=Test") + val window = X509Utilities.getCertificateValidityWindow(Duration.ZERO, 365.days) + val certificate = X509Utilities.createCertificate( + CertificateType.CONFIDENTIAL_LEGAL_IDENTITY, + x500Principal, + pubKey, + signer, + x500Principal, + pubKey, + window) + + certificate.checkValidity() + certificate.verify(pubKey) + } + + @Test + fun `When key does not exist getPublicKey, sign and getSigner should throw`() { + val nonExistingAlias = "nonExistingAlias" + val cryptoService = BCCryptoService(ALICE_NAME.x500Principal, signingCertificateStore) + assertFalse { cryptoService.containsKey(nonExistingAlias) } + assertFailsWith { cryptoService.getPublicKey(nonExistingAlias) } + assertFailsWith { cryptoService.sign(nonExistingAlias, clearData) } + assertFailsWith { cryptoService.getSigner(nonExistingAlias) } + } +} From 23907c40d908ed3aff18839fbff9885647c5a108 Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Mon, 12 Nov 2018 11:59:04 +0000 Subject: [PATCH 11/24] [CORDA-2210] Add matching party on the first X500 attribute match (#4211) --- .../services/api/IdentityServiceInternal.kt | 25 ++++++++----------- .../identity/InMemoryIdentityService.kt | 6 +++-- .../identity/PersistentIdentityService.kt | 8 +++--- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/api/IdentityServiceInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/IdentityServiceInternal.kt index d684948341..523c996f22 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/IdentityServiceInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/IdentityServiceInternal.kt @@ -36,22 +36,17 @@ interface IdentityServiceInternal : IdentityService { @Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class) fun verifyAndRegisterIdentity(identity: PartyAndCertificate, isNewRandomIdentity: Boolean): PartyAndCertificate? - fun partiesFromName(query: String, exactMatch: Boolean, x500name: CordaX500Name, results: LinkedHashSet, party: Party) { + // We can imagine this being a query over a lucene index in future. + // + // Kostas says: When exactMatch = false, we can easily use the Jaro-Winkler distance metric as it is best suited for short + // strings such as entity/company names, and to detect small typos. We can also apply it for city + // or any keyword related search in lists of records (not raw text - for raw text we need indexing) + // and we can return results in hierarchical order (based on normalised String similarity 0.0-1.0). + /** Check if [x500name] matches the [query]. */ + fun x500Matches(query: String, exactMatch: Boolean, x500name: CordaX500Name): Boolean { val components = listOfNotNull(x500name.commonName, x500name.organisationUnit, x500name.organisation, x500name.locality, x500name.state, x500name.country) - components.forEach { component -> - if (exactMatch && component == query) { - results += party - } else if (!exactMatch) { - // We can imagine this being a query over a lucene index in future. - // - // Kostas says: We can easily use the Jaro-Winkler distance metric as it is best suited for short - // strings such as entity/company names, and to detect small typos. We can also apply it for city - // or any keyword related search in lists of records (not raw text - for raw text we need indexing) - // and we can return results in hierarchical order (based on normalised String similarity 0.0-1.0). - if (component.contains(query, ignoreCase = true)) - results += party - } - } + return components.any { (exactMatch && it == query) + || (!exactMatch && it.contains(query, ignoreCase = true)) } } /** diff --git a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt index bf8ff575ce..3b46ddaca1 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt @@ -63,8 +63,10 @@ class InMemoryIdentityService(identities: List = emptyList( override fun partiesFromName(query: String, exactMatch: Boolean): Set { val results = LinkedHashSet() - for ((x500name, partyAndCertificate) in principalToParties) { - partiesFromName(query, exactMatch, x500name, results, partyAndCertificate.party) + principalToParties.forEach { (x500name, partyAndCertificate) -> + if (x500Matches(query, exactMatch, x500name)) { + results += partyAndCertificate.party + } } return results } diff --git a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt index 43de48c6b7..32cd247bb3 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt @@ -143,7 +143,7 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri val key = mapToKey(identity) if (isNewRandomIdentity) { // Because this is supposed to be new and random, there's no way we have it in the database already, so skip the pessimistic check. - keyToParties.set(key, identity) + keyToParties[key] = identity } else { keyToParties.addWithDuplicatesAllowed(key, identity) } @@ -174,8 +174,10 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri override fun partiesFromName(query: String, exactMatch: Boolean): Set { return database.transaction { val results = LinkedHashSet() - for ((x500name, partyId) in principalToParties.allPersisted()) { - partiesFromName(query, exactMatch, x500name, results, keyToParties[partyId]!!.party) + principalToParties.allPersisted().forEach { (x500name, partyId) -> + if (x500Matches(query, exactMatch, x500name)) { + results += keyToParties[partyId]!!.party + } } results } From 2f833f589c95b6c407fad1c20c127c6dc420e9a2 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Mon, 12 Nov 2018 13:38:35 +0000 Subject: [PATCH 12/24] Fixed missing entries for common modules in parent `build.gradle` file. (#4214) * Fixed missing entries for common modules in parent `build.gradle` file. * Fixed publication names to match existing convention. --- build.gradle | 4 +++- common/configuration-parsing/build.gradle | 2 +- common/validation/build.gradle | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 28a0247cb4..da93c2b13e 100644 --- a/build.gradle +++ b/build.gradle @@ -366,7 +366,9 @@ bintrayConfig { 'corda-tools-network-bootstrapper', 'corda-tools-cliutils', 'corda-notary-raft', - 'corda-notary-bft-smart' + 'corda-notary-bft-smart', + 'corda-common-configuration-parsing', + 'corda-common-validation' ] license { name = 'Apache-2.0' diff --git a/common/configuration-parsing/build.gradle b/common/configuration-parsing/build.gradle index 3b44430643..9445cbd417 100644 --- a/common/configuration-parsing/build.gradle +++ b/common/configuration-parsing/build.gradle @@ -17,7 +17,7 @@ dependencies { } jar { - baseName 'common-configuration-parsing' + baseName 'corda-common-configuration-parsing' } publish { diff --git a/common/validation/build.gradle b/common/validation/build.gradle index 17333abe48..61521193b3 100644 --- a/common/validation/build.gradle +++ b/common/validation/build.gradle @@ -11,7 +11,7 @@ dependencies { } jar { - baseName 'common-validation' + baseName 'corda-common-validation' } publish { From ac23fcdf24ddc3a9b2ea09901415cd2b7780c090 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Mon, 12 Nov 2018 13:50:10 +0000 Subject: [PATCH 13/24] [CORDA-2208]: NodeHandles hashcode attemps an RPC invocation. (#4217) --- .../rpc/internal/RPCClientProxyHandler.kt | 38 ++++++++++++++++++- .../node/services/rpc/NodeHandleTests.kt | 21 ++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 node/src/integration-test/kotlin/net/corda/node/services/rpc/NodeHandleTests.kt diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt index 6ceff2b9d0..6087429036 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt @@ -100,6 +100,8 @@ class RPCClientProxyHandler( private val log = contextLogger() // To check whether toString() is being invoked val toStringMethod: Method = Object::toString.javaMethod!! + val equalsMethod: Method = Object::equals.javaMethod!! + val hashCodeMethod: Method = Object::hashCode.javaMethod!! private fun addRpcCallSiteToThrowable(throwable: Throwable, callSite: CallSite) { var currentThrowable = throwable @@ -234,7 +236,13 @@ class RPCClientProxyHandler( lifeCycle.requireState { it == State.STARTED || it == State.SERVER_VERSION_NOT_SET } checkProtocolVersion(method) if (method == toStringMethod) { - return "Client RPC proxy for $rpcOpsClass" + return toString() + } + if (method == equalsMethod) { + return equals(arguments?.getOrNull(0)) + } + if (method == hashCodeMethod) { + return hashCode() } if (consumerSession!!.isClosed) { throw RPCException("RPC Proxy is closed") @@ -364,6 +372,8 @@ class RPCClientProxyHandler( close(false) } + + /** * Closes this handler and sends notifications to all observables, so it can immediately clean up resources. * Notifications sent to observables are to be acknowledged, therefore this call blocks until all acknowledgements are received. @@ -568,6 +578,32 @@ class RPCClientProxyHandler( rpcReplyMap.clear() callSiteMap?.clear() } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RPCClientProxyHandler + + if (rpcUsername != other.rpcUsername) return false + if (clientAddress != other.clientAddress) return false + if (sessionId != other.sessionId) return false + if (targetLegalIdentity != other.targetLegalIdentity) return false + + return true + } + + override fun hashCode(): Int { + var result = rpcUsername.hashCode() + result = 31 * result + clientAddress.hashCode() + result = 31 * result + sessionId.hashCode() + result = 31 * result + (targetLegalIdentity?.hashCode() ?: 0) + return result + } + + override fun toString(): String { + return "{rpcUsername='$rpcUsername', clientAddress=$clientAddress, sessionId=$sessionId, targetLegalIdentity=$targetLegalIdentity}" + } } private typealias RpcObservableMap = Cache>> diff --git a/node/src/integration-test/kotlin/net/corda/node/services/rpc/NodeHandleTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/rpc/NodeHandleTests.kt new file mode 100644 index 0000000000..77852b5f7a --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/services/rpc/NodeHandleTests.kt @@ -0,0 +1,21 @@ +package net.corda.node.services.rpc + +import net.corda.core.utilities.getOrThrow +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.driver +import org.assertj.core.api.Assertions.assertThatCode +import org.junit.Test + +class NodeHandleTests { + @Test + fun object_defined_functions_are_static_for_node_rpc_ops() { + driver(DriverParameters(startNodesInProcess = true)) { + val rpcClient = startNode().getOrThrow().rpc + + assertThatCode { rpcClient.hashCode() }.doesNotThrowAnyException() + @Suppress("UnusedEquals") + assertThatCode { rpcClient == rpcClient }.doesNotThrowAnyException() + assertThatCode { rpcClient.toString() }.doesNotThrowAnyException() + } + } +} \ No newline at end of file From dc62b20c5d87d5866609ed5707c09c795e4c0c26 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Mon, 12 Nov 2018 15:56:04 +0000 Subject: [PATCH 14/24] [CORDA-1879]: Ensure Node dies on unrecoverable errors. (#4213) --- .../client/jfx/model/NodeMonitorModel.kt | 6 +- .../corda/client/rpc/CordaRPCClientTest.kt | 2 +- .../rpc/internal/RPCClientProxyHandler.kt | 4 +- .../net/corda/core/contracts/Attachment.kt | 9 +- .../internal/concurrent/CordaFutureImpl.kt | 12 +- .../core/transactions/LedgerTransaction.kt | 4 +- .../kotlin/net/corda/core/utilities/Try.kt | 4 +- experimental/kryo-hook/build.gradle | 31 ---- .../kotlin/net/corda/kryohook/KryoHook.kt | 164 ------------------ .../main/kotlin/net/corda/kryohook/README.md | 23 --- .../quasarhook/QuasarInstrumentationHook.kt | 1 + .../main/kotlin/net/corda/nodeapi/RPCApi.kt | 4 +- .../internal/persistence/CordaPersistence.kt | 6 +- .../internal/crypto/X509UtilitiesTest.kt | 2 +- .../net/corda/node/internal/AbstractNode.kt | 4 +- .../kotlin/net/corda/node/internal/Node.kt | 2 +- .../net/corda/node/internal/NodeStartup.kt | 2 +- .../node/services/messaging/RPCServer.kt | 1 + .../services/network/NetworkMapUpdater.kt | 4 +- .../statemachine/FlowStateMachineImpl.kt | 14 +- .../SingleThreadedStateMachineManager.kt | 11 +- .../statemachine/TransitionExecutorImpl.kt | 2 +- ...FiberDeserializationCheckingInterceptor.kt | 4 +- .../services/vault/NodeVaultServiceTest.kt | 4 +- .../node/utilities/TLSAuthenticationTests.kt | 2 +- .../corda/irs/web/api/InterestSwapRestAPI.kt | 2 +- .../internal/amqp/DeserializationInput.kt | 5 +- settings.gradle | 1 - .../net/corda/testing/driver/DriverTests.kt | 4 +- .../node/internal/InternalTestUtils.kt | 4 +- .../testing/node/internal/ShutdownManager.kt | 3 + .../node/internal/performance/Injectors.kt | 4 +- .../net/corda/blobinspector/BlobInspector.kt | 2 +- .../kotlin/net/corda/loadtest/LoadTest.kt | 6 +- .../instance/docker/DockerInstantiator.kt | 4 +- .../corda/bootstrapper/nodes/NodeFinder.kt | 3 +- .../bootstrapper/notaries/NotaryFinder.kt | 3 +- .../volumes/azure/AzureSmbVolume.kt | 2 +- 38 files changed, 83 insertions(+), 282 deletions(-) delete mode 100644 experimental/kryo-hook/build.gradle delete mode 100644 experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/KryoHook.kt delete mode 100644 experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/README.md diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt index 550344ae35..b42deddb8c 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt @@ -204,13 +204,13 @@ class NodeMonitorModel : AutoCloseable { val nodeInfo = _connection.proxy.nodeInfo() require(nodeInfo.legalIdentitiesAndCerts.isNotEmpty()) _connection - } catch (throwable: Throwable) { + } catch (exception: Exception) { if (shouldRetry) { // Deliberately not logging full stack trace as it will be full of internal stacktraces. - logger.info("Exception upon establishing connection: {}", throwable.message) + logger.info("Exception upon establishing connection: {}", exception.message) null } else { - throw throwable + throw exception } } diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt index 9b788ed016..3b842ab922 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt @@ -116,7 +116,7 @@ class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance")) { nodeIsShut.onCompleted() } catch (e: ActiveMQSecurityException) { // nothing here - this happens if trying to connect before the node is started - } catch (e: Throwable) { + } catch (e: Exception) { nodeIsShut.onError(e) } }, 1, 1, TimeUnit.SECONDS) diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt index 6087429036..ee9acfe3b4 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt @@ -564,8 +564,8 @@ class RPCClientProxyHandler( observationExecutorPool.run(k) { try { m[k]?.onError(ConnectionFailureException()) - } catch (th: Throwable) { - log.error("Unexpected exception when RPC connection failure handling", th) + } catch (e: Exception) { + log.error("Unexpected exception when RPC connection failure handling", e) } } } diff --git a/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt b/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt index 0535f051e6..42faff24f2 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt @@ -4,6 +4,7 @@ import net.corda.core.KeepForDJVM import net.corda.core.internal.extractFile import net.corda.core.serialization.CordaSerializable import java.io.FileNotFoundException +import java.io.IOException import java.io.InputStream import java.io.OutputStream import java.security.PublicKey @@ -36,10 +37,10 @@ interface Attachment : NamedByHash { @JvmDefault fun openAsJAR(): JarInputStream { val stream = open() - try { - return JarInputStream(stream) - } catch (t: Throwable) { - stream.use { throw t } + return try { + JarInputStream(stream) + } catch (e: IOException) { + stream.use { throw e } } } diff --git a/core/src/main/kotlin/net/corda/core/internal/concurrent/CordaFutureImpl.kt b/core/src/main/kotlin/net/corda/core/internal/concurrent/CordaFutureImpl.kt index 5102fee290..da2c2a7eea 100644 --- a/core/src/main/kotlin/net/corda/core/internal/concurrent/CordaFutureImpl.kt +++ b/core/src/main/kotlin/net/corda/core/internal/concurrent/CordaFutureImpl.kt @@ -71,8 +71,8 @@ fun CordaFuture.flatMap(transform: (V) -> CordaFuture): Cor thenMatch(success@ { result.captureLater(try { transform(it) - } catch (t: Throwable) { - result.setException(t) + } catch (e: Exception) { + result.setException(e) return@success }) }, { @@ -128,8 +128,8 @@ interface ValueOrException { fun capture(block: () -> V): Boolean { return set(try { block() - } catch (t: Throwable) { - return setException(t) + } catch (e: Exception) { + return setException(e) }) } } @@ -153,8 +153,8 @@ internal class CordaFutureImpl(private val impl: CompletableFuture = Compl impl.whenComplete { _, _ -> try { callback(this) - } catch (t: Throwable) { - log.error(listenerFailedMessage, t) + } catch (e: Exception) { + log.error(listenerFailedMessage, e) } } } diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 873615def6..c9557bf7c7 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -193,7 +193,7 @@ data class LedgerTransaction @JvmOverloads constructor( is Try.Success -> { try { contractInstances.add(result.value.newInstance()) - } catch (e: Throwable) { + } catch (e: Exception) { throw TransactionVerificationException.ContractCreationError(id, result.value.name, e) } } @@ -202,7 +202,7 @@ data class LedgerTransaction @JvmOverloads constructor( contractInstances.forEach { contract -> try { contract.verify(this) - } catch (e: Throwable) { + } catch (e: Exception) { throw TransactionVerificationException.ContractRejection(id, contract, e) } } diff --git a/core/src/main/kotlin/net/corda/core/utilities/Try.kt b/core/src/main/kotlin/net/corda/core/utilities/Try.kt index d6b5dbdf14..d68128cf77 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/Try.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/Try.kt @@ -19,8 +19,8 @@ sealed class Try { inline fun on(body: () -> T): Try { return try { Success(body()) - } catch (t: Throwable) { - Failure(t) + } catch (e: Exception) { + Failure(e) } } } diff --git a/experimental/kryo-hook/build.gradle b/experimental/kryo-hook/build.gradle deleted file mode 100644 index ab36a754b6..0000000000 --- a/experimental/kryo-hook/build.gradle +++ /dev/null @@ -1,31 +0,0 @@ -ext { - javaassist_version = "3.12.1.GA" -} - -apply plugin: 'kotlin' -apply plugin: 'idea' - -description 'A javaagent to allow hooking into Kryo' - -dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - compile "javassist:javassist:$javaassist_version" - compile "com.esotericsoftware:kryo:4.0.0" - compile "$quasar_group:quasar-core:$quasar_version:jdk8" -} - -jar { - archiveName = "${project.name}.jar" - manifest { - attributes( - 'Premain-Class': 'net.corda.kryohook.KryoHookAgent', - 'Can-Redefine-Classes': 'true', - 'Can-Retransform-Classes': 'true', - 'Can-Set-Native-Method-Prefix': 'true', - 'Implementation-Title': "KryoHook", - 'Implementation-Version': rootProject.version - ) - } - from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } -} diff --git a/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/KryoHook.kt b/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/KryoHook.kt deleted file mode 100644 index 49b0add073..0000000000 --- a/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/KryoHook.kt +++ /dev/null @@ -1,164 +0,0 @@ -package net.corda.kryohook - -import co.paralleluniverse.strands.Strand -import com.esotericsoftware.kryo.Kryo -import com.esotericsoftware.kryo.io.Output -import javassist.ClassPool -import javassist.CtClass -import java.lang.instrument.ClassFileTransformer -import java.lang.instrument.Instrumentation -import java.security.ProtectionDomain -import java.util.concurrent.ConcurrentHashMap - -class KryoHookAgent { - companion object { - @JvmStatic - fun premain(@Suppress("UNUSED_PARAMETER") argumentsString: String?, instrumentation: Instrumentation) { - Runtime.getRuntime().addShutdownHook(Thread { - val statsTrees = KryoHook.events.values.flatMap { - readTrees(it, 0).second - } - val builder = StringBuilder() - statsTrees.forEach { - prettyStatsTree(0, it, builder) - } - print(builder.toString()) - }) - instrumentation.addTransformer(KryoHook) - } - } -} - -fun prettyStatsTree(indent: Int, statsTree: StatsTree, builder: StringBuilder) { - when (statsTree) { - is StatsTree.Object -> { - builder.append(kotlin.CharArray(indent) { ' ' }) - builder.append(statsTree.className) - builder.append(" ") - builder.append(statsTree.size) - builder.append("\n") - for (child in statsTree.children) { - prettyStatsTree(indent + 2, child, builder) - } - } - } -} - -/** - * The hook simply records the write() entries and exits together with the output offset at the time of the call. - * This is recorded in a StrandID -> List map. - * - * Later we "parse" these lists into a tree. - */ -object KryoHook : ClassFileTransformer { - val classPool = ClassPool.getDefault()!! - - val hookClassName = javaClass.name!! - - override fun transform( - loader: ClassLoader?, - className: String, - classBeingRedefined: Class<*>?, - protectionDomain: ProtectionDomain?, - classfileBuffer: ByteArray - ): ByteArray? { - if (className.startsWith("java") || className.startsWith("javassist") || className.startsWith("kotlin")) { - return null - } - return try { - val clazz = classPool.makeClass(classfileBuffer.inputStream()) - instrumentClass(clazz)?.toBytecode() - } catch (throwable: Throwable) { - println("SOMETHING WENT WRONG") - throwable.printStackTrace(System.out) - null - } - } - - private fun instrumentClass(clazz: CtClass): CtClass? { - for (method in clazz.declaredBehaviors) { - if (method.name == "write") { - val parameterTypeNames = method.parameterTypes.map { it.name } - if (parameterTypeNames == listOf("com.esotericsoftware.kryo.Kryo", "com.esotericsoftware.kryo.io.Output", "java.lang.Object")) { - if (method.isEmpty) continue - println("Instrumenting ${clazz.name}") - method.insertBefore("$hookClassName.${this::writeEnter.name}($1, $2, $3);") - method.insertAfter("$hookClassName.${this::writeExit.name}($1, $2, $3);") - return clazz - } - } - } - return null - } - - // StrandID -> StatsEvent map - val events = ConcurrentHashMap>() - - @JvmStatic - fun writeEnter(@Suppress("UNUSED_PARAMETER") kryo: Kryo, output: Output, obj: Any) { - events.getOrPut(Strand.currentStrand().id) { ArrayList() }.add( - StatsEvent.Enter(obj.javaClass.name, output.total()) - ) - } - @JvmStatic - fun writeExit(@Suppress("UNUSED_PARAMETER") kryo: Kryo, output: Output, obj: Any) { - events[Strand.currentStrand().id]!!.add( - StatsEvent.Exit(obj.javaClass.name, output.total()) - ) - } -} - -/** - * TODO we could add events on entries/exits to field serializers to get more info on what's being serialised. - */ -sealed class StatsEvent { - data class Enter(val className: String, val offset: Long) : StatsEvent() - data class Exit(val className: String, val offset: Long) : StatsEvent() -} - -/** - * TODO add Field constructor. - */ -sealed class StatsTree { - data class Object( - val className: String, - val size: Long, - val children: List - ) : StatsTree() -} - -fun readTree(events: List, index: Int): Pair { - val event = events[index] - when (event) { - is StatsEvent.Enter -> { - val (nextIndex, children) = readTrees(events, index + 1) - val exit = events[nextIndex] as StatsEvent.Exit - require(event.className == exit.className) - return Pair(nextIndex + 1, StatsTree.Object(event.className, exit.offset - event.offset, children)) - } - is StatsEvent.Exit -> { - throw IllegalStateException("Wasn't expecting Exit") - } - } -} - -fun readTrees(events: List, index: Int): Pair> { - val trees = ArrayList() - var i = index - while (true) { - val event = events.getOrNull(i) - when (event) { - is StatsEvent.Enter -> { - val (nextIndex, tree) = readTree(events, i) - trees.add(tree) - i = nextIndex - } - is StatsEvent.Exit -> { - return Pair(i, trees) - } - null -> { - return Pair(i, trees) - } - } - } -} diff --git a/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/README.md b/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/README.md deleted file mode 100644 index ec7899a290..0000000000 --- a/experimental/kryo-hook/src/main/kotlin/net/corda/kryohook/README.md +++ /dev/null @@ -1,23 +0,0 @@ -What is this ------------- - -This is a javaagent that hooks into Kryo serializers to record a breakdown of how many bytes objects take in the output. - -The dump is quite ugly now, but the in-memory representation is a simple tree so we could put some nice visualisation on -top if we want. - -How do I run it ---------------- - -Build the agent: -``` -./gradlew experimental:kryo-hook:jar -``` - -Add this JVM flag to what you're running: - -``` --javaagent:/experimental/kryo-hook/build/libs/kryo-hook.jar -``` - -The agent will dump the output when the JVM shuts down. diff --git a/experimental/quasar-hook/src/main/kotlin/net/corda/quasarhook/QuasarInstrumentationHook.kt b/experimental/quasar-hook/src/main/kotlin/net/corda/quasarhook/QuasarInstrumentationHook.kt index 2ae0c41e9d..f547e4cc38 100644 --- a/experimental/quasar-hook/src/main/kotlin/net/corda/quasarhook/QuasarInstrumentationHook.kt +++ b/experimental/quasar-hook/src/main/kotlin/net/corda/quasarhook/QuasarInstrumentationHook.kt @@ -191,6 +191,7 @@ object QuasarInstrumentationHook : ClassFileTransformer { } catch (throwable: Throwable) { println("SOMETHING WENT WRONG") throwable.printStackTrace(System.out) + if (throwable is VirtualMachineError) throw throwable classfileBuffer } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt b/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt index 1f118aa631..cf6b813cf5 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt @@ -220,8 +220,8 @@ object RPCApi { companion object { private fun Any.safeSerialize(context: SerializationContext, wrap: (Throwable) -> Any) = try { serialize(context = context) - } catch (t: Throwable) { - wrap(t).serialize(context = context) + } catch (e: Exception) { + wrap(e).serialize(context = context) } fun fromClientMessage(context: SerializationContext, message: ClientMessage): ServerToClient { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt index 388438948b..ad86bee392 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt @@ -160,8 +160,8 @@ class CordaPersistence( var recoverableFailureCount = 0 fun quietly(task: () -> T) = try { task() - } catch (t: Throwable) { - log.warn("Cleanup task failed:", t) + } catch (e: Exception) { + log.warn("Cleanup task failed:", e) } while (true) { val transaction = contextDatabase.currentOrNew(isolationLevel) // XXX: Does this code really support statement changing the contextDatabase? @@ -169,7 +169,7 @@ class CordaPersistence( val answer = transaction.statement() transaction.commit() return answer - } catch (e: Throwable) { + } catch (e: Exception) { quietly(transaction::rollback) if (e is SQLException || (recoverAnyNestedSQLException && e.hasSQLExceptionCause())) { if (++recoverableFailureCount > recoverableFailureTolerance) throw e diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt index e2cfff84ca..ee0676639a 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt @@ -318,7 +318,7 @@ class X509UtilitiesTest { lock.notifyAll() } sslServerSocket.close() - } catch (ex: Throwable) { + } catch (ex: Exception) { serverError = true } } 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 1add6142bf..d48580b5e5 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -493,8 +493,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val republishInterval = try { networkMapClient.publish(signedNodeInfo) heartbeatInterval - } catch (t: Throwable) { - log.warn("Error encountered while publishing node info, will retry again", t) + } catch (e: Exception) { + log.warn("Error encountered while publishing node info, will retry again", e) // TODO: Exponential backoff? It should reach max interval of eventHorizon/2. 1.minutes } 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 395b6c5697..20d4cca782 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -333,7 +333,7 @@ open class Node(configuration: NodeConfiguration, log.info("Retrieved public IP from Network Map Service: $this. This will be used instead of the provided \"$host\" as the advertised address.") } retrievedHostName - } catch (ignore: Throwable) { + } catch (ignore: Exception) { // Cannot reach the network map service, ignore the exception and use provided P2P address instead. log.warn("Cannot connect to the network map service for public IP detection.") null 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 79595ac719..2c9344a087 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -190,7 +190,7 @@ open class NodeStartup : NodeStartupLogging { node.startupComplete.then { try { InteractiveShell.runLocalShell(node::stop) - } catch (e: Throwable) { + } catch (e: Exception) { logger.error("Shell failed to start", e) } } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt index d8e144e17b..d5b3b0bc7d 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt @@ -232,6 +232,7 @@ class RPCServer( log.error("Failed to send message, kicking client. Message was ${job.message}", throwable) serverControl!!.closeConsumerConnectionsForAddress(job.clientAddress.toString()) invalidateClient(job.clientAddress) + if (throwable is VirtualMachineError) throw throwable } } diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt index 9194b413e2..6536f5869c 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt @@ -94,8 +94,8 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, override fun run() { val nextScheduleDelay = try { updateNetworkMapCache() - } catch (t: Throwable) { - logger.warn("Error encountered while updating network map, will retry in $defaultRetryInterval", t) + } catch (e: Exception) { + logger.warn("Error encountered while updating network map, will retry in $defaultRetryInterval", e) defaultRetryInterval } // Schedule the next update. diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt index 34b81ff9df..5bb11fa435 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt @@ -219,9 +219,13 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, val result = logic.call() suspend(FlowIORequest.WaitForSessionConfirmations, maySkipCheckpoint = true) Try.Success(result) - } catch (throwable: Throwable) { - logger.info("Flow threw exception... sending it to flow hospital", throwable) - Try.Failure(throwable) + } catch (t: Throwable) { + if(t is VirtualMachineError) { + logger.error("Caught unrecoverable error from flow. Forcibly terminating the JVM, this might leave resources open, and most likely will.", t) + Runtime.getRuntime().halt(1) + } + logger.info("Flow raised an error... sending it to flow hospital", t) + Try.Failure(t) } val softLocksId = if (hasSoftLockedStates) logic.runId.uuid else null val finalEvent = when (resultOrError) { @@ -373,8 +377,8 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, maySkipCheckpoint = skipPersistingCheckpoint, fiber = this.checkpointSerialize(context = serializationContext.value) ) - } catch (throwable: Throwable) { - Event.Error(throwable) + } catch (exception: Exception) { + Event.Error(exception) } // We must commit the database transaction before returning from this closure otherwise Quasar may schedule diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt index 112b65ace0..19e1284f83 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/SingleThreadedStateMachineManager.kt @@ -43,6 +43,7 @@ import net.corda.nodeapi.internal.persistence.wrapWithDatabaseTransaction import net.corda.serialization.internal.CheckpointSerializeAsTokenContextImpl import net.corda.serialization.internal.withTokenContext import org.apache.activemq.artemis.utils.ReusableLatch +import org.apache.logging.log4j.LogManager import rx.Observable import rx.subjects.PublishSubject import java.security.SecureRandom @@ -135,7 +136,13 @@ class SingleThreadedStateMachineManager( val fibers = restoreFlowsFromCheckpoints() metrics.register("Flows.InFlight", Gauge { mutex.content.flows.size }) Fiber.setDefaultUncaughtExceptionHandler { fiber, throwable -> - (fiber as FlowStateMachineImpl<*>).logger.warn("Caught exception from flow", throwable) + if (throwable is VirtualMachineError) { + (fiber as FlowStateMachineImpl<*>).logger.error("Caught unrecoverable error from flow. Forcibly terminating the JVM, this might leave resources open, and most likely will.", throwable) + LogManager.shutdown(true) + Runtime.getRuntime().halt(1) + } else { + (fiber as FlowStateMachineImpl<*>).logger.warn("Caught exception from flow", throwable) + } } serviceHub.networkMapCache.nodeReady.then { logger.info("Node ready, info: ${serviceHub.myInfo}") @@ -606,7 +613,7 @@ class SingleThreadedStateMachineManager( private fun deserializeCheckpoint(serializedCheckpoint: SerializedBytes): Checkpoint? { return try { serializedCheckpoint.checkpointDeserialize(context = checkpointSerializationContext!!) - } catch (exception: Throwable) { + } catch (exception: Exception) { logger.error("Encountered unrestorable checkpoint!", exception) null } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/TransitionExecutorImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/TransitionExecutorImpl.kt index f8fe721679..d781bda14a 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/TransitionExecutorImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/TransitionExecutorImpl.kt @@ -39,7 +39,7 @@ class TransitionExecutorImpl( for (action in transition.actions) { try { actionExecutor.executeAction(fiber, action) - } catch (exception: Throwable) { + } catch (exception: Exception) { contextTransactionOrNull?.close() if (transition.newState.checkpoint.errorState is ErrorState.Errored) { // If we errored while transitioning to an error state then we cannot record the additional diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/interceptors/FiberDeserializationCheckingInterceptor.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/interceptors/FiberDeserializationCheckingInterceptor.kt index 37033977de..463d5bf10c 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/interceptors/FiberDeserializationCheckingInterceptor.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/interceptors/FiberDeserializationCheckingInterceptor.kt @@ -77,8 +77,8 @@ class FiberDeserializationChecker { is Job.Check -> { try { job.serializedFiber.checkpointDeserialize(context = checkpointSerializationContext) - } catch (throwable: Throwable) { - log.error("Encountered unrestorable checkpoint!", throwable) + } catch (exception: Exception) { + log.error("Encountered unrestorable checkpoint!", exception) foundUnrestorableFibers = true } } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index 5bc7e6b287..edf277fcdf 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -276,7 +276,7 @@ class NodeVaultServiceTest { assertThat(vaultService.queryBy(criteriaByLockId1).states).hasSize(3) } println("SOFT LOCK STATES #1 succeeded") - } catch (e: Throwable) { + } catch (e: Exception) { println("SOFT LOCK STATES #1 failed") } finally { countDown.countDown() @@ -292,7 +292,7 @@ class NodeVaultServiceTest { assertThat(vaultService.queryBy(criteriaByLockId2).states).hasSize(3) } println("SOFT LOCK STATES #2 succeeded") - } catch (e: Throwable) { + } catch (e: Exception) { println("SOFT LOCK STATES #2 failed") } finally { countDown.countDown() diff --git a/node/src/test/kotlin/net/corda/node/utilities/TLSAuthenticationTests.kt b/node/src/test/kotlin/net/corda/node/utilities/TLSAuthenticationTests.kt index 463b6511cd..9be77927f9 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/TLSAuthenticationTests.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/TLSAuthenticationTests.kt @@ -327,7 +327,7 @@ class TLSAuthenticationTests { lock.notifyAll() } sslServerSocket.close() - } catch (ex: Throwable) { + } catch (ex: Exception) { serverError = true } } diff --git a/samples/irs-demo/web/src/main/kotlin/net/corda/irs/web/api/InterestSwapRestAPI.kt b/samples/irs-demo/web/src/main/kotlin/net/corda/irs/web/api/InterestSwapRestAPI.kt index 09cba2437c..c62ff2aa30 100644 --- a/samples/irs-demo/web/src/main/kotlin/net/corda/irs/web/api/InterestSwapRestAPI.kt +++ b/samples/irs-demo/web/src/main/kotlin/net/corda/irs/web/api/InterestSwapRestAPI.kt @@ -65,7 +65,7 @@ class InterestRateSwapAPI { return try { rpc.startFlow(AutoOfferFlow::Requester, newDeal).returnValue.getOrThrow() ResponseEntity.created(URI.create(generateDealLink(newDeal))).build() - } catch (ex: Throwable) { + } catch (ex: Exception) { logger.info("Exception when creating deal: $ex", ex) ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.toString()) } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt index 0cea2003f7..7bc06c5a24 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt @@ -14,6 +14,7 @@ import org.apache.qpid.proton.amqp.UnsignedInteger import org.apache.qpid.proton.codec.Data import java.io.InputStream import java.io.NotSerializableException +import java.lang.Exception import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import java.lang.reflect.TypeVariable @@ -100,8 +101,8 @@ class DeserializationInput constructor( throw NotSerializableException(amqp.mitigation) } catch (nse: NotSerializableException) { throw nse - } catch (t: Throwable) { - throw NotSerializableException("Internal deserialization failure: ${t.javaClass.name}: ${t.message}").apply { initCause(t) } + } catch (e: Exception) { + throw NotSerializableException("Internal deserialization failure: ${e.javaClass.name}: ${e.message}").apply { initCause(e) } } finally { objectHistory.clear() } diff --git a/settings.gradle b/settings.gradle index 51a5677ce4..ddbe185474 100644 --- a/settings.gradle +++ b/settings.gradle @@ -21,7 +21,6 @@ include 'experimental' include 'experimental:avalanche' include 'experimental:behave' include 'experimental:quasar-hook' -include 'experimental:kryo-hook' include 'experimental:corda-utils' include 'experimental:notary-raft' include 'experimental:notary-bft-smart' diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt index 4795c190a0..87185cc923 100644 --- a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt @@ -167,8 +167,8 @@ class DriverTests { fun `driver waits for in-process nodes to finish`() { fun NodeHandle.stopQuietly() = try { stop() - } catch (t: Throwable) { - t.printStackTrace() + } catch (e: Exception) { + e.printStackTrace() } val handlesFuture = openFuture>() diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt index 1346e22276..e052e2b36c 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt @@ -95,8 +95,8 @@ fun poll( } else { executorService.schedule(this, pollInterval.toMillis(), TimeUnit.MILLISECONDS) } - } catch (t: Throwable) { - resultFuture.setException(t) + } catch (e: Exception) { + resultFuture.setException(e) } } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ShutdownManager.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ShutdownManager.kt index f7cd8f7823..3d043e186d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ShutdownManager.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ShutdownManager.kt @@ -65,6 +65,9 @@ class ShutdownManager(private val executorService: ExecutorService) { it.value() } catch (t: Throwable) { log.warn("Exception while calling a shutdown action, this might create resource leaks", t) + if (t is VirtualMachineError) { + throw t + } } is Try.Failure -> log.warn("Exception while getting shutdown method, disregarding", it.exception) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/performance/Injectors.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/performance/Injectors.kt index 0e73bbf070..4e173b657c 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/performance/Injectors.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/performance/Injectors.kt @@ -86,8 +86,8 @@ fun startPublishingFixedRateInjector( ) } } - } catch (throwable: Throwable) { - throwable.printStackTrace() + } catch (e: Exception) { + e.printStackTrace() } } } diff --git a/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt b/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt index 8d8e762ed7..7cdedde753 100644 --- a/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt +++ b/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt @@ -110,7 +110,7 @@ class BlobInspector : CordaCliWrapper("blob-inspector", "Convert AMQP serialised } else { null // Not an AMQP blob. } - } catch (t: Throwable) { + } catch (e: Exception) { return null // Failed to parse in some other way. } } diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTest.kt index 0f4fade46c..152eda8997 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTest.kt @@ -106,10 +106,10 @@ data class LoadTest( log.info("Executing $it") try { nodes.execute(it) - } catch (exception: Throwable) { - val diagnostic = executeDiagnostic(state, newState, it, exception) + } catch (throwable: Throwable) { + val diagnostic = executeDiagnostic(state, newState, it, throwable) log.error(diagnostic) - throw Exception(diagnostic) + throw if (throwable is Exception) Exception(diagnostic) else throwable } } } diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/instance/docker/DockerInstantiator.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/instance/docker/DockerInstantiator.kt index 0f29838407..eaf0bf6882 100644 --- a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/instance/docker/DockerInstantiator.kt +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/containers/instance/docker/DockerInstantiator.kt @@ -30,13 +30,13 @@ class DockerInstantiator(private val volume: LocalVolume, try { localClient.killContainerCmd(container.id).exec() LOG.info("Found running container: $instanceName killed") - } catch (e: Throwable) { + } catch (e: Exception) { //container not running } try { localClient.removeContainerCmd(container.id).exec() LOG.info("Found existing container: $instanceName removed") - } catch (e: Throwable) { + } catch (e: Exception) { //this *only* occurs of the container had been previously scheduled for removal //but did not complete before this attempt was begun. } diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeFinder.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeFinder.kt index 4de42a5224..1f4cd1adbd 100644 --- a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeFinder.kt +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeFinder.kt @@ -1,5 +1,6 @@ package net.corda.bootstrapper.nodes +import com.typesafe.config.ConfigException import com.typesafe.config.ConfigFactory import net.corda.bootstrapper.Constants import net.corda.core.utilities.contextLogger @@ -11,7 +12,7 @@ class NodeFinder(private val scratchDir: File) { return scratchDir.walkBottomUp().filter { it.name == "node.conf" && !it.absolutePath.contains(Constants.BOOTSTRAPPER_DIR_NAME) }.map { try { ConfigFactory.parseFile(it) to it - } catch (t: Throwable) { + } catch (e: ConfigException) { null } }.filterNotNull() diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/notaries/NotaryFinder.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/notaries/NotaryFinder.kt index d4ae23d072..2801141031 100644 --- a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/notaries/NotaryFinder.kt +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/notaries/NotaryFinder.kt @@ -1,5 +1,6 @@ package net.corda.bootstrapper.notaries +import com.typesafe.config.ConfigException import com.typesafe.config.ConfigFactory import net.corda.bootstrapper.Constants import net.corda.bootstrapper.nodes.FoundNode @@ -12,7 +13,7 @@ class NotaryFinder(private val dirToSearch: File) { .map { try { ConfigFactory.parseFile(it) to it - } catch (t: Throwable) { + } catch (e: ConfigException) { null } }.filterNotNull() diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/volumes/azure/AzureSmbVolume.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/volumes/azure/AzureSmbVolume.kt index 185868bbf1..7c85080729 100644 --- a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/volumes/azure/AzureSmbVolume.kt +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/volumes/azure/AzureSmbVolume.kt @@ -45,7 +45,7 @@ class AzureSmbVolume(private val azure: Azure, private val resourceGroup: Resour cloudFileShare.createIfNotExists() networkParamsFolder.createIfNotExists() break - } catch (e: Throwable) { + } catch (e: Exception) { LOG.debug("storage account not ready, waiting") Thread.sleep(5000) } From bb0ed9e98cd8f7e047d4fadbd50f43acc2378e94 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Mon, 12 Nov 2018 17:15:19 +0000 Subject: [PATCH 15/24] Update CONTRIBUTORS.md --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index f2add88db0..e7b03d89e0 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -121,6 +121,7 @@ see changes to this list. * Lulu Ren (Monad-Labs) * Maksymilian Pawlak (R3) * Manila Gauns (Persistent Systems Limited) +* Manos Batsis * Marek Scocovsky (ABSA) * marekdapps * Mark Lauer (Westpac) From 369f23e306866bb9db2e9052937b6a5cf12fb17d Mon Sep 17 00:00:00 2001 From: Anthony Keenan Date: Mon, 12 Nov 2018 17:22:22 +0000 Subject: [PATCH 16/24] CORDA-1817: Fix issue with misleading capsule error messages (#4215) * Make caplet respect all acceptable combinations of cmd line parameters * If cordapp dir fails to create, still run corda.jar and let it fail gracefully * Don't parse additional options as the parameter for the previous one. * Remove commented lines --- node/build.gradle | 1 + node/src/main/java/CordaCaplet.java | 44 ++++++++++++++++--- ...CapletBaseDirectoryParsingFailureTest.java | 39 ++++++++++++++++ .../CordaCapletBaseDirectoryParsingTest.java | 39 ++++++++++++++++ ...rdaCapletConfigFileParsingFailureTest.java | 40 +++++++++++++++++ .../CordaCapletConfigFileParsingTest.java | 40 +++++++++++++++++ node/src/test/java/CordaCapletTestUtils.java | 16 +++++++ 7 files changed, 212 insertions(+), 7 deletions(-) create mode 100644 node/src/test/java/CordaCapletBaseDirectoryParsingFailureTest.java create mode 100644 node/src/test/java/CordaCapletBaseDirectoryParsingTest.java create mode 100644 node/src/test/java/CordaCapletConfigFileParsingFailureTest.java create mode 100644 node/src/test/java/CordaCapletConfigFileParsingTest.java create mode 100644 node/src/test/java/CordaCapletTestUtils.java diff --git a/node/build.gradle b/node/build.gradle index 8528de21a4..e540562221 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -151,6 +151,7 @@ dependencies { // Capsule is a library for building independently executable fat JARs. // We only need this dependency to compile our Caplet against. compileOnly "co.paralleluniverse:capsule:$capsule_version" + testCompile "co.paralleluniverse:capsule:$capsule_version" // OkHTTP: Simple HTTP library. compile "com.squareup.okhttp3:okhttp:$okhttp_version" diff --git a/node/src/main/java/CordaCaplet.java b/node/src/main/java/CordaCaplet.java index da1e3b9ce4..78ccce99af 100644 --- a/node/src/main/java/CordaCaplet.java +++ b/node/src/main/java/CordaCaplet.java @@ -20,8 +20,7 @@ public class CordaCaplet extends Capsule { } private Config parseConfigFile(List args) { - String baseDirOption = getOption(args, "--base-directory"); - this.baseDir = Paths.get((baseDirOption == null) ? "." : baseDirOption).toAbsolutePath().normalize().toString(); + this.baseDir = getBaseDirectory(args); String config = getOption(args, "--config-file"); File configFile = (config == null) ? new File(baseDir, "node.conf") : new File(config); try { @@ -36,17 +35,44 @@ public class CordaCaplet extends Capsule { } } + File getConfigFile(List args, String baseDir) { + String config = getOptionMultiple(args, Arrays.asList("--config-file", "-f")); + return (config == null || config.equals("")) ? new File(baseDir, "node.conf") : new File(config); + } + + String getBaseDirectory(List args) { + String baseDir = getOptionMultiple(args, Arrays.asList("--base-directory", "-b")); + return Paths.get((baseDir == null) ? "." : baseDir).toAbsolutePath().normalize().toString(); + } + + private String getOptionMultiple(List args, List possibleOptions) { + String result = null; + for(String option: possibleOptions) { + result = getOption(args, option); + if (result != null) break; + } + return result; + } + private String getOption(List args, String option) { final String lowerCaseOption = option.toLowerCase(); int index = 0; for (String arg : args) { if (arg.toLowerCase().equals(lowerCaseOption)) { - if (index < args.size() - 1) { + if (index < args.size() - 1 && !args.get(index + 1).startsWith("-")) { return args.get(index + 1); } else { return null; } } + + if (arg.toLowerCase().startsWith(lowerCaseOption)) { + if (arg.length() > option.length() && arg.substring(option.length(), option.length() + 1).equals("=")) { + return arg.substring(option.length() + 1); + } else { + return null; + } + } index++; } return null; @@ -82,7 +108,10 @@ public class CordaCaplet extends Capsule { File cordappsDir = new File(baseDir, "cordapps"); // Create cordapps directory if it doesn't exist. - requireCordappsDirExists(cordappsDir); + if (!checkIfCordappDirExists(cordappsDir)) { + // If it fails, just return the existing class path. The main Corda jar will detect the error and fail gracefully. + return cp; + } // Add additional directories of JARs to the classpath (at the end), e.g., for JDBC drivers. augmentClasspath((List) cp, cordappsDir); try { @@ -152,17 +181,18 @@ public class CordaCaplet extends Capsule { } } - private void requireCordappsDirExists(File dir) { + private Boolean checkIfCordappDirExists(File dir) { try { if (!dir.mkdir() && !dir.exists()) { // It is unlikely to enter this if-branch, but just in case. logOnFailedCordappDir(); - throw new RuntimeException("Cordapps dir could not be created"); // Let Capsule handle the error (log error, clean up, die). + return false; } } catch (SecurityException | NullPointerException e) { logOnFailedCordappDir(); - throw e; // Let Capsule handle the error (log error, clean up, die). + return false; } + return true; } private void logOnFailedCordappDir() { diff --git a/node/src/test/java/CordaCapletBaseDirectoryParsingFailureTest.java b/node/src/test/java/CordaCapletBaseDirectoryParsingFailureTest.java new file mode 100644 index 0000000000..21c672d2e2 --- /dev/null +++ b/node/src/test/java/CordaCapletBaseDirectoryParsingFailureTest.java @@ -0,0 +1,39 @@ +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.Assert.assertEquals; + +@RunWith(Parameterized.class) +public class CordaCapletBaseDirectoryParsingFailureTest { + @Parameterized.Parameters + public static Collection CombinationsToTest() { + return Arrays.asList( + new Object[][]{ + {new String[]{"--base-directory", "--another-option"}}, + {new String[]{"--base-directory=", "-a"}}, + {new String[]{"-b", "--another-option"}}, + {new String[]{"-b=", "-a"}} + } + ); + } + + private String[] cmdLineArguments; + + public CordaCapletBaseDirectoryParsingFailureTest(String[] baseOption) { + this.cmdLineArguments = baseOption; + } + + @Test + public void testThatBaseDirectoryFallsBackToCurrentWhenBaseDirectoryIsNotSupplied() { + final CordaCaplet caplet = CordaCapletTestUtils.getCaplet(); + final String returnPath = caplet.getBaseDirectory(Arrays.asList(cmdLineArguments)); + final String expected = Paths.get(".").toAbsolutePath().normalize().toString(); + assertEquals(expected, returnPath); + } +} + diff --git a/node/src/test/java/CordaCapletBaseDirectoryParsingTest.java b/node/src/test/java/CordaCapletBaseDirectoryParsingTest.java new file mode 100644 index 0000000000..489e59f3e5 --- /dev/null +++ b/node/src/test/java/CordaCapletBaseDirectoryParsingTest.java @@ -0,0 +1,39 @@ + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.Assert.assertEquals; + +@RunWith(Parameterized.class) +public class CordaCapletBaseDirectoryParsingTest { + @Parameterized.Parameters + public static Collection CombinationsToTest() { + return Arrays.asList( + new Object[][]{ + {new String[]{"--base-directory", "blah"}}, + {new String[]{"--base-directory=blah"}}, + {new String[]{"-b", "blah"}}, + {new String[]{"-b=blah"}} + }); + } + + private String[] cmdLineArguments; + + public CordaCapletBaseDirectoryParsingTest(String[] arr) { + this.cmdLineArguments = arr; + } + + @Test + public void testThatBaseDirectoryParameterIsRecognised() { + final CordaCaplet caplet = CordaCapletTestUtils.getCaplet(); + final String returnPath = caplet.getBaseDirectory(Arrays.asList(cmdLineArguments)); + final String expected = Paths.get(".").resolve("blah").toAbsolutePath().normalize().toString(); + assertEquals(expected, returnPath); + } +} + diff --git a/node/src/test/java/CordaCapletConfigFileParsingFailureTest.java b/node/src/test/java/CordaCapletConfigFileParsingFailureTest.java new file mode 100644 index 0000000000..b94e137eb5 --- /dev/null +++ b/node/src/test/java/CordaCapletConfigFileParsingFailureTest.java @@ -0,0 +1,40 @@ +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.io.File; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.Assert.assertEquals; + +@RunWith(Parameterized.class) +public class CordaCapletConfigFileParsingFailureTest { + @Parameterized.Parameters + public static Collection CombinationsToTest() { + return Arrays.asList( + new Object[][]{ + {new String[]{"--config-file", "--another-option"}}, + {new String[]{"--config-file=", "-a"}}, + {new String[]{"-f", "--another-option"}}, + {new String[]{"-f=", "-a"}} + } + ); + } + + private String[] cmdLineArguments; + + public CordaCapletConfigFileParsingFailureTest(String[] baseOption) { + this.cmdLineArguments = baseOption; + } + + @Test + public void testThatBaseDirectoryFallsBackToDefaultWhenConfigFileIsNotSupplied() { + final CordaCaplet caplet = CordaCapletTestUtils.getCaplet(); + final File returnPath = caplet.getConfigFile(Arrays.asList(cmdLineArguments), CordaCapletTestUtils.getBaseDir()); + final File expected = Paths.get(".").resolve("node.conf").toAbsolutePath().normalize().toFile(); + assertEquals(expected, returnPath); + } +} + diff --git a/node/src/test/java/CordaCapletConfigFileParsingTest.java b/node/src/test/java/CordaCapletConfigFileParsingTest.java new file mode 100644 index 0000000000..48ddf1766d --- /dev/null +++ b/node/src/test/java/CordaCapletConfigFileParsingTest.java @@ -0,0 +1,40 @@ + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.io.File; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.Assert.assertEquals; + +@RunWith(Parameterized.class) +public class CordaCapletConfigFileParsingTest { + @Parameterized.Parameters + public static Collection CombinationsToTest() { + return Arrays.asList( + new Object[][]{ + {new String[]{"--config-file", "blah.conf"}}, + {new String[]{"--config-file=blah.conf"}}, + {new String[]{"-f", "blah.conf"}}, + {new String[]{"-f=blah.conf"}} + }); + } + + private String[] cmdLineArguments; + + public CordaCapletConfigFileParsingTest(String[] arr) { + this.cmdLineArguments = arr; + } + + @Test + public void testThatConfigFileParameterIsRecognised() { + final CordaCaplet caplet = CordaCapletTestUtils.getCaplet(); + final File returnPath = caplet.getConfigFile(Arrays.asList(cmdLineArguments), CordaCapletTestUtils.getBaseDir()); + final File expected = Paths.get(".").resolve("blah.conf").toAbsolutePath().normalize().toFile(); + assertEquals(expected, returnPath.getAbsoluteFile()); + } +} + diff --git a/node/src/test/java/CordaCapletTestUtils.java b/node/src/test/java/CordaCapletTestUtils.java new file mode 100644 index 0000000000..cb282365dd --- /dev/null +++ b/node/src/test/java/CordaCapletTestUtils.java @@ -0,0 +1,16 @@ +import java.io.File; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Objects; + +class CordaCapletTestUtils { + static CordaCaplet getCaplet() { + final String path = System.getProperty("user.dir") + File.separator + "build" + File.separator + "libs" + File.separator; + final File jar = Arrays.stream(Objects.requireNonNull(new File(path).listFiles())).filter(x -> x.getName().startsWith("corda-node") && x.getName().endsWith(".jar")).findFirst().get(); + return new CordaCaplet(new Capsule(jar.toPath())); + } + + static String getBaseDir() { + return Paths.get(".").toAbsolutePath().normalize().toString(); + } +} From 1c012f6403eb392778e16e94f11fd962b9f0039a Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Mon, 12 Nov 2018 18:38:47 +0000 Subject: [PATCH 17/24] Back porting clean up of FlowFrameworkTests.kt made in ENT (#4218) --- .../net/corda/node/internal/AbstractNode.kt | 13 +- .../FlowFrameworkPersistenceTests.kt | 166 +++++ .../statemachine/FlowFrameworkTests.kt | 577 +++++------------- .../FlowFrameworkTripartyTests.kt | 178 ++++++ 4 files changed, 488 insertions(+), 446 deletions(-) create mode 100644 node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkPersistenceTests.kt create mode 100644 node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTripartyTests.kt 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 d48580b5e5..6ff5306a4a 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -9,9 +9,9 @@ import net.corda.confidential.SwapIdentitiesHandler import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture import net.corda.core.context.InvocationContext -import net.corda.core.crypto.internal.AliasPrivateKey import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.internal.AliasPrivateKey import net.corda.core.crypto.newSecureRandom import net.corda.core.flows.* import net.corda.core.identity.AbstractParty @@ -122,14 +122,14 @@ abstract class AbstractNode(val configuration: NodeConfiguration, cacheFactoryPrototype: BindableNamedCacheFactory, protected val versionInfo: VersionInfo, protected val flowManager: FlowManager, - protected val serverThread: AffinityExecutor.ServiceAffinityExecutor, - private val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() { + val serverThread: AffinityExecutor.ServiceAffinityExecutor, + val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() { protected abstract val log: Logger @Suppress("LeakingThis") private var tokenizableServices: MutableList? = mutableListOf(platformClock, this) - protected val metricRegistry = MetricRegistry() + val metricRegistry = MetricRegistry() protected val cacheFactory = cacheFactoryPrototype.bindWithConfig(configuration).bindWithMetrics(metricRegistry).tokenize() val monitoringService = MonitoringService(metricRegistry).tokenize() @@ -146,7 +146,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } - protected val cordappLoader: CordappLoader = makeCordappLoader(configuration, versionInfo) + val cordappLoader: CordappLoader = makeCordappLoader(configuration, versionInfo) val schemaService = NodeSchemaService(cordappLoader.cordappSchemas).tokenize() val identityService = PersistentIdentityService(cacheFactory).tokenize() val database: CordaPersistence = createCordaPersistence( @@ -777,7 +777,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, // Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because // the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with // the identity key. But the infrastructure to make that easy isn't here yet. - return BasicHSMKeyManagementService(cacheFactory,identityService, database, cryptoService) + return BasicHSMKeyManagementService(cacheFactory, identityService, database, cryptoService) } open fun stop() { @@ -1008,7 +1008,6 @@ class FlowStarterImpl(private val smm: StateMachineManager, private val flowLogi private val _future = openFuture>() override val future: CordaFuture> get() = _future - } return startFlow(startFlowEvent) } diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkPersistenceTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkPersistenceTests.kt new file mode 100644 index 0000000000..8a9156af4a --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkPersistenceTests.kt @@ -0,0 +1,166 @@ +package net.corda.node.services.statemachine + +import net.corda.core.crypto.random63BitValue +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.registerCordappFlowFactory +import net.corda.core.identity.Party +import net.corda.core.utilities.getOrThrow +import net.corda.node.services.persistence.checkpoints +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.CHARLIE_NAME +import net.corda.testing.core.singleIdentity +import net.corda.testing.internal.LogHelper +import net.corda.testing.node.InMemoryMessagingNetwork +import net.corda.testing.node.internal.* +import org.assertj.core.api.Assertions.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Ignore +import org.junit.Test +import rx.Observable +import java.util.* +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class FlowFrameworkPersistenceTests { + companion object { + init { + LogHelper.setLevel("+net.corda.flow") + } + } + + private lateinit var mockNet: InternalMockNetwork + private val receivedSessionMessages = ArrayList() + private lateinit var aliceNode: TestStartedNode + private lateinit var bobNode: TestStartedNode + private lateinit var notaryIdentity: Party + private lateinit var alice: Party + private lateinit var bob: Party + private lateinit var aliceFlowManager: MockNodeFlowManager + private lateinit var bobFlowManager: MockNodeFlowManager + + @Before + fun start() { + mockNet = InternalMockNetwork( + cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"), + servicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin() + ) + aliceFlowManager = MockNodeFlowManager() + bobFlowManager = MockNodeFlowManager() + + aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME, flowManager = aliceFlowManager)) + bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, flowManager = bobFlowManager)) + + receivedSessionMessagesObservable().forEach { receivedSessionMessages += it } + + // Extract identities + alice = aliceNode.info.singleIdentity() + bob = bobNode.info.singleIdentity() + notaryIdentity = mockNet.defaultNotaryIdentity + } + + @After + fun cleanUp() { + mockNet.stopNodes() + receivedSessionMessages.clear() + } + + @Test + fun `newly added flow is preserved on restart`() { + aliceNode.services.startFlow(NoOpFlow(nonTerminating = true)) + aliceNode.internals.acceptableLiveFiberCountOnStop = 1 + val restoredFlow = aliceNode.restartAndGetRestoredFlow() + assertThat(restoredFlow.flowStarted).isTrue() + } + + @Test + fun `flow restarted just after receiving payload`() { + bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it) + .nonTerminating() } + aliceNode.services.startFlow(SendFlow("Hello", bob)) + + // We push through just enough messages to get only the payload sent + bobNode.pumpReceive() + bobNode.internals.disableDBCloseOnStop() + bobNode.internals.acceptableLiveFiberCountOnStop = 1 + bobNode.dispose() + mockNet.runNetwork() + val restoredFlow = bobNode.restartAndGetRestoredFlow() + assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello") + } + + @Test + fun `flow loaded from checkpoint will respond to messages from before start`() { + aliceNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) } + bobNode.services.startFlow(ReceiveFlow(alice).nonTerminating()) // Prepare checkpointed receive flow + val restoredFlow = bobNode.restartAndGetRestoredFlow() + assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello") + } + + @Ignore("Some changes in startup order make this test's assumptions fail.") + @Test + fun `flow with send will resend on interrupted restart`() { + val payload = random63BitValue() + val payload2 = random63BitValue() + + var sentCount = 0 + mockNet.messagingNetwork.sentMessages.toSessionTransfers().filter { it.isPayloadTransfer }.forEach { sentCount++ } + val charlieNode = mockNet.createNode(InternalMockNodeParameters(legalName = CHARLIE_NAME)) + val secondFlow = charlieNode.registerCordappFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) } + mockNet.runNetwork() + val charlie = charlieNode.info.singleIdentity() + + // Kick off first send and receive + bobNode.services.startFlow(PingPongFlow(charlie, payload)) + bobNode.database.transaction { + assertEquals(1, bobNode.internals.checkpointStorage.checkpoints().size) + } + // Make sure the add() has finished initial processing. + bobNode.internals.disableDBCloseOnStop() + // Restart node and thus reload the checkpoint and resend the message with same UUID + bobNode.dispose() + bobNode.database.transaction { + assertEquals(1, bobNode.internals.checkpointStorage.checkpoints().size) // confirm checkpoint + bobNode.services.networkMapCache.clearNetworkMapCache() + } + val node2b = mockNet.createNode(InternalMockNodeParameters(bobNode.internals.id)) + bobNode.internals.manuallyCloseDB() + val (firstAgain, fut1) = node2b.getSingleFlow() + // Run the network which will also fire up the second flow. First message should get deduped. So message data stays in sync. + mockNet.runNetwork() + fut1.getOrThrow() + + val receivedCount = receivedSessionMessages.count { it.isPayloadTransfer } + // Check flows completed cleanly and didn't get out of phase + assertEquals(4, receivedCount, "Flow should have exchanged 4 unique messages")// Two messages each way + // can't give a precise value as every addMessageHandler re-runs the undelivered messages + assertTrue(sentCount > receivedCount, "Node restart should have retransmitted messages") + node2b.database.transaction { + assertEquals(0, node2b.internals.checkpointStorage.checkpoints().size, "Checkpoints left after restored flow should have ended") + } + charlieNode.database.transaction { + assertEquals(0, charlieNode.internals.checkpointStorage.checkpoints().size, "Checkpoints left after restored flow should have ended") + } + assertEquals(payload2, firstAgain.receivedPayload, "Received payload does not match the first value on Node 3") + assertEquals(payload2 + 1, firstAgain.receivedPayload2, "Received payload does not match the expected second value on Node 3") + assertEquals(payload, secondFlow.getOrThrow().receivedPayload, "Received payload does not match the (restarted) first value on Node 2") + assertEquals(payload + 1, secondFlow.getOrThrow().receivedPayload2, "Received payload does not match the expected second value on Node 2") + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + //region Helpers + + private inline fun > TestStartedNode.restartAndGetRestoredFlow(): P { + val newNode = mockNet.restartNode(this) + newNode.internals.acceptableLiveFiberCountOnStop = 1 + mockNet.runNetwork() + return newNode.getSingleFlow

().first + } + + private fun receivedSessionMessagesObservable(): Observable { + return mockNet.messagingNetwork.receivedMessages.toSessionTransfers() + } + + //endregion Helpers +} 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 53ec7d9f2a..23b919eb66 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 @@ -40,16 +40,13 @@ import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType import org.junit.After import org.junit.Before -import org.junit.Ignore import org.junit.Test import rx.Notification import rx.Observable import java.time.Instant import java.util.* import kotlin.reflect.KClass -import kotlin.test.assertEquals import kotlin.test.assertFailsWith -import kotlin.test.assertTrue class FlowFrameworkTests { companion object { @@ -449,320 +446,142 @@ class FlowFrameworkTests { private val normalEnd = ExistingSessionMessage(SessionId(0), EndSessionMessage) // NormalSessionEnd(0) - private fun TestStartedNode.sendSessionMessage(message: SessionMessage, destination: Party) { - services.networkService.apply { - val address = getAddressOfParty(PartyInfo.SingleNode(destination, emptyList())) - send(createMessage(FlowMessagingImpl.sessionTopic, message.serialize().bytes), address) - } - } - private fun assertSessionTransfers(vararg expected: SessionTransfer) { assertThat(receivedSessionMessages).containsExactly(*expected) } - //endregion Helpers -} + private val FlowLogic<*>.progressSteps: CordaFuture>> + get() { + return progressTracker!!.changes + .ofType(Change.Position::class.java) + .map { it.newStep } + .materialize() + .toList() + .toFuture() + } -class FlowFrameworkTripartyTests { + @InitiatingFlow + private class WaitForOtherSideEndBeforeSendAndReceive(val otherParty: Party, + @Transient val receivedOtherFlowEnd: Semaphore) : FlowLogic() { + @Suspendable + override fun call() { + // Kick off the flow on the other side ... + val session = initiateFlow(otherParty) + session.send(1) + // ... then pause this one until it's received the session-end message from the other side + receivedOtherFlowEnd.acquire() + session.sendAndReceive(2) + } + } - companion object { + // we need brand new class for a flow to fail, so here it is + @InitiatingFlow + private open class NeverRegisteredFlow(val payload: Any, vararg val otherParties: Party) : FlowLogic() { init { - LogHelper.setLevel("+net.corda.flow") + require(otherParties.isNotEmpty()) } - private lateinit var mockNet: InternalMockNetwork - private lateinit var aliceNode: TestStartedNode - private lateinit var bobNode: TestStartedNode - private lateinit var charlieNode: TestStartedNode - private lateinit var alice: Party - private lateinit var bob: Party - private lateinit var charlie: Party - private lateinit var notaryIdentity: Party - private val receivedSessionMessages = ArrayList() - } - - @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)) - - - // 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`() { - bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } - charlieNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } - val payload = "Hello World" - aliceNode.services.startFlow(SendFlow(payload, bob, charlie)) - mockNet.runNetwork() - bobNode.internals.acceptableLiveFiberCountOnStop = 1 - charlieNode.internals.acceptableLiveFiberCountOnStop = 1 - val bobFlow = bobNode.getSingleFlow().first - val charlieFlow = charlieNode.getSingleFlow().first - assertThat(bobFlow.receivedPayloads[0]).isEqualTo(payload) - assertThat(charlieFlow.receivedPayloads[0]).isEqualTo(payload) - - assertSessionTransfers(bobNode, - aliceNode sent sessionInit(SendFlow::class, payload = payload) to bobNode, - bobNode sent sessionConfirm() to aliceNode, - aliceNode sent normalEnd to bobNode - //There's no session end from the other flows as they're manually suspended - ) - - assertSessionTransfers(charlieNode, - aliceNode sent sessionInit(SendFlow::class, payload = payload) to charlieNode, - charlieNode sent sessionConfirm() to aliceNode, - aliceNode sent normalEnd to charlieNode - //There's no session end from the other flows as they're manually suspended - ) - } - - @Test - fun `receiving from multiple parties`() { - val bobPayload = "Test 1" - val charliePayload = "Test 2" - bobNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(bobPayload, it) } - charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(charliePayload, it) } - val multiReceiveFlow = ReceiveFlow(bob, charlie).nonTerminating() - aliceNode.services.startFlow(multiReceiveFlow) - aliceNode.internals.acceptableLiveFiberCountOnStop = 1 - mockNet.runNetwork() - assertThat(multiReceiveFlow.receivedPayloads[0]).isEqualTo(bobPayload) - assertThat(multiReceiveFlow.receivedPayloads[1]).isEqualTo(charliePayload) - - assertSessionTransfers(bobNode, - aliceNode sent sessionInit(ReceiveFlow::class) to bobNode, - bobNode sent sessionConfirm() to aliceNode, - bobNode sent sessionData(bobPayload) to aliceNode, - bobNode sent normalEnd to aliceNode - ) - - assertSessionTransfers(charlieNode, - aliceNode sent sessionInit(ReceiveFlow::class) to charlieNode, - charlieNode sent sessionConfirm() to aliceNode, - charlieNode sent sessionData(charliePayload) to aliceNode, - charlieNode sent normalEnd to aliceNode - ) - } - - @Test - fun `FlowException only propagated to parent`() { - charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } } - bobNode.registerCordappFlowFactory(ReceiveFlow::class) { ReceiveFlow(charlie) } - val receivingFiber = aliceNode.services.startFlow(ReceiveFlow(bob)) - mockNet.runNetwork() - assertThatExceptionOfType(UnexpectedFlowEndException::class.java) - .isThrownBy { receivingFiber.resultFuture.getOrThrow() } - } - - @Test - fun `FlowException thrown and there is a 3rd unrelated party flow`() { - // Bob will send its payload and then block waiting for the receive from Alice. Meanwhile Alice will move - // onto Charlie which will throw the exception - val node2Fiber = bobNode - .registerCordappFlowFactory(ReceiveFlow::class) { SendAndReceiveFlow(it, "Hello") } - .map { it.stateMachine } - charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } } - - val aliceFiber = aliceNode.services.startFlow(ReceiveFlow(bob, charlie)) as FlowStateMachineImpl - mockNet.runNetwork() - - // Alice will terminate with the error it received from Charlie but it won't propagate that to Bob (as it's - // not relevant to it) but it will end its session with it - assertThatExceptionOfType(MyFlowException::class.java).isThrownBy { - aliceFiber.resultFuture.getOrThrow() - } - val bobResultFuture = node2Fiber.getOrThrow().resultFuture - assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy { - bobResultFuture.getOrThrow() - } - - assertSessionTransfers(bobNode, - aliceNode sent sessionInit(ReceiveFlow::class) to bobNode, - bobNode sent sessionConfirm() to aliceNode, - bobNode sent sessionData("Hello") to aliceNode, - aliceNode sent errorMessage() to bobNode - ) - } - - private val normalEnd = ExistingSessionMessage(SessionId(0), EndSessionMessage) // NormalSessionEnd(0) - - private fun assertSessionTransfers(node: TestStartedNode, vararg expected: SessionTransfer): List { - val actualForNode = receivedSessionMessages.filter { it.from == node.internals.id || it.to == node.network.myAddress } - assertThat(actualForNode).containsExactly(*expected) - return actualForNode - } - -} - -class FlowFrameworkPersistenceTests { - companion object { - init { - LogHelper.setLevel("+net.corda.flow") + @Suspendable + override fun call(): FlowInfo { + val flowInfos = otherParties.map { + val session = initiateFlow(it) + session.send(payload) + session.getCounterpartyFlowInfo() + }.toList() + return flowInfos.first() } } - private lateinit var mockNet: InternalMockNetwork - private val receivedSessionMessages = ArrayList() - private lateinit var aliceNode: TestStartedNode - private lateinit var bobNode: TestStartedNode - private lateinit var notaryIdentity: Party - private lateinit var alice: Party - private lateinit var bob: Party - private lateinit var aliceFlowManager: MockNodeFlowManager - private lateinit var bobFlowManager: MockNodeFlowManager - - @Before - fun start() { - mockNet = InternalMockNetwork( - cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"), - servicePeerAllocationStrategy = RoundRobin() - ) - aliceFlowManager = MockNodeFlowManager() - bobFlowManager = MockNodeFlowManager() - - aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME, flowManager = aliceFlowManager)) - bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, flowManager = bobFlowManager)) - - receivedSessionMessagesObservable().forEach { receivedSessionMessages += it } - - // Extract identities - alice = aliceNode.info.singleIdentity() - bob = bobNode.info.singleIdentity() - notaryIdentity = mockNet.defaultNotaryIdentity - } - - @After - fun cleanUp() { - mockNet.stopNodes() - receivedSessionMessages.clear() - } - - @Test - fun `newly added flow is preserved on restart`() { - aliceNode.services.startFlow(NoOpFlow(nonTerminating = true)) - aliceNode.internals.acceptableLiveFiberCountOnStop = 1 - val restoredFlow = aliceNode.restartAndGetRestoredFlow() - assertThat(restoredFlow.flowStarted).isTrue() - } - - @Test - fun `flow restarted just after receiving payload`() { - bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } - aliceNode.services.startFlow(SendFlow("Hello", bob)) - - // We push through just enough messages to get only the payload sent - bobNode.pumpReceive() - bobNode.internals.disableDBCloseOnStop() - bobNode.internals.acceptableLiveFiberCountOnStop = 1 - bobNode.dispose() - mockNet.runNetwork() - val restoredFlow = bobNode.restartAndGetRestoredFlow() - assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello") - } - - @Test - fun `flow loaded from checkpoint will respond to messages from before start`() { - aliceNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) } - bobNode.services.startFlow(ReceiveFlow(alice).nonTerminating()) // Prepare checkpointed receive flow - val restoredFlow = bobNode.restartAndGetRestoredFlow() - assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello") - } - - @Ignore("Some changes in startup order make this test's assumptions fail.") - @Test - fun `flow with send will resend on interrupted restart`() { - val payload = random63BitValue() - val payload2 = random63BitValue() - - var sentCount = 0 - mockNet.messagingNetwork.sentMessages.toSessionTransfers().filter { it.isPayloadTransfer }.forEach { sentCount++ } - val charlieNode = mockNet.createNode(InternalMockNodeParameters(legalName = CHARLIE_NAME)) - val secondFlow = charlieNode.registerCordappFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) } - mockNet.runNetwork() - val charlie = charlieNode.info.singleIdentity() - - // Kick off first send and receive - bobNode.services.startFlow(PingPongFlow(charlie, payload)) - bobNode.database.transaction { - assertEquals(1, bobNode.internals.checkpointStorage.checkpoints().size) + private object WaitingFlows { + @InitiatingFlow + class Waiter(val stx: SignedTransaction, val otherParty: Party) : FlowLogic() { + @Suspendable + override fun call(): SignedTransaction { + val otherPartySession = initiateFlow(otherParty) + otherPartySession.send(stx) + return waitForLedgerCommit(stx.id) + } } - // Make sure the add() has finished initial processing. - bobNode.internals.disableDBCloseOnStop() - // Restart node and thus reload the checkpoint and resend the message with same UUID - bobNode.dispose() - bobNode.database.transaction { - assertEquals(1, bobNode.internals.checkpointStorage.checkpoints().size) // confirm checkpoint - bobNode.services.networkMapCache.clearNetworkMapCache() - } - val node2b = mockNet.createNode(InternalMockNodeParameters(bobNode.internals.id)) - bobNode.internals.manuallyCloseDB() - val (firstAgain, fut1) = node2b.getSingleFlow() - // Run the network which will also fire up the second flow. First message should get deduped. So message data stays in sync. - mockNet.runNetwork() - fut1.getOrThrow() - val receivedCount = receivedSessionMessages.count { it.isPayloadTransfer } - // Check flows completed cleanly and didn't get out of phase - assertEquals(4, receivedCount, "Flow should have exchanged 4 unique messages")// Two messages each way - // can't give a precise value as every addMessageHandler re-runs the undelivered messages - assertTrue(sentCount > receivedCount, "Node restart should have retransmitted messages") - node2b.database.transaction { - assertEquals(0, node2b.internals.checkpointStorage.checkpoints().size, "Checkpoints left after restored flow should have ended") + class Committer(val otherPartySession: FlowSession, val throwException: (() -> Exception)? = null) : FlowLogic() { + @Suspendable + override fun call(): SignedTransaction { + val stx = otherPartySession.receive().unwrap { it } + if (throwException != null) throw throwException.invoke() + return subFlow(FinalityFlow(stx, setOf(otherPartySession.counterparty))) + } } - charlieNode.database.transaction { - assertEquals(0, charlieNode.internals.checkpointStorage.checkpoints().size, "Checkpoints left after restored flow should have ended") - } - assertEquals(payload2, firstAgain.receivedPayload, "Received payload does not match the first value on Node 3") - assertEquals(payload2 + 1, firstAgain.receivedPayload2, "Received payload does not match the expected second value on Node 3") - assertEquals(payload, secondFlow.getOrThrow().receivedPayload, "Received payload does not match the (restarted) first value on Node 2") - assertEquals(payload + 1, secondFlow.getOrThrow().receivedPayload2, "Received payload does not match the expected second value on Node 2") } - //////////////////////////////////////////////////////////////////////////////////////////////////////////// - //region Helpers - - private inline fun > TestStartedNode.restartAndGetRestoredFlow(): P { - val newNode = mockNet.restartNode(this) - newNode.internals.acceptableLiveFiberCountOnStop = 1 - mockNet.runNetwork() - return newNode.getSingleFlow

().first + private class LazyServiceHubAccessFlow : FlowLogic() { + val lazyTime: Instant by lazy { serviceHub.clock.instant() } + @Suspendable + override fun call() = Unit } - private fun receivedSessionMessagesObservable(): Observable { - return mockNet.messagingNetwork.receivedMessages.toSessionTransfers() + private interface CustomInterface + + private class CustomSendFlow(payload: String, otherParty: Party) : CustomInterface, SendFlow(payload, otherParty) + + @InitiatingFlow + private class IncorrectCustomSendFlow(payload: String, otherParty: Party) : CustomInterface, SendFlow(payload, otherParty) + + @InitiatingFlow + private class VaultQueryFlow(val stx: SignedTransaction, val otherParty: Party) : FlowLogic>>() { + @Suspendable + override fun call(): List> { + val otherPartySession = initiateFlow(otherParty) + otherPartySession.send(stx) + // hold onto reference here to force checkpoint of vaultService and thus + // prove it is registered as a tokenizableService in the node + val vaultQuerySvc = serviceHub.vaultService + waitForLedgerCommit(stx.id) + return vaultQuerySvc.queryBy().states + } + } + + @InitiatingFlow(version = 2) + private class UpgradedFlow(val otherParty: Party, val otherPartySession: FlowSession? = null) : FlowLogic>() { + constructor(otherPartySession: FlowSession) : this(otherPartySession.counterparty, otherPartySession) + + @Suspendable + override fun call(): Pair { + val otherPartySession = this.otherPartySession ?: initiateFlow(otherParty) + val received = otherPartySession.receive().unwrap { it } + val otherFlowVersion = otherPartySession.getCounterpartyFlowInfo().flowVersion + return Pair(received, otherFlowVersion) + } + } + + private class SingleInlinedSubFlow(val otherPartySession: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + val payload = otherPartySession.receive().unwrap { it } + subFlow(InlinedSendFlow(payload + payload, otherPartySession)) + } + } + + private class DoubleInlinedSubFlow(val otherPartySession: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + subFlow(SingleInlinedSubFlow(otherPartySession)) + } + } + + private data class NonSerialisableData(val a: Int) + private class NonSerialisableFlowException(@Suppress("unused") val data: NonSerialisableData) : FlowException() + + private class InlinedSendFlow(val payload: String, val otherPartySession: FlowSession) : FlowLogic() { + @Suspendable + override fun call() = otherPartySession.send(payload) } //endregion Helpers } -private fun sessionConfirm(flowVersion: Int = 1) = ExistingSessionMessage(SessionId(0), ConfirmSessionMessage(SessionId(0), FlowInfo(flowVersion, ""))) +internal fun sessionConfirm(flowVersion: Int = 1) = ExistingSessionMessage(SessionId(0), ConfirmSessionMessage(SessionId(0), FlowInfo(flowVersion, ""))) -private inline fun > TestStartedNode.getSingleFlow(): Pair> { +internal inline fun > TestStartedNode.getSingleFlow(): Pair> { return smm.findStateMachines(P::class.java).single() } @@ -786,7 +605,7 @@ private fun sanitise(message: SessionMessage) = when (message) { } } -private fun Observable.toSessionTransfers(): Observable { +internal fun Observable.toSessionTransfers(): Observable { return filter { it.getMessage().topic == FlowMessagingImpl.sessionTopic }.map { val from = it.sender.id val message = it.messageData.deserialize() @@ -794,12 +613,19 @@ private fun Observable.toSessionTransfers(): Observable = Pair(internals.id, message) -private infix fun Pair.to(node: TestStartedNode): SessionTransfer = SessionTransfer(first, second, node.network.myAddress) +internal fun errorMessage(errorResponse: FlowException? = null) = ExistingSessionMessage(SessionId(0), ErrorSessionMessage(errorResponse, 0)) -private data class SessionTransfer(val from: Int, val message: SessionMessage, val to: MessageRecipients) { +internal infix fun TestStartedNode.sent(message: SessionMessage): Pair = Pair(internals.id, message) +internal infix fun Pair.to(node: TestStartedNode): SessionTransfer = SessionTransfer(first, second, node.network.myAddress) + +internal data class SessionTransfer(val from: Int, val message: SessionMessage, val to: MessageRecipients) { val isPayloadTransfer: Boolean get() = message is ExistingSessionMessage && message.payload is DataSessionMessage || @@ -808,40 +634,14 @@ private data class SessionTransfer(val from: Int, val message: SessionMessage, v override fun toString(): String = "$from sent $message to $to" } - -private fun sessionInit(clientFlowClass: KClass>, flowVersion: Int = 1, payload: Any? = null): InitialSessionMessage { +internal fun sessionInit(clientFlowClass: KClass>, flowVersion: Int = 1, payload: Any? = null): InitialSessionMessage { return InitialSessionMessage(SessionId(0), 0, clientFlowClass.java.name, flowVersion, "", payload?.serialize()) } -private fun sessionData(payload: Any) = ExistingSessionMessage(SessionId(0), DataSessionMessage(payload.serialize())) - - -private val FlowLogic<*>.progressSteps: CordaFuture>> - get() { - return progressTracker!!.changes - .ofType(Change.Position::class.java) - .map { it.newStep } - .materialize() - .toList() - .toFuture() - } +internal fun sessionData(payload: Any) = ExistingSessionMessage(SessionId(0), DataSessionMessage(payload.serialize())) @InitiatingFlow -private class WaitForOtherSideEndBeforeSendAndReceive(val otherParty: Party, - @Transient val receivedOtherFlowEnd: Semaphore) : FlowLogic() { - @Suspendable - override fun call() { - // Kick off the flow on the other side ... - val session = initiateFlow(otherParty) - session.send(1) - // ... then pause this one until it's received the session-end message from the other side - receivedOtherFlowEnd.acquire() - session.sendAndReceive(2) - } -} - -@InitiatingFlow -private open class SendFlow(val payload: Any, vararg val otherParties: Party) : FlowLogic() { +internal open class SendFlow(private val payload: Any, private vararg val otherParties: Party) : FlowLogic() { init { require(otherParties.isNotEmpty()) } @@ -857,46 +657,7 @@ private open class SendFlow(val payload: Any, vararg val otherParties: Party) : } } -// we need brand new class for a flow to fail, so here it is -@InitiatingFlow -private open class NeverRegisteredFlow(val payload: Any, vararg val otherParties: Party) : FlowLogic() { - init { - require(otherParties.isNotEmpty()) - } - - @Suspendable - override fun call(): FlowInfo { - val flowInfos = otherParties.map { - val session = initiateFlow(it) - session.send(payload) - session.getCounterpartyFlowInfo() - }.toList() - return flowInfos.first() - } -} - -private object WaitingFlows { - @InitiatingFlow - class Waiter(val stx: SignedTransaction, val otherParty: Party) : FlowLogic() { - @Suspendable - override fun call(): SignedTransaction { - val otherPartySession = initiateFlow(otherParty) - otherPartySession.send(stx) - return waitForLedgerCommit(stx.id) - } - } - - class Committer(val otherPartySession: FlowSession, val throwException: (() -> Exception)? = null) : FlowLogic() { - @Suspendable - override fun call(): SignedTransaction { - val stx = otherPartySession.receive().unwrap { it } - if (throwException != null) throw throwException.invoke() - return subFlow(FinalityFlow(stx, setOf(otherPartySession.counterparty))) - } - } -} - -private class NoOpFlow(val nonTerminating: Boolean = false) : FlowLogic() { +internal class NoOpFlow(val nonTerminating: Boolean = false) : FlowLogic() { @Transient var flowStarted = false @@ -909,7 +670,7 @@ private class NoOpFlow(val nonTerminating: Boolean = false) : FlowLogic() } } -private class InitiatedReceiveFlow(val otherPartySession: FlowSession) : FlowLogic() { +internal class InitiatedReceiveFlow(private val otherPartySession: FlowSession) : FlowLogic() { object START_STEP : ProgressTracker.Step("Starting") object RECEIVED_STEP : ProgressTracker.Step("Received") @@ -934,26 +695,13 @@ private class InitiatedReceiveFlow(val otherPartySession: FlowSession) : FlowLog } } -private class LazyServiceHubAccessFlow : FlowLogic() { - val lazyTime: Instant by lazy { serviceHub.clock.instant() } - @Suspendable - override fun call() = Unit -} - -private open class InitiatedSendFlow(val payload: Any, val otherPartySession: FlowSession) : FlowLogic() { +internal open class InitiatedSendFlow(private val payload: Any, private val otherPartySession: FlowSession) : FlowLogic() { @Suspendable override fun call() = otherPartySession.send(payload) } -private interface CustomInterface - -private class CustomSendFlow(payload: String, otherParty: Party) : CustomInterface, SendFlow(payload, otherParty) - @InitiatingFlow -private class IncorrectCustomSendFlow(payload: String, otherParty: Party) : CustomInterface, SendFlow(payload, otherParty) - -@InitiatingFlow -private class ReceiveFlow(vararg val otherParties: Party) : FlowLogic() { +internal class ReceiveFlow(private vararg val otherParties: Party) : FlowLogic() { object START_STEP : ProgressTracker.Step("Starting") object RECEIVED_STEP : ProgressTracker.Step("Received") @@ -982,72 +730,23 @@ private class ReceiveFlow(vararg val otherParties: Party) : FlowLogic() { } } -private class MyFlowException(override val message: String) : FlowException() { +internal class MyFlowException(override val message: String) : FlowException() { override fun equals(other: Any?): Boolean = other is MyFlowException && other.message == this.message override fun hashCode(): Int = message.hashCode() } @InitiatingFlow -private class VaultQueryFlow(val stx: SignedTransaction, val otherParty: Party) : FlowLogic>>() { - @Suspendable - override fun call(): List> { - val otherPartySession = initiateFlow(otherParty) - otherPartySession.send(stx) - // hold onto reference here to force checkpoint of vaultService and thus - // prove it is registered as a tokenizableService in the node - val vaultQuerySvc = serviceHub.vaultService - waitForLedgerCommit(stx.id) - return vaultQuerySvc.queryBy().states - } -} - -@InitiatingFlow(version = 2) -private class UpgradedFlow(val otherParty: Party, val otherPartySession: FlowSession? = null) : FlowLogic>() { - constructor(otherPartySession: FlowSession) : this(otherPartySession.counterparty, otherPartySession) - - @Suspendable - override fun call(): Pair { - val otherPartySession = this.otherPartySession ?: initiateFlow(otherParty) - val received = otherPartySession.receive().unwrap { it } - val otherFlowVersion = otherPartySession.getCounterpartyFlowInfo().flowVersion - return Pair(received, otherFlowVersion) - } -} - -private class SingleInlinedSubFlow(val otherPartySession: FlowSession) : FlowLogic() { - @Suspendable - override fun call() { - val payload = otherPartySession.receive().unwrap { it } - subFlow(InlinedSendFlow(payload + payload, otherPartySession)) - } -} - -private class DoubleInlinedSubFlow(val otherPartySession: FlowSession) : FlowLogic() { - @Suspendable - override fun call() { - subFlow(SingleInlinedSubFlow(otherPartySession)) - } -} - -private data class NonSerialisableData(val a: Int) -private class NonSerialisableFlowException(@Suppress("unused") val data: NonSerialisableData) : FlowException() - -@InitiatingFlow -private class SendAndReceiveFlow(val otherParty: Party, val payload: Any, val otherPartySession: FlowSession? = null) : FlowLogic() { +internal class SendAndReceiveFlow(private val otherParty: Party, private val payload: Any, private val otherPartySession: FlowSession? = null) : FlowLogic() { constructor(otherPartySession: FlowSession, payload: Any) : this(otherPartySession.counterparty, payload, otherPartySession) @Suspendable - override fun call(): Any = (otherPartySession - ?: initiateFlow(otherParty)).sendAndReceive(payload).unwrap { it } -} - -private class InlinedSendFlow(val payload: String, val otherPartySession: FlowSession) : FlowLogic() { - @Suspendable - override fun call() = otherPartySession.send(payload) + override fun call(): Any { + return (otherPartySession ?: initiateFlow(otherParty)).sendAndReceive(payload).unwrap { it } + } } @InitiatingFlow -private class PingPongFlow(val otherParty: Party, val payload: Long, val otherPartySession: FlowSession? = null) : FlowLogic() { +internal class PingPongFlow(private val otherParty: Party, private val payload: Long, private val otherPartySession: FlowSession? = null) : FlowLogic() { constructor(otherPartySession: FlowSession, payload: Long) : this(otherPartySession.counterparty, payload, otherPartySession) @Transient @@ -1063,7 +762,7 @@ private class PingPongFlow(val otherParty: Party, val payload: Long, val otherPa } } -private class ExceptionFlow(val exception: () -> E) : FlowLogic() { +internal class ExceptionFlow(val exception: () -> E) : FlowLogic() { object START_STEP : ProgressTracker.Step("Starting") override val progressTracker: ProgressTracker = ProgressTracker(START_STEP) @@ -1075,4 +774,4 @@ private class ExceptionFlow(val exception: () -> E) : FlowLogic() + } + + @Before + fun setUpGlobalMockNet() { + mockNet = InternalMockNetwork( + cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"), + servicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin() + ) + + 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 } + } + + @After + fun cleanUp() { + mockNet.stopNodes() + receivedSessionMessages.clear() + } + + private fun receivedSessionMessagesObservable(): Observable { + return mockNet.messagingNetwork.receivedMessages.toSessionTransfers() + } + + @Test + fun `sending to multiple parties`() { + bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it) + .nonTerminating() } + charlieNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it) + .nonTerminating() } + val payload = "Hello World" + aliceNode.services.startFlow(SendFlow(payload, bob, charlie)) + mockNet.runNetwork() + bobNode.internals.acceptableLiveFiberCountOnStop = 1 + charlieNode.internals.acceptableLiveFiberCountOnStop = 1 + val bobFlow = bobNode.getSingleFlow().first + val charlieFlow = charlieNode.getSingleFlow().first + assertThat(bobFlow.receivedPayloads[0]).isEqualTo(payload) + assertThat(charlieFlow.receivedPayloads[0]).isEqualTo(payload) + + assertSessionTransfers(bobNode, + aliceNode sent sessionInit(SendFlow::class, payload = payload) to bobNode, + bobNode sent sessionConfirm() to aliceNode, + aliceNode sent normalEnd to bobNode + //There's no session end from the other flows as they're manually suspended + ) + + assertSessionTransfers(charlieNode, + aliceNode sent sessionInit(SendFlow::class, payload = payload) to charlieNode, + charlieNode sent sessionConfirm() to aliceNode, + aliceNode sent normalEnd to charlieNode + //There's no session end from the other flows as they're manually suspended + ) + } + + @Test + fun `receiving from multiple parties`() { + val bobPayload = "Test 1" + val charliePayload = "Test 2" + bobNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(bobPayload, it) } + charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(charliePayload, it) } + val multiReceiveFlow = ReceiveFlow(bob, charlie).nonTerminating() + aliceNode.services.startFlow(multiReceiveFlow) + aliceNode.internals.acceptableLiveFiberCountOnStop = 1 + mockNet.runNetwork() + assertThat(multiReceiveFlow.receivedPayloads[0]).isEqualTo(bobPayload) + assertThat(multiReceiveFlow.receivedPayloads[1]).isEqualTo(charliePayload) + + assertSessionTransfers(bobNode, + aliceNode sent sessionInit(ReceiveFlow::class) to bobNode, + bobNode sent sessionConfirm() to aliceNode, + bobNode sent sessionData(bobPayload) to aliceNode, + bobNode sent normalEnd to aliceNode + ) + + assertSessionTransfers(charlieNode, + aliceNode sent sessionInit(ReceiveFlow::class) to charlieNode, + charlieNode sent sessionConfirm() to aliceNode, + charlieNode sent sessionData(charliePayload) to aliceNode, + charlieNode sent normalEnd to aliceNode + ) + } + + @Test + fun `FlowException only propagated to parent`() { + charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } } + bobNode.registerCordappFlowFactory(ReceiveFlow::class) { ReceiveFlow(charlie) } + val receivingFiber = aliceNode.services.startFlow(ReceiveFlow(bob)) + mockNet.runNetwork() + AssertionsForClassTypes.assertThatExceptionOfType(UnexpectedFlowEndException::class.java) + .isThrownBy { receivingFiber.resultFuture.getOrThrow() } + } + + @Test + fun `FlowException thrown and there is a 3rd unrelated party flow`() { + // Bob will send its payload and then block waiting for the receive from Alice. Meanwhile Alice will move + // onto Charlie which will throw the exception + val node2Fiber = bobNode + .registerCordappFlowFactory(ReceiveFlow::class) { SendAndReceiveFlow(it, "Hello") } + .map { it.stateMachine } + charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } } + + val aliceFiber = aliceNode.services.startFlow(ReceiveFlow(bob, charlie)) as FlowStateMachineImpl + mockNet.runNetwork() + + // Alice will terminate with the error it received from Charlie but it won't propagate that to Bob (as it's + // not relevant to it) but it will end its session with it + AssertionsForClassTypes.assertThatExceptionOfType(MyFlowException::class.java) + .isThrownBy { + aliceFiber.resultFuture.getOrThrow() + } + val bobResultFuture = node2Fiber.getOrThrow().resultFuture + AssertionsForClassTypes.assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy { + bobResultFuture.getOrThrow() + } + + assertSessionTransfers(bobNode, + aliceNode sent sessionInit(ReceiveFlow::class) to bobNode, + bobNode sent sessionConfirm() to aliceNode, + bobNode sent sessionData("Hello") to aliceNode, + aliceNode sent errorMessage() to bobNode + ) + } + + private val normalEnd = ExistingSessionMessage(SessionId(0), EndSessionMessage) // NormalSessionEnd(0) + + private fun assertSessionTransfers(node: TestStartedNode, vararg expected: SessionTransfer): List { + val actualForNode = receivedSessionMessages.filter { it.from == node.internals.id || it.to == node.network.myAddress } + assertThat(actualForNode).containsExactly(*expected) + return actualForNode + } +} From 71117ebdadf7ea8322c0761ba7981d23163af317 Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Mon, 12 Nov 2018 19:34:21 +0000 Subject: [PATCH 18/24] [CORDA-2212] Better error on empty Party name fuzzy matching (#4220) * typo - no matching --- .../main/kotlin/net/corda/client/jackson/JacksonSupport.kt | 6 +++++- .../net/corda/node/services/api/IdentityServiceInternal.kt | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt index d551889fb1..856091aa3b 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt @@ -351,7 +351,11 @@ object JacksonSupport { val nameMatches = mapper.partiesFromName(parser.text) return when { nameMatches.isEmpty() -> { - val publicKey = parser.readValueAs() + val publicKey = try { + parser.readValueAs() + } catch (e: Exception) { + throw JsonParseException(parser, "No matching Party found, then tried to directly deserialise ${parser.text} as a PublicKey with no success", e) + } mapper.partyFromKey(publicKey) ?: throw JsonParseException(parser, "Could not find a Party with key ${publicKey.toStringShort()}") } diff --git a/node/src/main/kotlin/net/corda/node/services/api/IdentityServiceInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/IdentityServiceInternal.kt index 523c996f22..07057b113a 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/IdentityServiceInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/IdentityServiceInternal.kt @@ -1,7 +1,6 @@ package net.corda.node.services.api import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.CertRole import net.corda.core.node.services.IdentityService From e2a351cb706b384ea789379eba34418f09fb0633 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Tue, 13 Nov 2018 10:44:04 +0000 Subject: [PATCH 19/24] [CORDA-2123]: Unhandled IndexOutOfBoundsException for malformed Shell commands. (fix) (#4225) --- .../main/kotlin/net/corda/tools/shell/InteractiveShell.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt index 55109ff233..8ed572a14a 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt @@ -128,7 +128,11 @@ object InteractiveShell { InterruptHandler { jlineProcessor.interrupt() }.install() thread(name = "Command line shell processor", isDaemon = true) { Emoji.renderIfSupported { - jlineProcessor.run() + try { + jlineProcessor.run() + } catch (e: IndexOutOfBoundsException) { + log.warn("Cannot parse malformed command.") + } } } thread(name = "Command line shell terminator", isDaemon = true) { From f3b09988a92cf85750df903d26f0172e21dd13cf Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Tue, 13 Nov 2018 11:50:24 +0000 Subject: [PATCH 20/24] Updates tutorial to reflect new template structure. Clean-up. (#4216) * Initial improvements. * Updates tutorials. * Missing imports. * Addresses review feedback. --- .../java/tutorial/helloworld/IOUFlow.java | 14 ++--- .../java/tutorial/helloworld/IOUState.java | 6 +-- .../java/tutorial/twoparty/IOUContract.java | 40 ++++++++------- .../docs/java/tutorial/twoparty/IOUFlow.java | 22 ++++---- .../docs/java/tutorial/twoparty/IOUState.java | 4 +- .../kotlin/tutorial/helloworld/IOUFlow.kt | 6 +-- .../kotlin/tutorial/helloworld/IOUState.kt | 2 +- .../kotlin/tutorial/twoparty/IOUContract.kt | 12 ++--- .../docs/kotlin/tutorial/twoparty/IOUFlow.kt | 15 +++--- .../tutorial/twoparty/IOUFlowResponder.kt | 5 +- docs/source/hello-world-flow.rst | 3 +- docs/source/hello-world-introduction.rst | 51 +++++++++++-------- docs/source/hello-world-running.rst | 25 ++++++--- docs/source/hello-world-state.rst | 8 +-- docs/source/hello-world-template.rst | 28 +++++----- docs/source/tut-two-party-contract.rst | 10 ++-- docs/source/tut-two-party-flow.rst | 5 +- docs/source/tut-two-party-introduction.rst | 4 +- 18 files changed, 136 insertions(+), 124 deletions(-) diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlow.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlow.java index 5697aed05e..cadcae2ce2 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlow.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlow.java @@ -2,16 +2,19 @@ package net.corda.docs.java.tutorial.helloworld; import co.paralleluniverse.fibers.Suspendable; import com.template.TemplateContract; -import net.corda.core.flows.*; +import net.corda.core.flows.FlowException; +import net.corda.core.flows.FlowLogic; +import net.corda.core.flows.InitiatingFlow; +import net.corda.core.flows.StartableByRPC; +import net.corda.core.utilities.ProgressTracker; // DOCSTART 01 // Add these imports: import net.corda.core.contracts.Command; -import net.corda.core.contracts.CommandData; +import net.corda.core.flows.FinalityFlow; import net.corda.core.identity.Party; import net.corda.core.transactions.SignedTransaction; import net.corda.core.transactions.TransactionBuilder; -import net.corda.core.utilities.ProgressTracker; // Replace Initiator's definition with: @InitiatingFlow @@ -46,13 +49,12 @@ public class IOUFlow extends FlowLogic { // We create the transaction components. IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty); - CommandData cmdType = new TemplateContract.Commands.Action(); - Command cmd = new Command<>(cmdType, getOurIdentity().getOwningKey()); + Command command = new Command<>(new TemplateContract.Commands.Action(), getOurIdentity().getOwningKey()); // We create a transaction builder and add the components. TransactionBuilder txBuilder = new TransactionBuilder(notary) .addOutputState(outputState, TemplateContract.ID) - .addCommand(cmd); + .addCommand(command); // Signing the transaction. SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUState.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUState.java index f724508648..4e91806b7f 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUState.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUState.java @@ -2,11 +2,11 @@ package net.corda.docs.java.tutorial.helloworld; import net.corda.core.contracts.ContractState; import net.corda.core.identity.AbstractParty; +import java.util.Arrays; import java.util.List; // DOCSTART 01 -// Add these imports: -import com.google.common.collect.ImmutableList; +// Add this import: import net.corda.core.identity.Party; // Replace TemplateState's definition with: @@ -35,7 +35,7 @@ public class IOUState implements ContractState { @Override public List getParticipants() { - return ImmutableList.of(lender, borrower); + return Arrays.asList(lender, borrower); } } // DOCEND 01 \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUContract.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUContract.java index af2bb40daa..5f96ecd450 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUContract.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUContract.java @@ -6,15 +6,14 @@ import net.corda.core.transactions.LedgerTransaction; // DOCSTART 01 // Add these imports: -import com.google.common.collect.ImmutableList; import net.corda.core.contracts.CommandWithParties; import net.corda.core.identity.Party; import java.security.PublicKey; +import java.util.Arrays; import java.util.List; import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; -import static net.corda.core.contracts.ContractsDSL.requireThat; // Replace TemplateContract's definition with: public class IOUContract implements Contract { @@ -28,26 +27,29 @@ public class IOUContract implements Contract { public void verify(LedgerTransaction tx) { final CommandWithParties command = requireSingleCommand(tx.getCommands(), IOUContract.Create.class); - requireThat(check -> { - // Constraints on the shape of the transaction. - check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty()); - check.using("There should be one output state of type IOUState.", tx.getOutputs().size() == 1); + // Constraints on the shape of the transaction. + if (!tx.getInputs().isEmpty()) + throw new IllegalArgumentException("No inputs should be consumed when issuing an IOU."); + if (!(tx.getOutputs().size() == 1)) + throw new IllegalArgumentException("There should be one output state of type IOUState."); - // IOU-specific constraints. - final IOUState out = tx.outputsOfType(IOUState.class).get(0); - final Party lender = out.getLender(); - final Party borrower = out.getBorrower(); - check.using("The IOU's value must be non-negative.", out.getValue() > 0); - check.using("The lender and the borrower cannot be the same entity.", lender != borrower); + // IOU-specific constraints. + final IOUState output = tx.outputsOfType(IOUState.class).get(0); + final Party lender = output.getLender(); + final Party borrower = output.getBorrower(); + if (output.getValue() <= 0) + throw new IllegalArgumentException("The IOU's value must be non-negative."); + if (lender.equals(borrower)) + throw new IllegalArgumentException("The lender and the borrower cannot be the same entity."); - // Constraints on the signers. - final List signers = command.getSigners(); - check.using("There must be two signers.", signers.size() == 2); - check.using("The borrower and lender must be signers.", signers.containsAll( - ImmutableList.of(borrower.getOwningKey(), lender.getOwningKey()))); + // Constraints on the signers. + final List requiredSigners = command.getSigners(); + final List expectedSigners = Arrays.asList(borrower.getOwningKey(), lender.getOwningKey()); + if (requiredSigners.size() != 2) + throw new IllegalArgumentException("There must be two signers."); + if (!(requiredSigners.containsAll(expectedSigners))) + throw new IllegalArgumentException("The borrower and lender must be signers."); - return null; - }); } } // DOCEND 01 \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java index eb5158d24b..8f5221c641 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlow.java @@ -2,9 +2,7 @@ package net.corda.docs.java.tutorial.twoparty; // DOCSTART 01 import co.paralleluniverse.fibers.Suspendable; -import com.google.common.collect.ImmutableList; import net.corda.core.contracts.Command; -import net.corda.core.contracts.StateAndContract; import net.corda.core.flows.*; import net.corda.core.identity.Party; import net.corda.core.transactions.SignedTransaction; @@ -12,6 +10,7 @@ import net.corda.core.transactions.TransactionBuilder; import net.corda.core.utilities.ProgressTracker; import java.security.PublicKey; +import java.util.Arrays; import java.util.List; // DOCEND 01 @@ -42,22 +41,19 @@ public class IOUFlow extends FlowLogic { @Suspendable @Override public Void call() throws FlowException { + // DOCSTART 02 // We retrieve the notary identity from the network map. Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0); - // DOCSTART 02 - // We create a transaction builder. - TransactionBuilder txBuilder = new TransactionBuilder(); - txBuilder.setNotary(notary); - // We create the transaction components. IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty); - StateAndContract outputContractAndState = new StateAndContract(outputState, IOUContract.ID); - List requiredSigners = ImmutableList.of(getOurIdentity().getOwningKey(), otherParty.getOwningKey()); - Command cmd = new Command<>(new IOUContract.Create(), requiredSigners); + List requiredSigners = Arrays.asList(getOurIdentity().getOwningKey(), otherParty.getOwningKey()); + Command command = new Command<>(new IOUContract.Create(), requiredSigners); - // We add the items to the builder. - txBuilder.withItems(outputContractAndState, cmd); + // We create a transaction builder and add the components. + TransactionBuilder txBuilder = new TransactionBuilder(notary) + .addOutputState(outputState, IOUContract.ID) + .addCommand(command); // Verifying the transaction. txBuilder.verify(getServiceHub()); @@ -70,7 +66,7 @@ public class IOUFlow extends FlowLogic { // Obtaining the counterparty's signature. SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow( - signedTx, ImmutableList.of(otherPartySession), CollectSignaturesFlow.tracker())); + signedTx, Arrays.asList(otherPartySession), CollectSignaturesFlow.tracker())); // Finalising the transaction. subFlow(new FinalityFlow(fullySignedTx)); diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUState.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUState.java index 4cc6f9f76c..556746b139 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUState.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUState.java @@ -1,10 +1,10 @@ package net.corda.docs.java.tutorial.twoparty; -import com.google.common.collect.ImmutableList; import net.corda.core.contracts.ContractState; import net.corda.core.identity.AbstractParty; import net.corda.core.identity.Party; +import java.util.Arrays; import java.util.List; public class IOUState implements ContractState { @@ -32,6 +32,6 @@ public class IOUState implements ContractState { @Override public List getParticipants() { - return ImmutableList.of(lender, borrower); + return Arrays.asList(lender, borrower); } } \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUFlow.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUFlow.kt index 8d24d540f7..d3fef7f66d 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUFlow.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUFlow.kt @@ -8,13 +8,13 @@ import net.corda.core.flows.FinalityFlow import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StartableByRPC +import net.corda.core.utilities.ProgressTracker // DOCSTART 01 // Add these imports: import net.corda.core.contracts.Command import net.corda.core.identity.Party import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.ProgressTracker // Replace Initiator's definition with: @InitiatingFlow @@ -33,12 +33,12 @@ class IOUFlow(val iouValue: Int, // We create the transaction components. val outputState = IOUState(iouValue, ourIdentity, otherParty) - val cmd = Command(TemplateContract.Commands.Action(), ourIdentity.owningKey) + val command = Command(TemplateContract.Commands.Action(), ourIdentity.owningKey) // We create a transaction builder and add the components. val txBuilder = TransactionBuilder(notary = notary) .addOutputState(outputState, TemplateContract.ID) - .addCommand(cmd) + .addCommand(command) // We sign the transaction. val signedTx = serviceHub.signInitialTransaction(txBuilder) diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUState.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUState.kt index b66604a504..27a3a52e18 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUState.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUState.kt @@ -5,7 +5,7 @@ package net.corda.docs.kotlin.tutorial.helloworld import net.corda.core.contracts.ContractState // DOCSTART 01 -// Add these imports: +// Add this import: import net.corda.core.identity.Party // Replace TemplateState's definition with: diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUContract.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUContract.kt index 2cc42f6dd7..578039feb2 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUContract.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUContract.kt @@ -5,7 +5,7 @@ import net.corda.core.contracts.Contract import net.corda.core.transactions.LedgerTransaction // DOCSTART 01 -// Add these imports: +// Add this import: import net.corda.core.contracts.* class IOUContract : Contract { @@ -25,14 +25,14 @@ class IOUContract : Contract { "There should be one output state of type IOUState." using (tx.outputs.size == 1) // IOU-specific constraints. - val out = tx.outputsOfType().single() - "The IOU's value must be non-negative." using (out.value > 0) - "The lender and the borrower cannot be the same entity." using (out.lender != out.borrower) + val output = tx.outputsOfType().single() + "The IOU's value must be non-negative." using (output.value > 0) + "The lender and the borrower cannot be the same entity." using (output.lender != output.borrower) // Constraints on the signers. + val expectedSigners = listOf(output.borrower.owningKey, output.lender.owningKey) "There must be two signers." using (command.signers.toSet().size == 2) - "The borrower and lender must be signers." using (command.signers.containsAll(listOf( - out.borrower.owningKey, out.lender.owningKey))) + "The borrower and lender must be signers." using (command.signers.containsAll(expectedSigners)) } } } diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlow.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlow.kt index ef1e9d9b03..6eb508b3f0 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlow.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlow.kt @@ -5,7 +5,6 @@ package net.corda.docs.kotlin.tutorial.twoparty // DOCSTART 01 import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Command -import net.corda.core.contracts.StateAndContract import net.corda.core.flows.CollectSignaturesFlow import net.corda.core.flows.FinalityFlow import net.corda.core.flows.FlowLogic @@ -27,20 +26,18 @@ class IOUFlow(val iouValue: Int, /** The flow logic is encapsulated within the call() method. */ @Suspendable override fun call() { + // DOCSTART 02 // We retrieve the notary identity from the network map. val notary = serviceHub.networkMapCache.notaryIdentities[0] - // DOCSTART 02 - // We create a transaction builder. - val txBuilder = TransactionBuilder(notary = notary) - // We create the transaction components. val outputState = IOUState(iouValue, ourIdentity, otherParty) - val outputContractAndState = StateAndContract(outputState, IOUContract.ID) - val cmd = Command(IOUContract.Create(), listOf(ourIdentity.owningKey, otherParty.owningKey)) + val command = Command(IOUContract.Create(), listOf(ourIdentity.owningKey, otherParty.owningKey)) - // We add the items to the builder. - txBuilder.withItems(outputContractAndState, cmd) + // We create a transaction builder and add the components. + val txBuilder = TransactionBuilder(notary = notary) + .addOutputState(outputState, IOUContract.ID) + .addCommand(command) // Verifying the transaction. txBuilder.verify(serviceHub) diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlowResponder.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlowResponder.kt index c6ac0704b4..882d3061a5 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlowResponder.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/twoparty/IOUFlowResponder.kt @@ -3,10 +3,7 @@ package net.corda.docs.kotlin.tutorial.twoparty import co.paralleluniverse.fibers.Suspendable -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowSession -import net.corda.core.flows.InitiatedBy -import net.corda.core.flows.SignTransactionFlow +import net.corda.core.flows.* import net.corda.docs.kotlin.tutorial.helloworld.IOUFlow import net.corda.docs.kotlin.tutorial.helloworld.IOUState diff --git a/docs/source/hello-world-flow.rst b/docs/source/hello-world-flow.rst index 10c3ba66f6..b6281a51d4 100644 --- a/docs/source/hello-world-flow.rst +++ b/docs/source/hello-world-flow.rst @@ -40,8 +40,7 @@ FlowLogic --------- All flows must subclass ``FlowLogic``. You then define the steps taken by the flow by overriding ``FlowLogic.call``. -Let's define our ``IOUFlow`` in either ``Initiator.java`` or ``Flows.kt``. Delete the two existing flows in the -template (``Initiator`` and ``Responder``), and replace them with the following: +Let's define our ``IOUFlow``. Delete the existing ``Responder`` flow. Then replace the definition of ``Initiator`` with the following: .. container:: codeset diff --git a/docs/source/hello-world-introduction.rst b/docs/source/hello-world-introduction.rst index 06a9fcb46e..a2b7ea2a0f 100644 --- a/docs/source/hello-world-introduction.rst +++ b/docs/source/hello-world-introduction.rst @@ -13,29 +13,38 @@ By this point, :doc:`your dev environment should be set up `, yo :doc:`your first CorDapp `, and you're familiar with Corda's :doc:`key concepts `. What comes next? -If you're a developer, the next step is to write your own CorDapp. CorDapps are plugins that are installed on one or -more Corda nodes, and give the nodes' owners the ability to make their node conduct some new process - anything from +If you're a developer, the next step is to write your own CorDapp. CorDapps are applications that are installed on one or +more Corda nodes, and that allow the node's operator to instruct their node to perform some new process - anything from issuing a debt instrument to making a restaurant booking. Our use-case ------------ -Our CorDapp will model IOUs on-ledger. An IOU – short for “I O(we) (yo)U” – records the fact that one person owes -another person a given amount of money. Clearly this is sensitive information that we'd only want to communicate on -a need-to-know basis between the lender and the borrower. Fortunately, this is one of the areas where Corda excels. -Corda makes it easy to allow a small set of parties to agree on a shared fact without needing to share this fact with -everyone else on the network, as is the norm in blockchain platforms. +We will write a CorDapp to model IOUs on the blockchain. Each IOU – short for “I O(we) (yo)U” – will record the fact that one node owes +another node a certain amount. This simple CorDapp will showcase several key benefits of Corda as a blockchain platform: -To serve any useful function, our CorDapp will need at least two things: +* **Privacy** - Since IOUs represent sensitive information, we will be taking advantage of Corda's ability to only share + ledger updates with other nodes on a need-to-know basis, instead of using a gossip protocol to share this information with every node on + the network as you would with a traditional blockchain platform -* **States**, the shared facts that Corda nodes reach consensus over and are then stored on the ledger -* **Flows**, which encapsulate the procedure for carrying out a specific ledger update +* **Well-known identities** - Each Corda node has a well-known identity on the network. This allows us to write code in terms of real + identities, rather than anonymous public keys -Our IOU CorDapp is no exception. It will define both a state and a flow: +* **Re-use of existing, proven technologies** - We will be writing our CorDapp using standard Java. It will run on a Corda node, which is + simply a Java process and runs on a regular Java machine (e.g. on your local machine or in the cloud). The nodes will store their data in + a standard SQL database + +CorDapps usually define at least three things: + +* **States** - the (possibly shared) facts that are written to the ledger +* **Flows** - the procedures for carrying out specific ledger updates +* **Contracts** - the constraints governing how states of a given type can evolve over time + +Our IOU CorDapp is no exception. It will define the following components: The IOUState ^^^^^^^^^^^^ -Our state will be the ``IOUState``. It will store the value of the IOU, as well as the identities of the lender and the -borrower. We can visualize ``IOUState`` as follows: +Our state will be the ``IOUState``, representing an IOU. It will contain the IOU's value, its lender and its borrower. We can visualize +``IOUState`` as follows: .. image:: resources/tutorial-state.png :scale: 25% @@ -43,20 +52,20 @@ borrower. We can visualize ``IOUState`` as follows: The IOUFlow ^^^^^^^^^^^ -Our flow will be the ``IOUFlow``. This flow will completely automate the process of issuing a new IOU onto a ledger. It -is composed of the following steps: +Our flow will be the ``IOUFlow``. This flow will completely automate the process of issuing a new IOU onto a ledger. It has the following +steps: .. image:: resources/simple-tutorial-flow.png :scale: 25% :align: center -In traditional distributed ledger systems, where all data is broadcast to every network participant, you don’t need to -think about data flows – you simply package up your ledger update and send it to everyone else on the network. But in -Corda, where privacy is a core focus, flows allow us to carefully control who sees what during the process of -agreeing a ledger update. +The IOUContract +^^^^^^^^^^^^^^^ +For this tutorial, we will use the default ``TemplateContract``. We will update it to create a fully-fledged ``IOUContract`` in the next +tutorial. Progress so far --------------- -We've sketched out a simple CorDapp that will allow nodes to confidentially issue new IOUs onto a ledger. +We've designed a simple CorDapp that will allow nodes to agree new IOUs on the blockchain. -Next, we'll be taking a look at the template project we'll be using as the basis for our CorDapp. \ No newline at end of file +Next, we'll take a look at the template project we'll be using as the basis for our CorDapp. diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index d217dee102..b57979a7ec 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -107,11 +107,9 @@ commands. We want to create an IOU of 99 with PartyB. We start the ``IOUFlow`` by typing: -.. container:: codeset +.. code-block:: bash - .. code-block:: kotlin - - start IOUFlow iouValue: 99, otherParty: "O=PartyB,L=New York,C=US" + start IOUFlow iouValue: 99, otherParty: "O=PartyB,L=New York,C=US" This single command will cause PartyA and PartyB to automatically agree an IOU. This is one of the great advantages of the flow framework - it allows you to reduce complex negotiation and update processes into a single function call. @@ -122,7 +120,7 @@ We can check the contents of each node's vault by running: .. code-block:: bash - run vaultQuery contractStateType: com.template.IOUState + run vaultQuery contractStateType: com.template.IOUState The vaults of PartyA and PartyB should both display the following output: @@ -162,12 +160,27 @@ The vaults of PartyA and PartyB should both display the following output: This is the transaction issuing our ``IOUState`` onto a ledger. +However, if we run the same command on the other node (the notary), we will see the following: + +.. code:: bash + + { + "states" : [ ], + "statesMetadata" : [ ], + "totalStatesAvailable" : -1, + "stateTypes" : "UNCONSUMED", + "otherResults" : [ ] + } + +This is the result of Corda's privacy model. Because the notary was not involved in the transaction and had no need to see the data, the +transaction was not distributed to them. + Conclusion ---------- We have written a simple CorDapp that allows IOUs to be issued onto the ledger. Our CorDapp is made up of two key parts: -* The ``IOUState``, representing IOUs on the ledger +* The ``IOUState``, representing IOUs on the blockchain * The ``IOUFlow``, orchestrating the process of agreeing the creation of an IOU on-ledger After completing this tutorial, your CorDapp should look like this: diff --git a/docs/source/hello-world-state.rst b/docs/source/hello-world-state.rst index 0a2d9c4b12..1a72159201 100644 --- a/docs/source/hello-world-state.rst +++ b/docs/source/hello-world-state.rst @@ -7,7 +7,7 @@ Writing the state ================= -In Corda, shared facts on the ledger are represented as states. Our first task will be to define a new state type to +In Corda, shared facts on the blockchain are represented as states. Our first task will be to define a new state type to represent an IOU. The ContractState interface @@ -28,7 +28,7 @@ We can see that the ``ContractState`` interface has a single field, ``participan entities for which this state is relevant. Beyond this, our state is free to define any fields, methods, helpers or inner classes it requires to accurately -represent a given type of shared fact on the ledger. +represent a given type of shared fact on the blockchain. .. note:: @@ -46,7 +46,7 @@ represent a given type of shared fact on the ledger. Modelling IOUs -------------- -How should we define the ``IOUState`` representing IOUs on the ledger? Beyond implementing the ``ContractState`` +How should we define the ``IOUState`` representing IOUs on the blockchain? Beyond implementing the ``ContractState`` interface, our ``IOUState`` will also need properties to track the relevant features of the IOU: * The value of the IOU @@ -99,7 +99,7 @@ Corda are simply classes that implement the ``ContractState`` interface. They ca methods you like. All that's left to do is write the ``IOUFlow`` that will allow a node to orchestrate the creation of a new ``IOUState`` -on the ledger, while only sharing information on a need-to-know basis. +on the blockchain, while only sharing information on a need-to-know basis. What about the contract? ------------------------ diff --git a/docs/source/hello-world-template.rst b/docs/source/hello-world-template.rst index 5d421d0906..8ad3d76c65 100644 --- a/docs/source/hello-world-template.rst +++ b/docs/source/hello-world-template.rst @@ -7,41 +7,39 @@ The CorDapp Template ==================== -When writing a new CorDapp, you’ll generally want to base it on the standard templates: +When writing a new CorDapp, you’ll generally want to start from one of the standard templates: * The `Java Cordapp Template `_ * The `Kotlin Cordapp Template `_ -The Cordapp templates provide the required boilerplate for developing a CorDapp, and allow you to quickly deploy your -CorDapp onto a local test network of dummy nodes to test its functionality. +The Cordapp templates provide the boilerplate for developing a new CorDapp. CorDapps can be written in either Java or Kotlin. We will be +providing the code in both languages throughout this tutorial. -CorDapps can be written in both Java and Kotlin, and will be providing the code in both languages in this tutorial. - -Note that there's no need to download and install Corda itself. Corda's required libraries will be downloaded -automatically from an online Maven repository. +Note that there's no need to download and install Corda itself. The required libraries are automatically downloaded from an online Maven +repository and cached locally. Downloading the template ------------------------ -To download the template, open a terminal window in the directory where you want to download the CorDapp template, and -run the following command: +Open a terminal window in the directory where you want to download the CorDapp template, and run the following command: -.. code-block:: bash +.. container:: codeset - git clone https://github.com/corda/cordapp-template-java.git ; cd cordapp-template-java + .. code-block:: java - *or* + git clone https://github.com/corda/cordapp-template-java.git ; cd cordapp-template-java - git clone https://github.com/corda/cordapp-template-kotlin.git ; cd cordapp-template-kotlin + .. code-block:: kotlin + + git clone https://github.com/corda/cordapp-template-kotlin.git ; cd cordapp-template-kotlin Opening the template in IntelliJ -------------------------------- - Once the template is download, open it in IntelliJ by following the instructions here: https://docs.corda.net/tutorial-cordapp.html#opening-the-example-cordapp-in-intellij. Template structure ------------------ -The template has a number of files, but we can ignore most of them. We will only be modifying the following files: +For this tutorial, we will only be modifying the following files: .. container:: codeset diff --git a/docs/source/tut-two-party-contract.rst b/docs/source/tut-two-party-contract.rst index 8a94c75bad..720c40cb6f 100644 --- a/docs/source/tut-two-party-contract.rst +++ b/docs/source/tut-two-party-contract.rst @@ -154,7 +154,7 @@ Transaction constraints ~~~~~~~~~~~~~~~~~~~~~~~ We also want our transaction to have no inputs and only a single output - an issuance transaction. -To impose this and the subsequent constraints, we are using Corda's built-in ``requireThat`` block. ``requireThat`` +In Kotlin, we impose these and the subsequent constraints using Corda's built-in ``requireThat`` block. ``requireThat`` provides a terse way to write the following: * If the condition on the right-hand side doesn't evaluate to true... @@ -162,6 +162,8 @@ provides a terse way to write the following: As before, the act of throwing this exception causes the transaction to be considered invalid. +In Java, we simply throw an ``IllegalArgumentException`` manually instead. + IOU constraints ~~~~~~~~~~~~~~~ We want to impose two constraints on the ``IOUState`` itself: @@ -169,9 +171,7 @@ We want to impose two constraints on the ``IOUState`` itself: * Its value must be non-negative * The lender and the borrower cannot be the same entity -We impose these constraints in the same ``requireThat`` block as before. - -You can see that we're not restricted to only writing constraints in the ``requireThat`` block. We can also write +You can see that we're not restricted to only writing constraints inside ``verify``. We can also write other statements - in this case, extracting the transaction's single ``IOUState`` and assigning it to a variable. Signer constraints @@ -180,7 +180,7 @@ Finally, we require both the lender and the borrower to be required signers on t required signers is equal to the union of all the signers listed on the commands. We therefore extract the signers from the ``Create`` command we retrieved earlier. -This is an absolutely essential constraint - it ensures that no ``IOUState`` can ever be created on the ledger without +This is an absolutely essential constraint - it ensures that no ``IOUState`` can ever be created on the blockchain without the express agreement of both the lender and borrower nodes. Progress so far diff --git a/docs/source/tut-two-party-flow.rst b/docs/source/tut-two-party-flow.rst index 3a4cebb15d..7542eaed14 100644 --- a/docs/source/tut-two-party-flow.rst +++ b/docs/source/tut-two-party-flow.rst @@ -31,8 +31,7 @@ In ``IOUFlow.java``/``Flows.kt``, change the imports block to the following: :start-after: DOCSTART 01 :end-before: DOCEND 01 -And update ``IOUFlow.call`` by changing the code following the retrieval of the notary's identity from the network as -follows: +And update ``IOUFlow.call`` to the following: .. container:: codeset @@ -136,7 +135,7 @@ defined in ``IOUContract``. We can now re-run our updated CorDapp, using the :doc:`same instructions as before `. Our CorDapp now imposes restrictions on the issuance of IOUs. Most importantly, IOU issuance now requires agreement -from both the lender and the borrower before an IOU can be created on the ledger. This prevents either the lender or +from both the lender and the borrower before an IOU can be created on the blockchain. This prevents either the lender or the borrower from unilaterally updating the ledger in a way that only benefits themselves. After completing this tutorial, your CorDapp should look like this: diff --git a/docs/source/tut-two-party-introduction.rst b/docs/source/tut-two-party-introduction.rst index d44743b2e8..a12f022a08 100644 --- a/docs/source/tut-two-party-introduction.rst +++ b/docs/source/tut-two-party-introduction.rst @@ -6,10 +6,10 @@ Hello, World! Pt.2 - Contract constraints In the Hello, World tutorial, we built a CorDapp allowing us to model IOUs on ledger. Our CorDapp was made up of two elements: -* An ``IOUState``, representing IOUs on the ledger +* An ``IOUState``, representing IOUs on the blockchain * An ``IOUFlow``, orchestrating the process of agreeing the creation of an IOU on-ledger -However, our CorDapp did not impose any constraints on the evolution of IOUs on the ledger over time. Anyone was free +However, our CorDapp did not impose any constraints on the evolution of IOUs on the blockchain over time. Anyone was free to create IOUs of any value, between any party. In this tutorial, we'll write a contract to imposes rules on how an ``IOUState`` can change over time. In turn, this From aa0ccecfde1c4a1d2d011e308d783c5348575c43 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Tue, 13 Nov 2018 15:46:21 +0000 Subject: [PATCH 21/24] [CORDA-2172]: Add link to error-codes-webapp in Corda. (#4224) --- .../net/corda/node/logging/ErrorCodeLoggingTests.kt | 2 +- .../net/corda/cliutils/CordaVersionProvider.kt | 12 +++++++++--- .../net/corda/cliutils/ErrorCodeRewritePolicy.kt | 6 +++--- .../corda/cliutils/ExceptionsErrorCodeFunctions.kt | 4 +++- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/node/src/integration-test/kotlin/net/corda/node/logging/ErrorCodeLoggingTests.kt b/node/src/integration-test/kotlin/net/corda/node/logging/ErrorCodeLoggingTests.kt index 3b07bcbb49..a22ab7a8dc 100644 --- a/node/src/integration-test/kotlin/net/corda/node/logging/ErrorCodeLoggingTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/logging/ErrorCodeLoggingTests.kt @@ -22,7 +22,7 @@ class ErrorCodeLoggingTests { node.rpc.startFlow(::MyFlow).waitForCompletion() val logFile = node.logFile() - val linesWithErrorCode = logFile.useLines { lines -> lines.filter { line -> line.contains("[errorCode=") }.toList() } + val linesWithErrorCode = logFile.useLines { lines -> lines.filter { line -> line.contains("[errorCode=") }.filter { line -> line.contains("moreInformationAt=https://errors.corda.net/") }.toList() } assertThat(linesWithErrorCode).isNotEmpty } diff --git a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaVersionProvider.kt b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaVersionProvider.kt index 85f9e9100b..0231f9d1d5 100644 --- a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaVersionProvider.kt +++ b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaVersionProvider.kt @@ -9,12 +9,18 @@ import picocli.CommandLine */ class CordaVersionProvider : CommandLine.IVersionProvider { companion object { + private const val UNKNOWN = "Unknown" + const val current_major_release = "4.0-SNAPSHOT" + const val platformEditionCode = "OS" + private fun manifestValue(name: String): String? = if (Manifests.exists(name)) Manifests.read(name) else null - val releaseVersion: String by lazy { manifestValue("Corda-Release-Version") ?: "Unknown" } - val revision: String by lazy { manifestValue("Corda-Revision") ?: "Unknown" } - val vendor: String by lazy { manifestValue("Corda-Vendor") ?: "Unknown" } + val releaseVersion: String by lazy { manifestValue("Corda-Release-Version") ?: UNKNOWN } + val revision: String by lazy { manifestValue("Corda-Revision") ?: UNKNOWN } + val vendor: String by lazy { manifestValue("Corda-Vendor") ?: UNKNOWN } val platformVersion: Int by lazy { manifestValue("Corda-Platform-Version")?.toInt() ?: 1 } + + internal val semanticVersion: String by lazy { if(releaseVersion == UNKNOWN) current_major_release else releaseVersion } } override fun getVersion(): Array { diff --git a/tools/cliutils/src/main/kotlin/net/corda/cliutils/ErrorCodeRewritePolicy.kt b/tools/cliutils/src/main/kotlin/net/corda/cliutils/ErrorCodeRewritePolicy.kt index dc35739ea7..dd0d587339 100644 --- a/tools/cliutils/src/main/kotlin/net/corda/cliutils/ErrorCodeRewritePolicy.kt +++ b/tools/cliutils/src/main/kotlin/net/corda/cliutils/ErrorCodeRewritePolicy.kt @@ -9,9 +9,9 @@ import org.apache.logging.log4j.core.impl.Log4jLogEvent @Plugin(name = "ErrorCodeRewritePolicy", category = Core.CATEGORY_NAME, elementType = "rewritePolicy", printObject = false) class ErrorCodeRewritePolicy : RewritePolicy { - override fun rewrite(source: LogEvent?): LogEvent? { - val newMessage = source?.message?.withErrorCodeFor(source.thrown, source.level) - return if (newMessage == source?.message) { + override fun rewrite(source: LogEvent): LogEvent? { + val newMessage = source.message?.withErrorCodeFor(source.thrown, source.level) + return if (newMessage == source.message) { source } else { Log4jLogEvent.Builder(source).setMessage(newMessage).build() diff --git a/tools/cliutils/src/main/kotlin/net/corda/cliutils/ExceptionsErrorCodeFunctions.kt b/tools/cliutils/src/main/kotlin/net/corda/cliutils/ExceptionsErrorCodeFunctions.kt index 08dbfcd157..ec7d74fb0a 100644 --- a/tools/cliutils/src/main/kotlin/net/corda/cliutils/ExceptionsErrorCodeFunctions.kt +++ b/tools/cliutils/src/main/kotlin/net/corda/cliutils/ExceptionsErrorCodeFunctions.kt @@ -8,11 +8,13 @@ import java.util.* internal fun Message.withErrorCodeFor(error: Throwable?, level: Level): Message { return when { - error != null && level.isInRange(Level.FATAL, Level.WARN) -> CompositeMessage("$formattedMessage [errorCode=${error.errorCode()}]", format, parameters, throwable) + error != null && level.isInRange(Level.FATAL, Level.WARN) -> CompositeMessage("$formattedMessage [errorCode=${error.errorCode()}, moreInformationAt=${error.errorCodeLocationUrl()}]", format, parameters, throwable) else -> this } } +private fun Throwable.errorCodeLocationUrl() = "https://errors.corda.net/${CordaVersionProvider.platformEditionCode}/${CordaVersionProvider.semanticVersion}/${errorCode()}" + private fun Throwable.errorCode(hashedFields: (Throwable) -> Array = Throwable::defaultHashedFields): String { val hash = staticLocationBasedHash(hashedFields) From 8aaf120881d538e6cd28dc187b02526c35109a57 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Wed, 14 Nov 2018 11:19:38 +0000 Subject: [PATCH 22/24] [CORDA-2224]: Removed field `value` from `Validated`. Renamed function `orThrow()` to `value()`. (#4231) --- .../parsing/internal/Properties.kt | 2 +- .../parsing/internal/SpecificationTest.kt | 2 +- .../versioned/VersionExtractorTest.kt | 10 ++++---- .../versioned/VersionedParsingExampleTest.kt | 6 ++--- .../common/validation/internal/Validated.kt | 25 ++++++------------- .../config/NodeConfigurationImplTest.kt | 6 ++--- .../testing/node/internal/DriverDSLImpl.kt | 2 +- .../testing/node/internal/NodeBasedTest.kt | 2 +- .../corda/demobench/model/NodeConfigTest.kt | 2 +- .../corda/bootstrapper/nodes/NodeBuilder.kt | 2 +- 10 files changed, 25 insertions(+), 34 deletions(-) diff --git a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Properties.kt b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Properties.kt index 5da5e35360..4892c3ecd2 100644 --- a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Properties.kt +++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Properties.kt @@ -117,7 +117,7 @@ private class OptionalPropertyWithDefault(delegate: Configuration.Pr private class FunctionalProperty(delegate: Configuration.Property.Definition.Standard, private val mappedTypeName: String, internal val extractListValue: (Config, String) -> List, private val convert: (TYPE) -> Valid) : RequiredDelegatedProperty>(delegate), Configuration.Property.Definition.Standard { - override fun valueIn(configuration: Config) = convert.invoke(delegate.valueIn(configuration)).orThrow() + override fun valueIn(configuration: Config) = convert.invoke(delegate.valueIn(configuration)).value() override val typeName: String = if (super.typeName == "#$mappedTypeName") super.typeName else "$mappedTypeName(${super.typeName})" diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SpecificationTest.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SpecificationTest.kt index be859d764a..c7f61f41e9 100644 --- a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SpecificationTest.kt +++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SpecificationTest.kt @@ -39,7 +39,7 @@ class SpecificationTest { val rpcSettings = RpcSettingsSpec.parse(configuration) assertThat(rpcSettings.isValid).isTrue() - assertThat(rpcSettings.orThrow()).satisfies { value -> + assertThat(rpcSettings.value()).satisfies { value -> assertThat(value.useSsl).isEqualTo(useSslValue) assertThat(value.addresses).satisfies { addresses -> diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionExtractorTest.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionExtractorTest.kt index 1cae46f740..0419e202b9 100644 --- a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionExtractorTest.kt +++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionExtractorTest.kt @@ -18,7 +18,7 @@ class VersionExtractorTest { val versionValue = Configuration.Version.Extractor.DEFAULT_VERSION_VALUE + 1 val rawConfiguration = configObject("configuration" to configObject("metadata" to configObject("version" to versionValue), "node" to configObject("p2pAddress" to "localhost:8080"))).toConfig() - val version = extractVersion.invoke(rawConfiguration).orThrow() + val version = extractVersion.invoke(rawConfiguration).value() assertThat(version).isEqualTo(versionValue) } @@ -27,7 +27,7 @@ class VersionExtractorTest { val rawConfiguration = configObject("configuration" to configObject("node" to configObject("p2pAddress" to "localhost:8080"))).toConfig() - val version = extractVersion.invoke(rawConfiguration).orThrow() + val version = extractVersion.invoke(rawConfiguration).value() assertThat(version).isEqualTo(Configuration.Version.Extractor.DEFAULT_VERSION_VALUE) } @@ -36,7 +36,7 @@ class VersionExtractorTest { val rawConfiguration = configObject("configuration" to configObject("metadata" to configObject(), "node" to configObject("p2pAddress" to "localhost:8080"))).toConfig() - val version = extractVersion.invoke(rawConfiguration).orThrow() + val version = extractVersion.invoke(rawConfiguration).value() assertThat(version).isEqualTo(Configuration.Version.Extractor.DEFAULT_VERSION_VALUE) } @@ -46,7 +46,7 @@ class VersionExtractorTest { val rawConfiguration = configObject("configuration" to configObject("metadata" to configObject("version" to null), "node" to configObject("p2pAddress" to "localhost:8080"))).toConfig() - val version = extractVersion.invoke(rawConfiguration).orThrow() + val version = extractVersion.invoke(rawConfiguration).value() assertThat(version).isEqualTo(Configuration.Version.Extractor.DEFAULT_VERSION_VALUE) } @@ -56,7 +56,7 @@ class VersionExtractorTest { val rawConfiguration = configObject().toConfig() - val version = extractVersion.invoke(rawConfiguration).orThrow() + val version = extractVersion.invoke(rawConfiguration).value() assertThat(version).isEqualTo(Configuration.Version.Extractor.DEFAULT_VERSION_VALUE) } diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionedParsingExampleTest.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionedParsingExampleTest.kt index 7b0a8a9be0..2f30fa2211 100644 --- a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionedParsingExampleTest.kt +++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionedParsingExampleTest.kt @@ -52,7 +52,7 @@ class VersionedParsingExampleTest { private fun assertResult(result: Valid, principalAddressValue: Address, adminAddressValue: Address) { assertThat(result.isValid).isTrue() - assertThat(result.orThrow()).satisfies { value -> + assertThat(result.value()).satisfies { value -> assertThat(value.principal).isEqualTo(principalAddressValue) assertThat(value.admin).isEqualTo(adminAddressValue) @@ -94,7 +94,7 @@ class VersionedParsingExampleTest { val adminAddress = addressFor(adminHost, adminPort) return if (principalAddress.isValid && adminAddress.isValid) { - return valid(RpcSettings(principalAddress.value, adminAddress.value)) + return valid(RpcSettings(principalAddress.value(), adminAddress.value())) } else { invalid(principalAddress.errors + adminAddress.errors) } @@ -128,4 +128,4 @@ class VersionedParsingExampleTest { } } -private fun Configuration.Version.Extractor.parseRequired(config: Config, options: Configuration.Validation.Options = Configuration.Validation.Options.defaults) = parse(config, options).map { it ?: throw IllegalStateException("Absent version value.") } \ No newline at end of file +private fun Configuration.Version.Extractor.parseRequired(config: Config, options: Configuration.Validation.Options = Configuration.Validation.Options.defaults) = parse(config, options).map { it } \ No newline at end of file diff --git a/common/validation/src/main/kotlin/net/corda/common/validation/internal/Validated.kt b/common/validation/src/main/kotlin/net/corda/common/validation/internal/Validated.kt index 708af605b9..a93f4b44fb 100644 --- a/common/validation/src/main/kotlin/net/corda/common/validation/internal/Validated.kt +++ b/common/validation/src/main/kotlin/net/corda/common/validation/internal/Validated.kt @@ -8,11 +8,11 @@ import java.util.Collections.emptySet */ interface Validated { /** - * The valid [TARGET] value. + * Returns a valid [TARGET] if no validation errors are present. Otherwise, it throws the exception produced by [exceptionOnErrors], defaulting to [IllegalStateException]. * - * @throws IllegalStateException if accessed in presence of validation errors. + * @throws IllegalStateException or the result of [exceptionOnErrors] if there are errors. */ - val value: TARGET + fun value(exceptionOnErrors: (Set) -> Exception = { errors -> IllegalStateException(errors.joinToString(System.lineSeparator())) }): TARGET /** * The errors produced during validation, if any. @@ -32,14 +32,7 @@ interface Validated { /** * Returns the underlying value as optional, with a null result instead of an exception if validation rules were violated. */ - val optional: TARGET? get() = if (isValid) value else null - - /** - * Returns a valid [TARGET] if no validation errors are present. Otherwise, it throws the exception produced by [exceptionOnErrors], defaulting to [IllegalStateException]. - * - * @throws IllegalStateException or the result of [exceptionOnErrors] if there are errors. - */ - fun orThrow(exceptionOnErrors: (Set) -> Exception = { errors -> IllegalStateException(errors.joinToString(System.lineSeparator())) }): TARGET + val optional: TARGET? get() = if (isValid) value() else null /** * Applies the [convert] function to the [TARGET] value, if valid. Otherwise, returns a [Validated] monad with a [MAPPED] generic type and the current errors set. @@ -62,7 +55,7 @@ interface Validated { */ fun doIfValid(action: (TARGET) -> Unit): Validated { if (isValid) { - action.invoke(value) + action.invoke(value()) } return this } @@ -110,10 +103,10 @@ interface Validated { /** * A successful validation result, containing a valid [TARGET] value and no [ERROR]s. */ - class Successful(override val value: TARGET) : Result(), Validated { + class Successful(private val value: TARGET) : Result(), Validated { override val errors: Set = emptySet() - override fun orThrow(exceptionOnErrors: (Set) -> Exception) = value + override fun value(exceptionOnErrors: (Set) -> Exception) = value override fun map(convert: (TARGET) -> MAPPED): Validated { return valid(convert.invoke(value)) @@ -136,9 +129,7 @@ interface Validated { require(errors.isNotEmpty()) } - override val value: TARGET get() = throw IllegalStateException("Invalid state.") - - override fun orThrow(exceptionOnErrors: (Set) -> Exception) = throw exceptionOnErrors.invoke(errors) + override fun value(exceptionOnErrors: (Set) -> Exception) = throw exceptionOnErrors.invoke(errors) override fun map(convert: (TARGET) -> MAPPED): Validated { return invalid(errors) diff --git a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt index 3c24a25a65..8c1f524f76 100644 --- a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt @@ -234,7 +234,7 @@ class NodeConfigurationImplTest { @Test fun `jmxReporterType is null and defaults to Jokolia`() { val rawConfig = getConfig("working-config.conf", ConfigFactory.parseMap(mapOf("devMode" to true))) - val nodeConfig = rawConfig.parseAsNodeConfiguration().orThrow() + val nodeConfig = rawConfig.parseAsNodeConfiguration().value() assertTrue(JmxReporterType.JOLOKIA.toString() == nodeConfig.jmxReporterType.toString()) } @@ -242,7 +242,7 @@ class NodeConfigurationImplTest { fun `jmxReporterType is not null and is set to New Relic`() { var rawConfig = getConfig("working-config.conf", ConfigFactory.parseMap(mapOf("devMode" to true))) rawConfig = rawConfig.withValue("jmxReporterType", ConfigValueFactory.fromAnyRef("NEW_RELIC")) - val nodeConfig = rawConfig.parseAsNodeConfiguration().orThrow() + val nodeConfig = rawConfig.parseAsNodeConfiguration().value() assertTrue(JmxReporterType.NEW_RELIC.toString() == nodeConfig.jmxReporterType.toString()) } @@ -250,7 +250,7 @@ class NodeConfigurationImplTest { fun `jmxReporterType is not null and set to Jokolia`() { var rawConfig = getConfig("working-config.conf", ConfigFactory.parseMap(mapOf("devMode" to true))) rawConfig = rawConfig.withValue("jmxReporterType", ConfigValueFactory.fromAnyRef("JOLOKIA")) - val nodeConfig = rawConfig.parseAsNodeConfiguration().orThrow() + val nodeConfig = rawConfig.parseAsNodeConfiguration().value() assertTrue(JmxReporterType.JOLOKIA.toString() == nodeConfig.jmxReporterType.toString()) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index f111057302..324f8ca6de 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -694,7 +694,7 @@ class DriverDSLImpl( * Keeping [Config] around is needed as the user may specify extra config options not specified in [NodeConfiguration]. */ private class NodeConfig(val typesafe: Config) { - val corda: NodeConfiguration = typesafe.parseAsNodeConfiguration().orThrow() + val corda: NodeConfiguration = typesafe.parseAsNodeConfiguration().value() } companion object { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt index 4da6895cbf..1b53ddc6b5 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt @@ -116,7 +116,7 @@ abstract class NodeBasedTest(private val cordappPackages: List = emptyLi val specificConfig = config.withValue(NodeConfiguration.cordappDirectoriesKey, ConfigValueFactory.fromIterable(cordappDirectories.toSet())) - val parsedConfig = specificConfig.parseAsNodeConfiguration().orThrow() + val parsedConfig = specificConfig.parseAsNodeConfiguration().value() defaultNetworkParameters.install(baseDirectory) val node = InProcessNode(parsedConfig, MOCK_VERSION_INFO.copy(platformVersion = platformVersion), flowManager = flowManager) diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt index df5889e17c..9c5600bb9b 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt @@ -39,7 +39,7 @@ class NodeConfigTest { .withFallback(ConfigFactory.parseResources("reference.conf")) .withFallback(ConfigFactory.parseMap(mapOf("devMode" to true))) .resolve() - val fullConfig = nodeConfig.parseAsNodeConfiguration().orThrow() + val fullConfig = nodeConfig.parseAsNodeConfiguration().value() // No custom configuration is created by default. assertFailsWith { nodeConfig.getConfig("custom") } diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeBuilder.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeBuilder.kt index e9e850016f..2c4cfc157d 100644 --- a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeBuilder.kt +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/nodes/NodeBuilder.kt @@ -32,7 +32,7 @@ open class NodeBuilder { .withBaseDirectory(nodeDir) .exec(BuildImageResultCallback()).awaitImageId() LOG.info("finished building docker image for: $nodeDir with id: $nodeImageId") - val config = nodeConfig.parseAsNodeConfigWithFallback(ConfigFactory.parseFile(copiedNode.configFile)).orThrow() + val config = nodeConfig.parseAsNodeConfigWithFallback(ConfigFactory.parseFile(copiedNode.configFile)).value() return copiedNode.builtNode(config, nodeImageId) } From 8f6047e3f83d393a929c476b777fd9a8e111e6f8 Mon Sep 17 00:00:00 2001 From: Tudor Malene Date: Wed, 14 Nov 2018 12:06:30 +0000 Subject: [PATCH 23/24] fix merge --- .../core/transactions/TransactionBuilder.kt | 43 ++++++++----------- 1 file changed, 19 insertions(+), 24 deletions(-) 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 31cf573072..9e6c0fceda 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -21,6 +21,7 @@ import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationFactory import net.corda.core.utilities.loggerFor import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.warnOnce import java.security.PublicKey import java.time.Duration import java.time.Instant @@ -54,16 +55,12 @@ open class TransactionBuilder @JvmOverloads constructor( ) { private companion object { - val logger = loggerFor() + private val log = contextLogger() } private val inputsWithTransactionState = arrayListOf>() private val referencesWithTransactionState = arrayListOf>() - companion object { - private val log = contextLogger() - } - /** * Creates a copy of the builder. */ @@ -182,14 +179,16 @@ open class TransactionBuilder @JvmOverloads constructor( } // For each contract, resolve the AutomaticPlaceholderConstraint, and select the attachment. - val contractAttachmentsAndResolvedOutputStates: List>?>> = allContracts.toSet().map { ctr -> - handleContract(ctr, inputContractGroups[ctr], outputContractGroups[ctr], explicitAttachmentContractsMap[ctr], serializationContext, services) - } + val contractAttachmentsAndResolvedOutputStates: List>?>> = allContracts.toSet() + .map { ctr -> + handleContract(ctr, inputContractGroups[ctr], outputContractGroups[ctr], explicitAttachmentContractsMap[ctr], serializationContext, services) + } - val resolvedStates: List> = contractAttachmentsAndResolvedOutputStates.mapNotNull { it.second }.flatten() + val resolvedStates: List> = contractAttachmentsAndResolvedOutputStates.mapNotNull { it.second } + .flatten() // The output states need to preserve the order in which they were added. - val resolvedOutputStatesInTheOriginalOrder: List> = outputStates().map { os -> resolvedStates.find { rs -> rs.data == os.data && rs.encumbrance == os.encumbrance}!! } + val resolvedOutputStatesInTheOriginalOrder: List> = outputStates().map { os -> resolvedStates.find { rs -> rs.data == os.data && rs.encumbrance == os.encumbrance }!! } val attachments: Collection = contractAttachmentsAndResolvedOutputStates.map { it.first } + refStateContractAttachments @@ -311,22 +310,18 @@ open class TransactionBuilder @JvmOverloads constructor( attachmentToUse: ContractAttachment, services: ServicesForResolution): AttachmentConstraint = when { inputStates != null -> attachmentConstraintsTransition(inputStates.groupBy { it.constraint }.keys, attachmentToUse) - useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters) -> WhitelistedByZoneAttachmentConstraint - attachmentToUse.signers.isNotEmpty() -> { - // Auto downgrade: signature constraints only available with a corda network minimum platform version of >= 4 - if (services.networkParameters.minimumPlatformVersion < 4) { - log.warn("Signature constraints not available on network requiring a minimum platform version of ${services.networkParameters.minimumPlatformVersion}") - if (useWhitelistedByZoneAttachmentConstraint(state.contract, services.networkParameters)) { - log.warn("Reverting back to using whitelisted zone constraints") - WhitelistedByZoneAttachmentConstraint - } - else { - log.warn("Reverting back to using hash constraints") - HashAttachmentConstraint(attachmentId) - } + attachmentToUse.signers.isNotEmpty() && services.networkParameters.minimumPlatformVersion < 4 -> { + log.warnOnce("Signature constraints not available on network requiring a minimum platform version of 4. Current is: ${services.networkParameters.minimumPlatformVersion}.") + if (useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters)) { + log.warnOnce("Reverting back to using whitelisted zone constraints for contract $contractClassName") + WhitelistedByZoneAttachmentConstraint + } else { + log.warnOnce("Reverting back to using hash constraints for contract $contractClassName") + HashAttachmentConstraint(attachmentToUse.id) } - else makeSignatureAttachmentConstraint(attachmentSigners) } + attachmentToUse.signers.isNotEmpty() -> makeSignatureAttachmentConstraint(attachmentToUse.signers) + useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters) -> WhitelistedByZoneAttachmentConstraint else -> HashAttachmentConstraint(attachmentToUse.id) } From 7696a570d1c2cc51680b94194efceb4f89ea5bce Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Wed, 14 Nov 2018 18:38:39 +0000 Subject: [PATCH 24/24] Merge fixes --- .idea/compiler.xml | 2 ++ build.gradle | 2 +- .../core/internal/JarSignatureCollector.kt | 6 ++--- .../corda/flowworker/FlowWorkerServiceHub.kt | 22 ++++++++++++++++--- .../corda/rpcWorker/RpcWorkerServiceHub.kt | 18 ++++++++++++++- node/src/main/java/CordaCaplet.java | 2 +- .../config/NodeConfigurationImplTest.kt | 2 +- .../net/corda/testing/driver/DriverTests.kt | 1 - 8 files changed, 44 insertions(+), 11 deletions(-) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 53159d93be..12df0021f7 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -165,6 +165,8 @@ + + diff --git a/build.gradle b/build.gradle index 03e266222c..c4ea228d4f 100644 --- a/build.gradle +++ b/build.gradle @@ -411,7 +411,7 @@ bintrayConfig { 'corda-notary-raft', 'corda-notary-bft-smart', 'corda-notary-jpa', - 'corda-notary-mysql' + 'corda-notary-mysql', 'corda-common-configuration-parsing', 'corda-common-validation' ] diff --git a/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt b/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt index 5bf200051b..505128d93a 100644 --- a/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt +++ b/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt @@ -76,9 +76,9 @@ object JarSignatureCollector { Party(it.signerCertPath.certificates[0] as X509Certificate) }.sortedBy { it.name.toString() } // Sorted for determinism. - private fun Set.toPartiesOrderedByName(): List = map { - Party(it.signerCertPath.certificates[0] as X509Certificate) - }.sortedBy { it.name.toString() } // Sorted for determinism. + private fun Set.toOrderedPublicKeys(): List = map { + (it.signerCertPath.certificates[0] as X509Certificate).publicKey + }.sortedBy { it.hash} // Sorted for determinism. private fun Set.toCertificates(): List = map { it.signerCertPath.certificates[0] as X509Certificate 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 e51433356e..e7462e69cf 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 @@ -22,7 +22,9 @@ import net.corda.core.node.NodeInfo import net.corda.core.node.services.CordaService import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.serialization.internal.* +import net.corda.core.serialization.internal.SerializationEnvironment +import net.corda.core.serialization.internal.effectiveSerializationEnv +import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.node.CordaClock @@ -62,6 +64,7 @@ import net.corda.node.utilities.EnterpriseNamedCacheFactory import net.corda.node.utilities.profiling.getTracingConfig import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.contextTransaction import net.corda.nodeapi.internal.persistence.isH2Database import net.corda.serialization.internal.* import org.apache.activemq.artemis.utils.ReusableLatch @@ -78,10 +81,15 @@ import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ExecutorService import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger +import java.util.function.Consumer +import javax.persistence.EntityManager import kotlin.reflect.KClass -class FlowWorkerServiceHub(override val configuration: NodeConfiguration, override val myInfo: NodeInfo, - private val ourKeyPair: KeyPair, private val trustRoot: X509Certificate, private val nodeCa: X509Certificate, +class FlowWorkerServiceHub(override val configuration: NodeConfiguration, + override val myInfo: NodeInfo, + private val ourKeyPair: KeyPair, + private val trustRoot: X509Certificate, + private val nodeCa: X509Certificate, private val signedNetworkParameters: NetworkParametersReader.NetworkParametersAndSigned) : ServiceHubInternal, SingletonSerializeAsToken() { override val networkParameters: NetworkParameters = signedNetworkParameters.networkParameters @@ -236,6 +244,14 @@ class FlowWorkerServiceHub(override val configuration: NodeConfiguration, overri override fun jdbcSession(): Connection = database.createSession() + override fun withEntityManager(block: EntityManager.() -> T): T { + throw NotImplementedError() + } + + override fun withEntityManager(block: Consumer) { + throw NotImplementedError() + } + override fun registerUnloadHandler(runOnStop: () -> Unit) { this.runOnStop += runOnStop } diff --git a/experimental/rpc-worker/src/main/kotlin/net/corda/rpcWorker/RpcWorkerServiceHub.kt b/experimental/rpc-worker/src/main/kotlin/net/corda/rpcWorker/RpcWorkerServiceHub.kt index 543bdeb0e3..7d392b70d3 100644 --- a/experimental/rpc-worker/src/main/kotlin/net/corda/rpcWorker/RpcWorkerServiceHub.kt +++ b/experimental/rpc-worker/src/main/kotlin/net/corda/rpcWorker/RpcWorkerServiceHub.kt @@ -49,6 +49,7 @@ import net.corda.node.utilities.EnterpriseNamedCacheFactory import net.corda.node.utilities.profiling.getTracingConfig import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.contextTransaction import net.corda.nodeapi.internal.persistence.isH2Database import net.corda.serialization.internal.* import org.slf4j.Logger @@ -59,8 +60,15 @@ import java.sql.Connection import java.time.Clock import java.time.Duration import java.util.* +import java.util.function.Consumer +import javax.persistence.EntityManager -class RpcWorkerServiceHub(override val configuration: NodeConfiguration, override val myInfo: NodeInfo, private val signedNetworkParameters: NetworkParametersReader.NetworkParametersAndSigned, private val ourKeyPair: KeyPair, private val trustRoot: X509Certificate, private val nodeCa: X509Certificate) : ServiceHubInternal, SingletonSerializeAsToken() { +class RpcWorkerServiceHub(override val configuration: NodeConfiguration, + override val myInfo: NodeInfo, + private val signedNetworkParameters: NetworkParametersReader.NetworkParametersAndSigned, + private val ourKeyPair: KeyPair, + private val trustRoot: X509Certificate, + private val nodeCa: X509Certificate) : ServiceHubInternal, SingletonSerializeAsToken() { override val clock: CordaClock = SimpleClock(Clock.systemUTC()) private val versionInfo = getVersionInfo() @@ -169,6 +177,14 @@ class RpcWorkerServiceHub(override val configuration: NodeConfiguration, overrid throw NotImplementedError() } + override fun withEntityManager(block: EntityManager.() -> T): T { + throw NotImplementedError() + } + + override fun withEntityManager(block: Consumer) { + throw NotImplementedError() + } + override fun registerUnloadHandler(runOnStop: () -> Unit) { this.runOnStop += runOnStop } diff --git a/node/src/main/java/CordaCaplet.java b/node/src/main/java/CordaCaplet.java index 0fbfbc50da..bcddb017be 100644 --- a/node/src/main/java/CordaCaplet.java +++ b/node/src/main/java/CordaCaplet.java @@ -124,7 +124,7 @@ public class CordaCaplet extends Capsule { // Create cordapps directory if it doesn't exist. if (!checkIfCordappDirExists(cordappsDir)) { // If it fails, just return the existing class path. The main Corda jar will detect the error and fail gracefully. - return cp; + return (T) cp; } // Add additional directories of JARs to the classpath (at the end), e.g., for JDBC drivers. augmentClasspath(cp, new File(baseDir, "drivers")); diff --git a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt index fe4d4a27c2..e87b2e2592 100644 --- a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt @@ -178,7 +178,7 @@ class NodeConfigurationImplTest { @Test fun `mutual exclusion machineName set to default if not explicitly set`() { - val config = getConfig("test-config-mutualExclusion-noMachineName.conf").parseAsNodeConfiguration(options = Configuration.Validation.Options(strict = false)).orThrow() + val config = getConfig("test-config-mutualExclusion-noMachineName.conf").parseAsNodeConfiguration(options = Configuration.Validation.Options(strict = false)).value() assertEquals(InetAddress.getLocalHost().hostName, config.enterpriseConfiguration.mutualExclusionConfiguration.machineName) } diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt index 7bf61e429e..ff3019f2f0 100644 --- a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt @@ -27,7 +27,6 @@ import net.corda.testing.node.internal.addressMustNotBeBound import net.corda.testing.node.internal.internalDriver import org.assertj.core.api.Assertions.* import org.json.simple.JSONObject -import org.junit.Assume.assumeTrue import org.junit.ClassRule import org.junit.Test import java.util.*