diff --git a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt index be24fe66ad..6d12af89c6 100644 --- a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt +++ b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt @@ -28,9 +28,9 @@ import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.Permissions.Companion.invokeRpc import net.corda.node.services.Permissions.Companion.startFlow -import net.corda.nodeapi.internal.config.User import net.corda.testing.* import net.corda.testing.driver.driver +import net.corda.testing.node.User import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.toDatabaseSchemaNames diff --git a/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java b/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java index 0b3646493b..5f5f53b3f8 100644 --- a/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java +++ b/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java @@ -10,8 +10,8 @@ import net.corda.finance.flows.CashPaymentFlow; import net.corda.finance.schemas.CashSchemaV1; import net.corda.node.internal.Node; import net.corda.node.internal.StartedNode; -import net.corda.nodeapi.internal.config.User; import net.corda.testing.CoreTestUtils; +import net.corda.testing.node.User; import net.corda.testing.internal.IntegrationTestKt; import net.corda.testing.internal.IntegrationTestSchemas; import net.corda.testing.node.internal.NodeBasedTest; 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 a5a3b81c40..9196b9d2de 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 @@ -20,8 +20,8 @@ import net.corda.node.internal.Node import net.corda.node.internal.StartedNode import net.corda.node.services.Permissions.Companion.invokeRpc import net.corda.node.services.Permissions.Companion.startFlow -import net.corda.nodeapi.internal.config.User import net.corda.testing.* +import net.corda.testing.node.User import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.toDatabaseSchemaName import net.corda.testing.node.internal.NodeBasedTest diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/AbstractRPCTest.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/AbstractRPCTest.kt index 11a9a027fa..4bcf3ec51f 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/AbstractRPCTest.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/AbstractRPCTest.kt @@ -5,8 +5,8 @@ import net.corda.core.internal.concurrent.flatMap import net.corda.core.internal.concurrent.map import net.corda.core.messaging.RPCOps import net.corda.node.services.messaging.RPCServerConfiguration -import net.corda.nodeapi.internal.config.User import net.corda.testing.SerializationEnvironmentRule +import net.corda.testing.node.User import net.corda.testing.node.internal.RPCDriverDSL import net.corda.testing.node.internal.rpcTestUser import net.corda.testing.node.internal.startInVmRpcClient diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt index 25ad584211..1f45c4ab13 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt @@ -2,7 +2,7 @@ package net.corda.client.rpc import net.corda.core.messaging.RPCOps import net.corda.node.services.messaging.rpcContext -import net.corda.nodeapi.internal.config.User +import net.corda.testing.node.User import net.corda.testing.node.internal.RPCDriverDSL import net.corda.testing.node.internal.rpcDriver import org.junit.Test diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt index 0fccf0cb4c..52a802ed0a 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt @@ -158,7 +158,7 @@ class NotaryFlow { */ data class TransactionParts(val id: SecureHash, val inputs: List, val timestamp: TimeWindow?, val notary: Party?) -class NotaryException(val error: NotaryError) : FlowException("Error response from Notary - $error") +class NotaryException(val error: NotaryError) : FlowException("Unable to notarise: $error") @CordaSerializable sealed class NotaryError { @@ -166,7 +166,7 @@ sealed class NotaryError { override fun toString() = "One or more input states for transaction $txId have been used in another transaction" } - /** Thrown if the time specified in the [TimeWindow] command is outside the allowed tolerance. */ + /** Occurs when time specified in the [TimeWindow] command is outside the allowed tolerance. */ object TimeWindowInvalid : NotaryError() data class TransactionInvalid(val cause: Throwable) : NotaryError() { @@ -174,4 +174,8 @@ sealed class NotaryError { } object WrongNotary : NotaryError() + + data class General(val cause: String): NotaryError() { + override fun toString() = cause + } } diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index 70724cb884..b5b8bc6f72 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -242,10 +242,14 @@ private fun IntProgression.toSpliterator(): Spliterator.OfInt { } fun IntProgression.stream(parallel: Boolean = false): IntStream = StreamSupport.intStream(toSpliterator(), parallel) - +inline fun Stream.toTypedArray() = toTypedArray(T::class.java) // When toArray has filled in the array, the component type is no longer T? but T (that may itself be nullable): -inline fun Stream.toTypedArray(): Array = uncheckedCast(toArray { size -> arrayOfNulls(size) }) +fun Stream.toTypedArray(componentType: Class): Array = toArray { size -> + uncheckedCast>(java.lang.reflect.Array.newInstance(componentType, size)) +} +fun Stream.filterNotNull(): Stream = uncheckedCast(filter(Objects::nonNull)) +fun Stream>.toMap(): Map = collect>(::LinkedHashMap, { m, (k, v) -> m.put(k, v) }, { m, t -> m.putAll(t) }) fun Class.castIfPossible(obj: Any): T? = if (isInstance(obj)) cast(obj) else null /** Returns a [DeclaredField] wrapper around the declared (possibly non-public) static field of the receiver [Class]. */ diff --git a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt index 8c6a160618..5fff5d54ce 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt @@ -78,6 +78,9 @@ abstract class TrustedAuthorityNotaryService : NotaryService() { log.warn("Notary conflicts for $txId: $conflicts") throw notaryException(txId, e) } + } catch (e: Exception) { + log.error("Internal error", e) + throw NotaryException(NotaryError.General("Service unavailable, please try again later")) } } diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index 2ae8a1f6ab..b2cf84d104 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -19,9 +19,9 @@ import net.corda.finance.flows.CashIssueFlow import net.corda.node.internal.SecureCordaRPCOps import net.corda.node.internal.StartedNode import net.corda.node.services.Permissions.Companion.startFlow -import net.corda.nodeapi.internal.config.User import net.corda.testing.ALICE_NAME import net.corda.testing.BOB_NAME +import net.corda.testing.node.User import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContractV2 import net.corda.testing.node.internal.RPCDriverDSL diff --git a/core/src/test/kotlin/net/corda/core/internal/InternalUtilsTest.kt b/core/src/test/kotlin/net/corda/core/internal/InternalUtilsTest.kt index 0a2fb69f26..12818f9a81 100644 --- a/core/src/test/kotlin/net/corda/core/internal/InternalUtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/InternalUtilsTest.kt @@ -3,6 +3,7 @@ package net.corda.core.internal import org.assertj.core.api.Assertions import org.junit.Assert.assertArrayEquals import org.junit.Test +import java.io.Serializable import java.util.stream.IntStream import java.util.stream.Stream import kotlin.test.assertEquals @@ -87,5 +88,17 @@ class InternalUtilsTest { val b: Array = Stream.of("one", "two", null).toTypedArray() assertEquals(Array::class.java, b.javaClass) assertArrayEquals(arrayOf("one", "two", null), b) + val c: Array = Stream.of("x", "y").toTypedArray(CharSequence::class.java) + assertEquals(Array::class.java, c.javaClass) + assertArrayEquals(arrayOf("x", "y"), c) + val d: Array = Stream.of("x", "y", null).toTypedArray(uncheckedCast(CharSequence::class.java)) + assertEquals(Array::class.java, d.javaClass) + assertArrayEquals(arrayOf("x", "y", null), d) + } + + @Test + fun `Stream of Pairs toMap works`() { + val m: Map, Serializable> = Stream.of, Serializable>>("x" to "y", 0 to 1, "x" to '2').toMap() + assertEquals>(mapOf("x" to '2', 0 to 1), m) } } diff --git a/docs/source/building-a-cordapp-index.rst b/docs/source/building-a-cordapp-index.rst index e71d285e93..ef4d17d334 100644 --- a/docs/source/building-a-cordapp-index.rst +++ b/docs/source/building-a-cordapp-index.rst @@ -7,6 +7,7 @@ CorDapps cordapp-overview writing-a-cordapp upgrade-notes + upgrading-cordapps cordapp-build-systems building-against-master corda-api diff --git a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt index 377b1858c4..33195c94ca 100644 --- a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt +++ b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt @@ -13,9 +13,9 @@ import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.Permissions.Companion.invokeRpc import net.corda.node.services.Permissions.Companion.startFlow -import net.corda.nodeapi.internal.config.User import net.corda.testing.* import net.corda.testing.driver.driver +import net.corda.testing.node.User import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.toDatabaseSchemaName diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt index 685a10c4ad..5e61c128b1 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt @@ -17,8 +17,8 @@ import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.Permissions.Companion.invokeRpc import net.corda.node.services.Permissions.Companion.startFlow -import net.corda.nodeapi.internal.config.User import net.corda.testing.ALICE_NAME +import net.corda.testing.node.User import net.corda.testing.driver.driver import org.graphstream.graph.Edge import org.graphstream.graph.Node diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java b/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java similarity index 97% rename from docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java rename to docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java index 0339b73754..087d632e83 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java +++ b/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java @@ -2,6 +2,7 @@ package net.corda.docs.java.tutorial.testdsl; import kotlin.Unit; import net.corda.core.contracts.PartyAndReference; +import net.corda.core.contracts.TransactionVerificationException; import net.corda.core.identity.CordaX500Name; import net.corda.finance.contracts.ICommercialPaperState; import net.corda.finance.contracts.JavaCommercialPaper; @@ -43,7 +44,8 @@ public class CommercialPaperTest { // DOCEND 1 // DOCSTART 2 - @Test + // This example test will fail with this exception. + @Test(expected = IllegalStateException.class) public void simpleCP() { ICommercialPaperState inState = getPaper(); ledger(ledgerServices, l -> { @@ -58,7 +60,8 @@ public class CommercialPaperTest { // DOCEND 2 // DOCSTART 3 - @Test + // This example test will fail with this exception. + @Test(expected = TransactionVerificationException.ContractRejection.class) public void simpleCPMove() { ICommercialPaperState inState = getPaper(); ledger(ledgerServices, l -> { diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt similarity index 96% rename from docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt rename to docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt index 1fc5f4c832..340cd1d9d9 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt @@ -2,6 +2,7 @@ package net.corda.docs.tutorial.testdsl import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.contracts.TransactionVerificationException import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.days @@ -53,7 +54,8 @@ class CommercialPaperTest { // DOCEND 1 // DOCSTART 2 - @Test + // This example test will fail with this exception. + @Test(expected = IllegalStateException::class) fun simpleCP() { val inState = getPaper() ledgerServices.ledger(DUMMY_NOTARY) { @@ -67,7 +69,8 @@ class CommercialPaperTest { // DOCEND 2 // DOCSTART 3 - @Test + // This example test will fail with this exception. + @Test(expected = TransactionVerificationException.ContractRejection::class) fun simpleCPMove() { val inState = getPaper() ledgerServices.ledger(DUMMY_NOTARY) { diff --git a/docs/source/resources/flow-interface.png b/docs/source/resources/flow-interface.png new file mode 100644 index 0000000000..ab38ad3d2b Binary files /dev/null and b/docs/source/resources/flow-interface.png differ diff --git a/docs/source/tutorial-test-dsl.rst b/docs/source/tutorial-test-dsl.rst index 6771f077cb..91d788cfd6 100644 --- a/docs/source/tutorial-test-dsl.rst +++ b/docs/source/tutorial-test-dsl.rst @@ -55,13 +55,13 @@ We will start with defining helper function that returns a ``CommercialPaper`` s .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java :language: java :start-after: DOCSTART 1 :end-before: DOCEND 1 @@ -122,13 +122,13 @@ last line of ``transaction``: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 2 :end-before: DOCEND 2 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java :language: java :start-after: DOCSTART 2 :end-before: DOCEND 2 @@ -138,13 +138,13 @@ Let's take a look at a transaction that fails. .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 3 :end-before: DOCEND 3 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java :language: java :start-after: DOCSTART 3 :end-before: DOCEND 3 @@ -167,13 +167,13 @@ However we can specify that this is an intended behaviour by changing ``verifies .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 4 :end-before: DOCEND 4 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java :language: java :start-after: DOCSTART 4 :end-before: DOCEND 4 @@ -183,13 +183,13 @@ We can continue to build the transaction until it ``verifies``: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 5 :end-before: DOCEND 5 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java :language: java :start-after: DOCSTART 5 :end-before: DOCEND 5 @@ -206,13 +206,13 @@ What should we do if we wanted to test what happens when the wrong party signs t .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 6 :end-before: DOCEND 6 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java :language: java :start-after: DOCSTART 6 :end-before: DOCEND 6 @@ -227,13 +227,13 @@ ledger with a single transaction: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 7 :end-before: DOCEND 7 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java :language: java :start-after: DOCSTART 7 :end-before: DOCEND 7 @@ -246,13 +246,13 @@ Now that we know how to define a single transaction, let's look at how to define .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 8 :end-before: DOCEND 8 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java :language: java :start-after: DOCSTART 8 :end-before: DOCEND 8 @@ -273,13 +273,13 @@ To do so let's create a simple example that uses the same input twice: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 9 :end-before: DOCEND 9 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java :language: java :start-after: DOCSTART 9 :end-before: DOCEND 9 @@ -290,13 +290,13 @@ verification (``fails()`` at the end). As in previous examples we can use ``twea .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 10 :end-before: DOCEND 10 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java :language: java :start-after: DOCSTART 10 :end-before: DOCEND 10 diff --git a/docs/source/upgrade-notes.rst b/docs/source/upgrade-notes.rst index da66072fbe..b7add79336 100644 --- a/docs/source/upgrade-notes.rst +++ b/docs/source/upgrade-notes.rst @@ -1,5 +1,5 @@ -Upgrading a CorDapp to a new version -==================================== +Upgrading a CorDapp to a new platform version +============================================= These notes provide instructions for upgrading your CorDapps from previous versions, starting with the upgrade from our first public Beta (:ref:`Milestone 12 `), to :ref:`V1.0 `. diff --git a/docs/source/upgrading-cordapps.rst b/docs/source/upgrading-cordapps.rst new file mode 100644 index 0000000000..f7f8a6b038 --- /dev/null +++ b/docs/source/upgrading-cordapps.rst @@ -0,0 +1,417 @@ +Upgrading a CorDapp (outside of platform version upgrades) +========================================================== + +.. note:: This document only concerns the upgrading of CorDapps and not the Corda platform itself (wire format, node + database schemas, etc.). + +.. contents:: + +CorDapp versioning +------------------ +The Corda platform does not mandate a version number on a per-CorDapp basis. Different elements of a CorDapp are +allowed to evolve separately: + +* States +* Contracts +* Services +* Flows +* Utilities and library functions +* All, or a subset, of the above + +Sometimes, however, a change to one element will require changes to other elements. For example, changing a shared data +structure may require flow changes that are not backwards-compatible. + +Areas of consideration +---------------------- +This document will consider the following types of versioning: + +* Flow versioning +* State and contract versioning +* State and state schema versioning +* Serialisation of custom types + +Flow versioning +--------------- +Any flow that initiates other flows must be annotated with the ``@InitiatingFlow`` annotation, which is defined as: + +.. sourcecode:: kotlin + + annotation class InitiatingFlow(val version: Int = 1) + +The ``version`` property, which defaults to 1, specifies the flow's version. This integer value should be incremented +whenever there is a release of a flow which has changes that are not backwards-compatible. A non-backwards compatible +change is one that changes the interface of the flow. + +Currently, CorDapp developers have to explicitly write logic to handle these flow version numbers. In the future, +however, the platform will use prescribed rules for handling versions. + +What defines the interface of a flow? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The flow interface is defined by the sequence of ``send`` and ``receive`` calls between an ``InitiatingFlow`` and an +``InitiatedBy`` flow, including the types of the data sent and received. We can picture a flow's interface as follows: + +.. image:: resources/flow-interface.png + :scale: 50% + :align: center + +In the diagram above, the ``InitiatingFlow``: + +* Sends an ``Int`` +* Receives a ``String`` +* Sends a ``String`` +* Receives a ``CustomType`` + +The ``InitiatedBy`` flow does the opposite: + +* Receives an ``Int`` +* Sends a ``String`` +* Receives a ``String`` +* Sends a ``CustomType`` + +As long as both the ``IntiatingFlow`` and the ``InitiatedBy`` flows conform to the sequence of actions, the flows can +be implemented in any way you see fit (including adding proprietary business logic that is not shared with other +parties). + +What constitutes a non-backwards compatible flow change? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +A flow can become backwards-incompatible in two main ways: + +* The sequence of ``send`` and ``receive`` calls changes: + + * A ``send`` or ``receive`` is added or removed from either the ``InitatingFlow`` or ``InitiatedBy`` flow + * The sequence of ``send`` and ``receive`` calls changes + +* The types of the ``send`` and ``receive`` calls changes + +What happens when running flows with incompatible versions? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Pairs of ``InitiatingFlow`` flows and ``InitiatedBy`` flows that have incompatible interfaces are likely to exhibit the +following behaviour: + +* The flows hang indefinitely and never terminate, usually because a flow expects a response which is never sent from + the other side +* One of the flow ends with an exception: "Expected Type X but Received Type Y", because the ``send`` or ``receive`` + types are incorrect +* One of the flows ends with an exception: "Counterparty flow terminated early on the other side", because one flow + sends some data to another flow, but the latter flow has already ended + +How do I upgrade my flows? +~~~~~~~~~~~~~~~~~~~~~~~~~~ +For flag-day upgrades, the process is simple. + +Assumptions +^^^^^^^^^^^ + +* All nodes in the business network can be shut down for a period of time +* All nodes retire the old flows and adopt the new flows at the same time + +Process +^^^^^^^ + +1. Update the flow and test the changes. Increment the flow version number in the ``InitiatingFlow`` annotation +2. Ensure that all versions of the existing flow have finished running and there are no pending ``SchedulableFlows`` on + any of the nodes on the business network +3. Shut down all the nodes +4. Replace the existing CorDapp JAR with the CorDapp JAR containing the new flow +5. Start the nodes + +From this point onwards, all the nodes will be using the updated flows. + +In situations where some nodes may still be using previous versions of a flow, the updated flows need to be +backwards-compatible. + +How do I ensure flow backwards-compatibility? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The ``InitiatingFlow`` version number is included in the flow session handshake and exposed to both parties via the +``FlowLogic.getFlowContext`` method. This method takes a ``Party`` and returns a ``FlowContext`` object which describes +the flow running on the other side. In particular, it has a ``flowVersion`` property which can be used to +programmatically evolve flows across versions. For example: + +.. sourcecode:: kotlin + + @Suspendable + override fun call() { + val otherFlowVersion = otherSession.getCounterpartyFlowInfo().flowVersion + val receivedString = if (otherFlowVersion == 1) { + receive(otherParty).unwrap { it.toString() } + } else { + receive(otherParty).unwrap { it } + } + } + +This code shows a flow that in its first version expected to receive an Int, but in subsequent versions was modified to +expect a String. This flow is still able to communicate with parties that are running the older CorDapp containing +the older flow. + +How do I deal with interface changes to inlined subflows? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Here is an example of an in-lined subflow: + +.. sourcecode:: kotlin + + @StartableByRPC + @InitiatingFlow + class FlowA(val recipient: Party) : FlowLogic() { + @Suspendable + override fun call() { + subFlow(FlowB(recipient)) + } + } + + @InitiatedBy(FlowA::class) + class FlowC(val otherSession: FlowSession) : FlowLogic() { + // Omitted. + } + + // Note: No annotations. This is used as an inlined subflow. + class FlowB(val recipient: Party) : FlowLogic() { + @Suspendable + override fun call() { + val message = "I'm an inlined subflow, so I inherit the @InitiatingFlow's session ID and type." + initiateFlow(recipient).send(message) + } + } + +Inlined subflows are treated as being the flow that invoked them when initiating a new flow session with a counterparty. +Suppose flow ``A`` calls inlined subflow B, which, in turn, initiates a session with a counterparty. The ``FlowLogic`` +type used by the counterparty to determine which counter-flow to invoke is determined by ``A``, and not by ``B``. This +means that the response logic for the inlined flow must be implemented explicitly in the ``InitiatedBy`` flow. This can +be done either by calling a matching inlined counter-flow, or by implementing the other side explicitly in the +initiated parent flow. Inlined subflows also inherit the session IDs of their parent flow. + +As such, an interface change to an inlined subflow must be considered a change to the parent flow interfaces. + +An example of an inlined subflow is ``CollectSignaturesFlow``. It has a response flow called ``SignTransactionFlow`` +that isn’t annotated with ``InitiatedBy``. This is because both of these flows are inlined. How these flows speak to +one another is defined by the parent flows that call ``CollectSignaturesFlow`` and ``SignTransactionFlow``. + +In code, inlined subflows appear as regular ``FlowLogic`` instances without either an ``InitiatingFlow`` or an +``InitiatedBy`` annotation. + +Inlined flows are not versioned, as they inherit the version of their parent ``InitiatingFlow`` or ``InitiatedBy`` +flow. + +Are there any other considerations? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Suspended flows +^^^^^^^^^^^^^^^ +Currently, serialised flow state machines persisted in the node's database cannot be updated. All flows must finish +before the updated flow classes are added to the node's plugins folder. + +Flows that don't create sessions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Flows which are not an ``InitiatingFlow`` or ``InitiatedBy`` flow, or inlined subflows that are not called from an +``InitiatingFlow`` or ``InitiatedBy`` flow, can be updated without consideration of backwards-compatibility. Flows of +this type include utility flows for querying the vault and flows for reaching out to external systems. + +Contract and state versioning +----------------------------- +Contracts and states can be upgraded if and only if all of the state's participants agree to the proposed upgrade. The +following combinations of upgrades are possible: + +* A contract is upgraded while the state definition remains the same +* A state is upgraded while the contract stays the same +* The state and the contract are updated simultaneously + +The procedure for updating a state or a contract using a flag-day approach is quite simple: + +* Update and test the state or contract +* Stop all the nodes on the business network +* Produce a new CorDapp JAR file and distribute it to all the relevant parties +* Start all nodes on the network +* Run the contract upgrade authorisation flow for each state that requires updating on every node +* For each state, one node should run the contract upgrade initiation flow + +Update Process +~~~~~~~~~~~~~~ + +Writing the new state and contract definitions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Start by updating the contract and/or state definitions. There are no restrictions on how states are updated. However, +upgraded contracts must implement the ``UpgradedContract`` interface. This interface is defined as: + +.. sourcecode:: kotlin + + interface UpgradedContract : Contract { + val legacyContract: ContractClassName + fun upgrade(state: OldState): NewState + } + +The ``upgrade`` method describes how the old state type is upgraded to the new state type. When the state isn't being +upgraded, the same state type can be used for both the old and new state type parameters. + +Authorising the upgrade +^^^^^^^^^^^^^^^^^^^^^^^ +Once the new states and contracts are on the classpath for all the relevant nodes, the next step is for all nodes to +run the ``ContractUpgradeFlow.Authorise`` flow. This flow takes a ``StateAndRef`` of the state to update as well as a +reference to the new contract, which must implement the ``UpgradedContract`` interface. + +At any point, a node administrator may de-authorise a contract upgrade by running the +``ContractUpgradeFlow.Deauthorise`` flow. + +Performing the upgrade +^^^^^^^^^^^^^^^^^^^^^^ +Once all nodes have performed the authorisation process, a participant must be chosen to initiate the upgrade via the +``ContractUpgradeFlow.Initiate`` flow for each state object. This flow has the following signature: + +.. sourcecode:: kotlin + + class Initiate( + originalState: StateAndRef, + newContractClass: Class> + ) : AbstractStateReplacementFlow.Instigator>>(originalState, newContractClass) + +This flow sub-classes ``AbstractStateReplacementFlow``, which can be used to upgrade state objects that do not need a +contract upgrade. + +One the flow ends successfully, all the participants of the old state object should have the upgraded state object +which references the new contract code. + +Points to note +~~~~~~~~~~~~~~ + +Capabilities of the contract upgrade flows +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* Despite its name, the ``ContractUpgradeFlow`` also handles the update of state object definitions +* The state can completely change as part of an upgrade! For example, it is possible to transmute a ``Cat`` state into + a ``Dog`` state, provided that all participants in the ``Cat`` state agree to the change +* Equally, the state doesn't have to change at all +* If a node has not yet run the contract upgrade authorisation flow, they will not be able to upgrade the contract + and/or state objects +* Upgrade authorisations can subsequently be deauthorised +* Upgrades do not have to happen immediately. For a period, the two parties can use the old states and contracts + side-by-side +* State schema changes are handled separately + +Writing new states and contracts +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* If a property is removed from a state, any references to it must be removed from the contract code. Otherwise, you + will not be able to compile your contract code. It is generally not advisable to remove properties from states. Mark + them as deprecated instead +* When adding properties to a state, consider how the new properties will affect transaction validation involving this + state. If the contract is not updated to add constraints over the new properties, they will be able to take on any + value +* Updated state objects can use the old contract code as long as there is no requirement to update it + +Dealing with old contract code JAR files +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* Currently, all parties **must** keep the old state and contract definitions on their node's classpath as they will + always be required to verify transactions involving previous versions of the state using previous versions of the + contract + + * This will change when the contract code as an attachment feature has been fully implemented. + +Permissioning +^^^^^^^^^^^^^ +* Only node administrators are able to run the contract upgrade authorisation and deauthorisation flows + +Logistics +^^^^^^^^^ +* All nodes need to run the contract upgrade authorisation flow +* Only one node should run the contract upgrade initiation flow. If multiple nodes run it for the same ``StateRef``, a + double-spend will occur for all but the first completed upgrade +* The supplied upgrade flows upgrade one state object at a time + +Serialisation +------------- + +Currently, the serialisation format for everything except flow checkpoints (which uses a Kryo-based format) is based +upon AMQP 1.0, a self-describing and controllable serialisation format. AMQP is desirable because it allows us to have +a schema describing what has been serialized alongside the data itself. This assists with versioning and deserialising +long-ago archived data, among other things. + +Writing classes +~~~~~~~~~~~~~~~ +Although not strictly related to versioning, AMQP serialisation dictates that we must write our classes in a particular way: + +* Your class must have a constructor that takes all the properties that you wish to record in the serialized form. This + is required in order for the serialization framework to reconstruct an instance of your class +* If more than one constructor is provided, the serialization framework needs to know which one to use. The + ``@ConstructorForDeserialization`` annotation can be used to indicate the chosen constructor. For a Kotlin class + without the ``@ConstructorForDeserialization`` annotation, the primary constructor is selected +* The class must be compiled with parameter names in the .class file. This is the default in Kotlin but must be turned + on in Java (using the ``-parameters`` command line option to ``javac``) +* Your class must provide a Java Bean getter for each of the properties in the constructor, with a matching name. For + example, if a class has the constructor parameter ``foo``, there must be a getter called ``getFoo()``. If ``foo`` is + a boolean, the getter may optionally be called ``isFoo()``. This is why the class must be compiled with parameter + names turned on +* The class must be annotated with ``@CordaSerializable`` +* The declared types of constructor arguments/getters must be supported, and where generics are used the generic + parameter must be a supported type, an open wildcard (*), or a bounded wildcard which is currently widened to an open + wildcard +* Any superclass must adhere to the same rules, but can be abstract +* Object graph cycles are not supported, so an object cannot refer to itself, directly or indirectly + +Writing enums +~~~~~~~~~~~~~ +Elements cannot be added to enums in a new version of the code. Hence, enums are only a good fit for genuinely static +data that will never change (e.g. days of the week). A ``Buy`` or ``Sell`` flag is another. However, something like +``Trade Type`` or ``Currency Code`` will likely change. For those, it is preferable to choose another representation, +such as a string. + +State schemas +------------- +By default, all state objects are serialised to the database as a string of bytes and referenced by their ``StateRef``. +However, it is also possible to define custom schemas for serialising particular properties or combinations of +properties, so that they can be queried from a source other than the Corda Vault. This is done by implementing the +``QueryableState`` interface and creating a custom object relational mapper for the state. See :doc:`api-persistence` +for details. + +For backwards compatible changes such as adding columns, the procedure for upgrading a state schema is to extend the +existing object relational mapper. For example, we can update: + +.. sourcecode:: kotlin + + object ObligationSchemaV1 : MappedSchema(Obligation::class.java, 1, listOf(ObligationEntity::class.java)) { + @Entity @Table(name = "obligations") + class ObligationEntity(obligation: Obligation) : PersistentState() { + @Column var currency: String = obligation.amount.token.toString() + @Column var amount: Long = obligation.amount.quantity + @Column @Lob var lender: ByteArray = obligation.lender.owningKey.encoded + @Column @Lob var borrower: ByteArray = obligation.borrower.owningKey.encoded + @Column var linear_id: String = obligation.linearId.id.toString() + } + } + +To: + +.. sourcecode:: kotlin + + object ObligationSchemaV1 : MappedSchema(Obligation::class.java, 1, listOf(ObligationEntity::class.java)) { + @Entity @Table(name = "obligations") + class ObligationEntity(obligation: Obligation) : PersistentState() { + @Column var currency: String = obligation.amount.token.toString() + @Column var amount: Long = obligation.amount.quantity + @Column @Lob var lender: ByteArray = obligation.lender.owningKey.encoded + @Column @Lob var borrower: ByteArray = obligation.borrower.owningKey.encoded + @Column var linear_id: String = obligation.linearId.id.toString() + @Column var defaulted: Bool = obligation.amount.inDefault // NEW COLUNM! + } + } + +Thus adding a new column with a default value. + +To make a non-backwards compatible change, the ``ContractUpgradeFlow`` or ``AbstractStateReplacementFlow`` must be +used, as changes to the state are required. To make a backwards-incompatible change such as deleting a column (e.g. +because a property was removed from a state object), the procedure is to define another object relational mapper, then +add it to the ``supportedSchemas`` property of your ``QueryableState``, like so: + +.. sourcecode:: kotlin + + override fun supportedSchemas(): Iterable = listOf(ExampleSchemaV1, ExampleSchemaV2) + +Then, in ``generateMappedObject``, add support for the new schema: + +.. sourcecode:: kotlin + + override fun generateMappedObject(schema: MappedSchema): PersistentState { + return when (schema) { + is DummyLinearStateSchemaV1 -> // Omitted. + is DummyLinearStateSchemaV2 -> // Omitted. + else -> throw IllegalArgumentException("Unrecognised schema $schema") + } + } + +With this approach, whenever the state object is stored in the vault, a representation of it will be stored in two +separate database tables where possible - one for each supported schema. \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt index 9811800de0..34b5bf7784 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt @@ -53,7 +53,8 @@ class ArtemisTcpTransport { // It does not use AMQP messages for its own messages e.g. topology and heartbeats. // TODO further investigate how to ensure we use a well defined wire level protocol for Node to Node communications. TransportConstants.PROTOCOLS_PROP_NAME to "CORE,AMQP", - TransportConstants.USE_GLOBAL_WORKER_POOL_PROP_NAME to (nodeSerializationEnv != null) + TransportConstants.USE_GLOBAL_WORKER_POOL_PROP_NAME to (nodeSerializationEnv != null), + TransportConstants.REMOTING_THREADS_PROPNAME to (if (nodeSerializationEnv != null) -1 else 1) ) if (config != null && enableSSL) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt index 9ccc286f4f..0965db3a42 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt @@ -12,6 +12,8 @@ import net.corda.nodeapi.internal.config.NodeSSLConfiguration import net.corda.nodeapi.internal.crypto.* import org.slf4j.LoggerFactory import java.nio.file.Path +import java.security.KeyPair +import java.security.PublicKey /** * Contains utility methods for generating identities for a node. @@ -42,33 +44,47 @@ object DevIdentityGenerator { return identity.party } - fun generateDistributedNotaryIdentity(dirs: List, notaryName: CordaX500Name, threshold: Int = 1): Party { + fun generateDistributedNotaryCompositeIdentity(dirs: List, notaryName: CordaX500Name, threshold: Int = 1): Party { require(dirs.isNotEmpty()) - log.trace { "Generating identity \"$notaryName\" for nodes: ${dirs.joinToString()}" } + log.trace { "Generating composite identity \"$notaryName\" for nodes: ${dirs.joinToString()}" } val keyPairs = (1..dirs.size).map { generateKeyPair() } - val compositeKey = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold) - + val notaryKey = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold) keyPairs.zip(dirs) { keyPair, nodeDir -> - val (serviceKeyCert, compositeKeyCert) = listOf(keyPair.public, compositeKey).map { publicKey -> - X509Utilities.createCertificate( - CertificateType.SERVICE_IDENTITY, - DEV_INTERMEDIATE_CA.certificate, - DEV_INTERMEDIATE_CA.keyPair, - notaryName.x500Principal, - publicKey) - } - val distServKeyStoreFile = (nodeDir / "certificates").createDirectories() / "distributedService.jks" - val keystore = loadOrCreateKeyStore(distServKeyStoreFile, "cordacadevpass") - keystore.setCertificateEntry("$DISTRIBUTED_NOTARY_ALIAS_PREFIX-composite-key", compositeKeyCert) - keystore.setKeyEntry( - "$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key", - keyPair.private, - "cordacadevkeypass".toCharArray(), - arrayOf(serviceKeyCert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate)) - keystore.save(distServKeyStoreFile, "cordacadevpass") + generateCertificates(keyPair, notaryKey, notaryName, nodeDir) } + return Party(notaryName, notaryKey) + } - return Party(notaryName, compositeKey) + fun generateDistributedNotarySingularIdentity(dirs: List, notaryName: CordaX500Name): Party { + require(dirs.isNotEmpty()) + + log.trace { "Generating singular identity \"$notaryName\" for nodes: ${dirs.joinToString()}" } + val keyPair = generateKeyPair() + val notaryKey = keyPair.public + dirs.forEach { dir -> + generateCertificates(keyPair, notaryKey, notaryName, dir) + } + return Party(notaryName, notaryKey) + } + + private fun generateCertificates(keyPair: KeyPair, notaryKey: PublicKey, notaryName: CordaX500Name, nodeDir: Path) { + val (serviceKeyCert, compositeKeyCert) = listOf(keyPair.public, notaryKey).map { publicKey -> + X509Utilities.createCertificate( + CertificateType.SERVICE_IDENTITY, + DEV_INTERMEDIATE_CA.certificate, + DEV_INTERMEDIATE_CA.keyPair, + notaryName.x500Principal, + publicKey) + } + val distServKeyStoreFile = (nodeDir / "certificates").createDirectories() / "distributedService.jks" + val keystore = loadOrCreateKeyStore(distServKeyStoreFile, "cordacadevpass") + keystore.setCertificateEntry("$DISTRIBUTED_NOTARY_ALIAS_PREFIX-composite-key", compositeKeyCert) + keystore.setKeyEntry( + "$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key", + keyPair.private, + "cordacadevkeypass".toCharArray(), + arrayOf(serviceKeyCert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate)) + keystore.save(distServKeyStoreFile, "cordacadevpass") } } diff --git a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt index 4cbb60a068..cf0adc7f28 100644 --- a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt @@ -8,8 +8,11 @@ import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow import net.corda.node.internal.NodeStartup import net.corda.node.services.Permissions.Companion.startFlow -import net.corda.nodeapi.internal.config.User -import net.corda.testing.* +import net.corda.testing.ALICE_NAME +import net.corda.testing.BOB_NAME +import net.corda.testing.DUMMY_BANK_A_NAME +import net.corda.testing.DUMMY_NOTARY_NAME +import net.corda.testing.node.User import net.corda.testing.common.internal.ProjectStructure.projectRootDir import net.corda.testing.driver.driver import net.corda.testing.internal.IntegrationTest diff --git a/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt b/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt index d4a97c24fd..ca0abe8259 100644 --- a/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt @@ -8,9 +8,9 @@ import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap import net.corda.node.services.Permissions.Companion.startFlow -import net.corda.nodeapi.internal.config.User import net.corda.testing.* import net.corda.testing.driver.driver +import net.corda.testing.node.User import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.toDatabaseSchemaName diff --git a/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt b/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt index dc891392cf..810aa0b3f7 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt @@ -26,7 +26,7 @@ class NodeKeystoreCheckTest : IntegrationTest() { @Test fun `starting node in non-dev mode with no key store`() { - driver(startNodesInProcess = true) { + driver(startNodesInProcess = true, notarySpecs = emptyList()) { assertThatThrownBy { startNode(customOverrides = mapOf("devMode" to false)).getOrThrow() }.hasMessageContaining("Identity certificate not found") @@ -35,7 +35,7 @@ class NodeKeystoreCheckTest : IntegrationTest() { @Test fun `node should throw exception if cert path doesn't chain to the trust root`() { - driver(startNodesInProcess = true) { + driver(startNodesInProcess = true, notarySpecs = emptyList()) { // Create keystores val keystorePassword = "password" val config = object : SSLConfiguration { diff --git a/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt index 8e7e8f547f..8332c95acc 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt @@ -13,8 +13,10 @@ import net.corda.finance.DOLLARS import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.Permissions.Companion.startFlow -import net.corda.nodeapi.internal.config.User -import net.corda.testing.* +import net.corda.testing.ALICE_NAME +import net.corda.testing.DUMMY_BANK_A_NAME +import net.corda.testing.DUMMY_NOTARY_NAME +import net.corda.testing.TestIdentity import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.driver @@ -24,6 +26,7 @@ import net.corda.testing.internal.performance.div import net.corda.testing.internal.toDatabaseSchemaName import net.corda.testing.internal.toDatabaseSchemaNames import net.corda.testing.node.NotarySpec +import net.corda.testing.node.User import net.corda.testing.node.internal.InternalDriverDSL import net.corda.testing.node.internal.performance.startPublishingFixedRateInjector import net.corda.testing.node.internal.performance.startReporter diff --git a/node/src/integration-test/kotlin/net/corda/node/SSHServerTest.kt b/node/src/integration-test/kotlin/net/corda/node/SSHServerTest.kt index 3ec641e49f..3608e42630 100644 --- a/node/src/integration-test/kotlin/net/corda/node/SSHServerTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/SSHServerTest.kt @@ -12,16 +12,15 @@ import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap import net.corda.node.services.Permissions.Companion.startFlow -import net.corda.nodeapi.internal.config.User import net.corda.testing.ALICE_NAME import net.corda.testing.driver.driver import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.toDatabaseSchemaName +import net.corda.testing.node.User import org.assertj.core.api.Assertions.assertThat import org.bouncycastle.util.io.Streams import org.junit.ClassRule -import org.junit.Ignore import org.junit.Test import java.net.ConnectException import java.util.regex.Pattern @@ -34,7 +33,6 @@ class SSHServerTest : IntegrationTest() { val databaseSchemas = IntegrationTestSchemas(ALICE_NAME.toDatabaseSchemaName()) } - @Ignore("Test has undeterministic capacity to hang, ignore till fixed") @Test() fun `ssh server does not start be default`() { val user = User("u", "p", setOf()) @@ -56,7 +54,6 @@ class SSHServerTest : IntegrationTest() { } } - @Ignore("Test has undeterministic capacity to hang, ignore till fixed") @Test fun `ssh server starts when configured`() { val user = User("u", "p", setOf()) @@ -76,8 +73,6 @@ class SSHServerTest : IntegrationTest() { } } - - @Ignore("Test has undeterministic capacity to hang, ignore till fixed") @Test fun `ssh server verify credentials`() { val user = User("u", "p", setOf()) @@ -101,7 +96,6 @@ class SSHServerTest : IntegrationTest() { } } - @Ignore("Test has undeterministic capacity to hang, ignore till fixed") @Test fun `ssh respects permissions`() { val user = User("u", "p", setOf(startFlow())) @@ -132,7 +126,6 @@ class SSHServerTest : IntegrationTest() { } } - @Ignore("Test has undeterministic capacity to hang, ignore till fixed") @Test fun `ssh runs flows`() { val user = User("u", "p", setOf(startFlow())) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt index 38880aef08..a9d190355a 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt @@ -28,7 +28,6 @@ import net.corda.testing.internal.* import net.corda.testing.services.MockAttachmentStorage import org.junit.Assert.assertEquals import org.junit.ClassRule -import org.junit.Ignore import org.junit.Rule import org.junit.Test import java.net.URLClassLoader @@ -104,7 +103,6 @@ class AttachmentLoadingTests : IntegrationTest() { assertEquals(expected, actual) } - @Ignore("Test has undeterministic capacity to hang, ignore till fixed") @Test fun `test that attachments retrieved over the network are not used for code`() = withoutTestSerialization { driver { @@ -117,7 +115,6 @@ class AttachmentLoadingTests : IntegrationTest() { Unit } - @Ignore("Test has undeterministic capacity to hang, ignore till fixed") @Test fun `tests that if the attachment is loaded on both sides already that a flow can run`() = withoutTestSerialization { driver { diff --git a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt index 00842db789..9230e85065 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt @@ -69,7 +69,7 @@ class BFTNotaryServiceTests : IntegrationTest() { (Paths.get("config") / "currentView").deleteIfExists() // XXX: Make config object warn if this exists? val replicaIds = (0 until clusterSize) - notary = DevIdentityGenerator.generateDistributedNotaryIdentity( + notary = DevIdentityGenerator.generateDistributedNotaryCompositeIdentity( replicaIds.map { mockNet.baseDirectory(mockNet.nextNodeId + it) }, CordaX500Name("BFT", "Zurich", "CH")) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt index dd76235b88..6512e9e083 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt @@ -13,7 +13,6 @@ import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.Permissions.Companion.invokeRpc import net.corda.node.services.Permissions.Companion.startFlow -import net.corda.nodeapi.internal.config.User import net.corda.testing.* import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver @@ -23,6 +22,8 @@ import net.corda.testing.internal.toDatabaseSchemaName import net.corda.testing.internal.toDatabaseSchemaNames import net.corda.testing.node.ClusterSpec import net.corda.testing.node.NotarySpec +import net.corda.testing.node.User +import net.corda.testing.node.internal.DummyClusterSpec import org.assertj.core.api.Assertions.assertThat import org.junit.ClassRule import org.junit.Ignore @@ -41,18 +42,22 @@ class DistributedServiceTests : IntegrationTest() { val databaseSchemas = IntegrationTestSchemas(*DUMMY_NOTARY_NAME.toDatabaseSchemaNames("_0", "_1", "_2").toTypedArray(), ALICE_NAME.toDatabaseSchemaName()) } - private fun setup(testBlock: () -> Unit) { + private fun setup(compositeIdentity: Boolean = false, testBlock: () -> Unit) { val testUser = User("test", "test", permissions = setOf( startFlow(), startFlow(), invokeRpc(CordaRPCOps::nodeInfo), invokeRpc(CordaRPCOps::stateMachinesFeed)) ) - driver( extraCordappPackagesToScan = listOf("net.corda.finance.contracts", "net.corda.finance.schemas"), - notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, rpcUsers = listOf(testUser), cluster = ClusterSpec.Raft(clusterSize = 3)))) - { + notarySpecs = listOf( + NotarySpec( + DUMMY_NOTARY_NAME, + rpcUsers = listOf(testUser), + cluster = DummyClusterSpec(clusterSize = 3, compositeServiceIdentity = compositeIdentity)) + ) + ) { alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(testUser)).getOrThrow() raftNotaryIdentity = defaultNotaryIdentity notaryNodes = defaultNotaryHandle.nodeHandles.getOrThrow().map { it as NodeHandle.OutOfProcess } @@ -81,11 +86,60 @@ class DistributedServiceTests : IntegrationTest() { } } + // TODO This should be in RaftNotaryServiceTests + @Test + fun `cluster survives if a notary is killed`() { + setup { + // Issue 100 pounds, then pay ourselves 10x5 pounds + issueCash(100.POUNDS) + + for (i in 1..10) { + paySelf(5.POUNDS) + } + + // Now kill a notary node + with(notaryNodes[0].process) { + destroy() + waitFor() + } + + // Pay ourselves another 20x5 pounds + for (i in 1..20) { + paySelf(5.POUNDS) + } + + val notarisationsPerNotary = HashMap() + notaryStateMachines.expectEvents(isStrict = false) { + replicate>(30) { + expect(match = { it.second is StateMachineUpdate.Added }) { (notary, update) -> + update as StateMachineUpdate.Added + notarisationsPerNotary.compute(notary) { _, number -> number?.plus(1) ?: 1 } + } + } + } + + println("Notarisation distribution: $notarisationsPerNotary") + require(notarisationsPerNotary.size == 3) + } + } + // TODO Use a dummy distributed service rather than a Raft Notary Service as this test is only about Artemis' ability // to handle distributed services - @Ignore("Test has undeterministic capacity to hang, ignore till fixed") @Test - fun `requests are distributed evenly amongst the nodes`() = setup { + fun `requests are distributed evenly amongst the nodes`() { + setup { + checkRequestsDistributedEvenly() + } + } + + @Test + fun `requests are distributed evenly amongst the nodes with a composite public key`() { + setup(true) { + checkRequestsDistributedEvenly() + } + } + + private fun checkRequestsDistributedEvenly() { // Issue 100 pounds, then pay ourselves 50x2 pounds issueCash(100.POUNDS) @@ -111,42 +165,6 @@ class DistributedServiceTests : IntegrationTest() { require(notarisationsPerNotary.values.all { it > 10 }) } - // TODO This should be in RaftNotaryServiceTests - @Ignore("Test has undeterministic capacity to hang, ignore till fixed") - @Test - fun `cluster survives if a notary is killed`() = setup { - // Issue 100 pounds, then pay ourselves 10x5 pounds - issueCash(100.POUNDS) - - for (i in 1..10) { - paySelf(5.POUNDS) - } - - // Now kill a notary node - with(notaryNodes[0].process) { - destroy() - waitFor() - } - - // Pay ourselves another 20x5 pounds - for (i in 1..20) { - paySelf(5.POUNDS) - } - - val notarisationsPerNotary = HashMap() - notaryStateMachines.expectEvents(isStrict = false) { - replicate>(30) { - expect(match = { it.second is StateMachineUpdate.Added }) { (notary, update) -> - update as StateMachineUpdate.Added - notarisationsPerNotary.compute(notary) { _, number -> number?.plus(1) ?: 1 } - } - } - } - - println("Notarisation distribution: $notarisationsPerNotary") - require(notarisationsPerNotary.size == 3) - } - private fun issueCash(amount: Amount) { aliceProxy.startFlow(::CashIssueFlow, amount, OpaqueBytes.of(0), raftNotaryIdentity).returnValue.getOrThrow() } diff --git a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt index ef28163986..e263effb60 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt @@ -36,7 +36,6 @@ class RaftNotaryServiceTests : IntegrationTest() { } private val notaryName = CordaX500Name("RAFT Notary Service", "London", "GB") - @Ignore("Test has undeterministic capacity to hang, ignore till fixed") @Test fun `detect double spend`() { driver( diff --git a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt index 156fd71f0d..682e912ea7 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt @@ -8,7 +8,6 @@ import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.startFlow import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow -import net.corda.nodeapi.internal.config.User import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState @@ -17,6 +16,7 @@ import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.toDatabaseSchemaName import org.junit.ClassRule +import net.corda.testing.node.User import org.junit.Test import kotlin.test.assertEquals diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsRPCTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsRPCTest.kt index eab85f937a..4609aab319 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsRPCTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsRPCTest.kt @@ -1,6 +1,6 @@ package net.corda.services.messaging -import net.corda.nodeapi.internal.config.User +import net.corda.testing.node.User import org.junit.Test /** diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt index 201b9fa506..d1a69c9be5 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt @@ -24,11 +24,13 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATI import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX import net.corda.nodeapi.internal.config.SSLConfiguration -import net.corda.nodeapi.internal.config.User -import net.corda.testing.* +import net.corda.testing.ALICE_NAME +import net.corda.testing.BOB_NAME +import net.corda.testing.chooseIdentity import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.configureTestSSL import net.corda.testing.internal.toDatabaseSchemaName +import net.corda.testing.node.User import net.corda.testing.node.internal.NodeBasedTest import net.corda.testing.node.startFlow import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException diff --git a/node/src/integration-test/kotlin/net/corda/test/node/NodeStatePersistenceTests.kt b/node/src/integration-test/kotlin/net/corda/test/node/NodeStatePersistenceTests.kt index 3021f4a699..22ce0e45f2 100644 --- a/node/src/integration-test/kotlin/net/corda/test/node/NodeStatePersistenceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/test/node/NodeStatePersistenceTests.kt @@ -19,7 +19,7 @@ import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.getOrThrow import net.corda.node.services.Permissions.Companion.invokeRpc import net.corda.node.services.Permissions.Companion.startFlow -import net.corda.nodeapi.internal.config.User +import net.corda.testing.node.User import net.corda.testing.* import net.corda.testing.driver.driver import net.corda.testing.internal.IntegrationTest diff --git a/node/src/main/kotlin/net/corda/lazyhub/LazyHub.kt b/node/src/main/kotlin/net/corda/lazyhub/LazyHub.kt new file mode 100644 index 0000000000..512fa1c74e --- /dev/null +++ b/node/src/main/kotlin/net/corda/lazyhub/LazyHub.kt @@ -0,0 +1,109 @@ +package net.corda.lazyhub + +import net.corda.core.serialization.CordaSerializable +import kotlin.reflect.KClass +import kotlin.reflect.KFunction + +/** Supertype of all exceptions thrown directly by [LazyHub]. */ +@CordaSerializable +abstract class LazyHubException(message: String) : RuntimeException(message) + +/** The type can't be instantiated because it is abstract, i.e. it's an interface or abstract class. */ +class AbstractTypeException(message: String) : LazyHubException(message) + +/** + * The class can't be instantiated because it has no public constructor. + * This is so that you can easily hide a constructor from LazyHub by making it non-public. + */ +class NoPublicConstructorsException(message: String) : LazyHubException(message) + +/** + * Nullable factory return types are not supported, as LazyHub has no concept of a provider that MAY supply an object. + * If you want an optional result, use logic to decide whether to add the factory to the lazyHub. + */ +class NullableReturnTypeException(message: String) : LazyHubException(message) + +/** The parameter can't be satisfied and doesn't have a default and isn't nullable. */ +abstract class UnsatisfiableParamException(message: String) : LazyHubException(message) + +/** No provider has been registered for the wanted type. */ +class NoSuchProviderException(message: String) : UnsatisfiableParamException(message) + +/** + * No provider has been registered for the component type of the wanted array. + * Note that LazyHub does not create empty arrays, make the array param type nullable to accept no elements. + * This allows you to express zero-or-more (nullable) or one-or-more via the parameter type. + */ +class UnsatisfiableArrayException(message: String) : UnsatisfiableParamException(message) + +/** More than one provider has been registered for the type but at most one object is wanted. */ +class TooManyProvidersException(message: String) : UnsatisfiableParamException(message) + +/** + * More than one public constructor is satisfiable and there is no clear winner. + * The winner is the constructor with the most params for which LazyHub actually supplies an arg. + */ +class NoUniqueGreediestSatisfiableConstructorException(message: String) : LazyHubException(message) + +/** The object being created depends on itself, i.e. it's already being instantiated/factoried. */ +class CircularDependencyException(message: String) : LazyHubException(message) + +/** Depend on this as a param (and add the [MutableLazyHub], which is a [LazyHubFactory], to itself) if you want to make child containers. */ +interface LazyHubFactory { + fun child(): MutableLazyHub +} + +/** + * Read-only interface to the lazyHub. + * Where possible, always obtain your object via a constructor/method param instead of directly from the [LazyHub]. + * This results in the greatest automatic benefits to the codebase e.g. separation of concerns and ease of testing. + * A notable exception to this rule is `getAll(Unit::class)` to (idempotently) run all side-effects. + */ +interface LazyHub : LazyHubFactory { + operator fun get(clazz: KClass) = get(clazz.java) + operator fun get(clazz: Class) = getOrNull(clazz) ?: throw NoSuchProviderException(clazz.toString()) + fun getAll(clazz: KClass) = getAll(clazz.java) + fun getAll(clazz: Class): List + fun getOrNull(clazz: KClass) = getOrNull(clazz.java) + fun getOrNull(clazz: Class): T? +} + +/** Fully-featured interface to the lazyHub. */ +interface MutableLazyHub : LazyHub { + /** Register the given object against its class and all supertypes. */ + fun obj(obj: Any) + + /** Like plain old [MutableLazyHub.obj] but removes all [service] providers first. */ + fun obj(service: KClass, obj: T) + + /** + * Register the given class as a provider for itself and all supertypes. + * The class is instantiated at most once, using the greediest public constructor satisfiable at the time. + */ + fun impl(impl: KClass<*>) + + /** + * Same as [MutableLazyHub.impl] if you don't have a static reference to the class. + * Note that Kotlin features such as nullable params and default args will not be available. + */ + fun impl(impl: Class<*>) + + /** Like plain old [MutableLazyHub.impl] but removes all [service] providers first. */ + fun impl(service: KClass, impl: KClass) + + /** Like the [KClass] variant if you don't have a static reference fo the class. */ + fun impl(service: KClass, impl: Class) + + /** + * Register the given function as a provider for its **declared** return type and all supertypes. + * The function is invoked at most once. Unlike constructors, the function may have any visibility. + * By convention the function should have side-effects iff its return type is [Unit]. + */ + fun factory(factory: KFunction<*>) + + /** Register a factory that provides the given type from the given hub. */ + fun factory(lh: LazyHub, type: KClass<*>) + + /** Like plain old [MutableLazyHub.factory] but removes all [service] providers first. */ + fun factory(service: KClass, factory: KFunction) +} diff --git a/node/src/main/kotlin/net/corda/lazyhub/LazyHubImpl.kt b/node/src/main/kotlin/net/corda/lazyhub/LazyHubImpl.kt new file mode 100644 index 0000000000..5edbb4027b --- /dev/null +++ b/node/src/main/kotlin/net/corda/lazyhub/LazyHubImpl.kt @@ -0,0 +1,200 @@ +package net.corda.lazyhub + +import net.corda.core.internal.filterNotNull +import net.corda.core.internal.toTypedArray +import net.corda.core.internal.uncheckedCast +import net.corda.lazyhub.JConcrete.Companion.validate +import net.corda.lazyhub.KConcrete.Companion.validate +import net.corda.lazyhub.KConstructor.Companion.validate +import java.util.* +import java.util.concurrent.Callable +import java.util.stream.Stream +import kotlin.reflect.KClass +import kotlin.reflect.KFunction + +/** + * Create a new [MutableLazyHub] with no parent. + * + * Basic usage: + * * Add classes/factories/objects to the LazyHub using [MutableLazyHub.impl], [MutableLazyHub.factory] and [MutableLazyHub.obj] + * * Then ask it for a type using [LazyHub.get] and it will create (and cache) the object graph for you + * * You can use [LazyHub.getAll] to get all objects of a type, e.g. by convention pass in [Unit] to run side-effects + * + * How it works: + * * [LazyHub.get] finds the unique registered class/factory/object for the given type (or fails) + * * If it's an object, that object is returned + * * If it's a factory, it is executed with args obtained recursively from the same LazyHub + * * If it's a class, it is instantiated using a public constructor in the same way as a factory + * * Of the public constructors that can be satisfied, the one that consumes the most args is chosen + * + * Advanced usage: + * * Use an array parameter to get one-or-more args of the component type, make it nullable for zero-or-more + * * If a LazyHub can't satisfy a type (or array param) and has a parent, it asks the parent + * * Typically the root LazyHub in the hierarchy will manage all singletons of the process + */ +fun lazyHub(): MutableLazyHub = LazyHubImpl(null) + +private class SimpleProvider(override val obj: T) : Provider { + override val type get() = obj.javaClass +} + +private class LazyProvider(private val busyProviders: BusyProviders, private val underlying: Any?, override val type: Class, val chooseInvocation: () -> Callable) : Provider { + override val obj by lazy { busyProviders.runFactory(this) } + override fun toString() = underlying.toString() +} + +private class Invocation(val constructor: PublicConstructor, val argSuppliers: List>) : Callable { + fun providerCount() = argSuppliers.stream().filter { (_, supplier) -> supplier.provider != null }.count() // Allow repeated providers. + override fun call() = constructor(argSuppliers) + override fun toString() = constructor.toString() +} + +private class BusyProviders { + private val busyProviders = mutableMapOf, Callable<*>>() + fun runFactory(provider: LazyProvider): T { + if (busyProviders.contains(provider)) throw CircularDependencyException("Provider '$provider' is already busy: ${busyProviders.values}") + val invocation = provider.chooseInvocation() + busyProviders.put(provider, invocation) + try { + return invocation.call() + } finally { + busyProviders.remove(provider) + } + } +} + +private val autotypes: Map, Class<*>> = mutableMapOf, Class<*>>().apply { + Arrays::class.java.declaredMethods.filter { it.name == "hashCode" }.map { it.parameterTypes[0].componentType }.filter { it.isPrimitive }.forEach { + val boxed = java.lang.reflect.Array.get(java.lang.reflect.Array.newInstance(it, 1), 0).javaClass + put(it, boxed) + put(boxed, it) + } +} + +private infix fun Class<*>.isSatisfiedBy(clazz: Class<*>): Boolean { + return isAssignableFrom(clazz) || autotypes[this] == clazz +} + +private class LazyHubImpl(private val parent: LazyHubImpl?, private val busyProviders: BusyProviders = parent?.busyProviders ?: BusyProviders()) : MutableLazyHub { + private val providers = mutableMapOf, MutableList>>() + private fun add(provider: Provider<*>, type: Class<*> = provider.type, registered: MutableSet> = mutableSetOf()) { + if (!registered.add(type)) return + providers[type]?.add(provider) ?: providers.put(type, mutableListOf(provider)) + Stream.concat(Arrays.stream(type.interfaces), Stream.of(type.superclass, autotypes[type]).filterNotNull()).forEach { + add(provider, it, registered) + } + } + + /** The non-empty list of providers, or null. */ + private fun findProviders(clazz: Class): List>? = uncheckedCast(providers[clazz]) ?: parent?.findProviders(clazz) + + private fun dropAll(serviceClass: Class<*>) { + val removed = mutableSetOf>() + providers.iterator().run { + while (hasNext()) { + val entry = next() + if (serviceClass isSatisfiedBy entry.key) { + removed.addAll(entry.value) + remove() + } + } + } + providers.values.iterator().run { + while (hasNext()) { + val providers = next() + providers.removeAll(removed) + if (providers.isEmpty()) remove() + } + } + } + + override fun getOrNull(clazz: Class) = findProviders(clazz)?.run { (singleOrNull() ?: throw TooManyProvidersException(clazz.toString())).obj } + override fun getAll(clazz: Class) = findProviders(clazz)?.map { it.obj } ?: emptyList() + override fun child(): MutableLazyHub = LazyHubImpl(this) + override fun obj(obj: Any) = add(SimpleProvider(obj)) + override fun obj(service: KClass, obj: T) { + dropAll(service.java) + obj(obj) + } + + override fun factory(service: KClass, factory: KFunction) = factory.validate().let { + dropAll(service.java) + addFactory(it) + } + + override fun impl(service: KClass, impl: KClass) = impl.validate().let { + dropAll(service.java) + addConcrete(it) + } + + override fun impl(service: KClass, impl: Class) = impl.validate().let { + dropAll(service.java) + addConcrete(it) + } + + override fun factory(factory: KFunction<*>) = addFactory(factory.validate()) + private fun addFactory(factory: KConstructor) { + val type = factory.kFunction.returnType.toJavaType().let { if (it == Void.TYPE) Unit::class.java else it as Class<*> } + add(LazyProvider(busyProviders, factory, uncheckedCast(type)) { factory.toInvocation() }) + } + + override fun factory(lh: LazyHub, type: KClass<*>) = addFactory(lh, type) + private fun addFactory(lh: LazyHub, type: KClass) { + add(LazyProvider(busyProviders, lh, type.java) { Callable { lh[type] } }) + } + + override fun impl(impl: KClass<*>) = implGeneric(impl) + private fun implGeneric(type: KClass) = addConcrete(type.validate()) + override fun impl(impl: Class<*>) = implGeneric(impl) + private fun implGeneric(type: Class) = addConcrete(type.validate()) + private fun

> addConcrete(concrete: Concrete) { + add(LazyProvider(busyProviders, concrete, concrete.clazz) { + var fail: UnsatisfiableParamException? = null + val satisfiable = concrete.publicConstructors.mapNotNull { constructor -> + try { + constructor.toInvocation() + } catch (e: UnsatisfiableParamException) { + fail?.addSuppressed(e) ?: run { fail = e } + null + } + } + if (satisfiable.isEmpty()) throw fail!! + val greediest = mutableListOf(satisfiable[0]) + var providerCount = greediest[0].providerCount() + satisfiable.stream().skip(1).forEach next@ { + val pc = it.providerCount() + if (pc < providerCount) return@next + if (pc > providerCount) { + greediest.clear() + providerCount = pc + } + greediest += it + } + greediest.singleOrNull() ?: throw NoUniqueGreediestSatisfiableConstructorException(greediest.toString()) + }) + } + + private fun arrayProvider(arrayType: Class<*>, componentType: Class): LazyProvider>? { + val providers = findProviders(componentType) ?: return null + return LazyProvider(busyProviders, null, uncheckedCast(arrayType)) { + Callable { providers.stream().map { it.obj }.toTypedArray(componentType) } + } + } + + private fun

PublicConstructor.toInvocation() = Invocation(this, params.mapNotNull { param -> + if (param.type.isArray) { + val provider = arrayProvider(param.type, param.type.componentType) + when (provider) { + null -> param.supplierWhenUnsatisfiable()?.let { param to it } + else -> param to ArgSupplier(provider) + } + } else { + val providers = findProviders(param.type) + when (providers?.size) { + null -> param.supplierWhenUnsatisfiable()?.let { param to it } + 1 -> param to ArgSupplier(providers[0]) + else -> throw TooManyProvidersException(param.toString()) + } + } + }) +} diff --git a/node/src/main/kotlin/net/corda/lazyhub/LazyHubModel.kt b/node/src/main/kotlin/net/corda/lazyhub/LazyHubModel.kt new file mode 100644 index 0000000000..7c3217f7d7 --- /dev/null +++ b/node/src/main/kotlin/net/corda/lazyhub/LazyHubModel.kt @@ -0,0 +1,130 @@ +package net.corda.lazyhub + +import net.corda.core.internal.toMap +import net.corda.core.internal.toTypedArray +import net.corda.core.internal.uncheckedCast +import java.lang.reflect.* +import kotlin.reflect.KClass +import kotlin.reflect.KFunction +import kotlin.reflect.KParameter +import kotlin.reflect.KVisibility +import kotlin.reflect.jvm.internal.ReflectProperties +import kotlin.reflect.jvm.isAccessible + +private val javaTypeDelegateField = Class.forName("kotlin.reflect.jvm.internal.KTypeImpl").getDeclaredField("javaType\$delegate").apply { isAccessible = true } +internal fun kotlin.reflect.KType.toJavaType() = (javaTypeDelegateField.get(this) as ReflectProperties.Val<*>)() +internal interface Provider { + /** Most specific known type i.e. directly registered implementation class, or declared return type of factory method. */ + val type: Class + /** May be lazily computed. */ + val obj: T +} + +/** Like [Provider] but capable of supplying null. */ +internal class ArgSupplier(val provider: Provider<*>?) { + companion object { + val nullSupplier = ArgSupplier(null) + } + + operator fun invoke() = provider?.obj +} + +/** Common interface to Kotlin/Java params. */ +internal interface Param { + val type: Class<*> + /** The supplier, or null to supply nothing so the Kotlin default is used. */ + fun supplierWhenUnsatisfiable(): ArgSupplier? = throw (if (type.isArray) ::UnsatisfiableArrayException else ::NoSuchProviderException)(toString()) +} + +internal class KParam(val kParam: KParameter) : Param { + override val type = run { + var jType = kParam.type.toJavaType() + loop@ while (true) { + jType = when (jType) { + is ParameterizedType -> jType.rawType + is TypeVariable<*> -> jType.bounds.first() // Potentially surprising but most consistent behaviour, see unit tests. + else -> break@loop + } + } + jType as Class<*> + } + + override fun supplierWhenUnsatisfiable() = when { + kParam.isOptional -> null // Use default value, even if param is also nullable. + kParam.type.isMarkedNullable -> ArgSupplier.nullSupplier + else -> super.supplierWhenUnsatisfiable() + } + + override fun toString() = kParam.toString() +} + +internal class JParam(private val param: Parameter, private val index: Int, override val type: Class<*>) : Param { + override fun toString() = "parameter #$index ${param.name} of ${param.declaringExecutable}" +} + +internal interface PublicConstructor { + val params: List

+ operator fun invoke(argSuppliers: List>): T +} + +internal class KConstructor(val kFunction: KFunction) : PublicConstructor { + companion object { + fun KFunction.validate() = run { + if (returnType.isMarkedNullable) throw NullableReturnTypeException(toString()) + isAccessible = true + KConstructor(this) + } + } + + override val params = kFunction.parameters.map(::KParam) + override fun invoke(argSuppliers: List>): T { + return kFunction.callBy(argSuppliers.stream().map { (param, supplier) -> param.kParam to supplier() }.toMap()) + } + + override fun toString() = kFunction.toString() +} + +internal class JConstructor(private val constructor: Constructor) : PublicConstructor { + // Much cheaper to get the types up-front than via the Parameter API: + override val params = constructor.parameters.zip(constructor.parameterTypes).mapIndexed { i, (p, t) -> JParam(p, i, t) } + + override fun invoke(argSuppliers: List>): T { + return constructor.newInstance(*argSuppliers.stream().map { (_, supplier) -> supplier() }.toTypedArray()) + } + + override fun toString() = constructor.toString() +} + +internal interface Concrete> { + val clazz: Class + val publicConstructors: List +} + +internal class KConcrete private constructor(private val kClass: KClass) : Concrete> { + companion object { + fun KClass.validate() = run { + if (isAbstract) throw AbstractTypeException(toString()) + KConcrete(this).apply { + if (publicConstructors.isEmpty()) throw NoPublicConstructorsException(toString()) + } + } + } + + override val clazz get() = kClass.java + override val publicConstructors = kClass.constructors.filter { it.visibility == KVisibility.PUBLIC }.map(::KConstructor) + override fun toString() = kClass.toString() +} + +internal class JConcrete private constructor(override val clazz: Class) : Concrete> { + companion object { + fun Class.validate() = run { + if (Modifier.isAbstract(modifiers)) throw AbstractTypeException(toString()) + JConcrete(this).apply { + if (publicConstructors.isEmpty()) throw NoPublicConstructorsException(toString()) + } + } + } + + override val publicConstructors = uncheckedCast>, Array>>(clazz.constructors).map(::JConstructor) + override fun toString() = clazz.toString() +} 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 551424f240..8eb40c46a4 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -30,12 +30,14 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.debug import net.corda.core.utilities.getOrThrow +import net.corda.lazyhub.LazyHub +import net.corda.lazyhub.MutableLazyHub +import net.corda.lazyhub.lazyHub import net.corda.node.VersionInfo import net.corda.node.internal.classloading.requireAnnotation import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.node.internal.cordapp.CordappProviderInternal -import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.services.ContractUpgradeHandler import net.corda.node.services.FinalityHandler import net.corda.node.services.NotaryChangeHandler @@ -58,7 +60,6 @@ import net.corda.node.services.transactions.* import net.corda.node.services.upgrade.ContractUpgradeServiceImpl import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.VaultSoftLockManager -import net.corda.node.shell.InteractiveShell import net.corda.node.utilities.AffinityExecutor import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.SignedNodeInfo @@ -146,9 +147,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, protected val runOnStop = ArrayList<() -> Any?>() private val _nodeReadyFuture = openFuture() protected var networkMapClient: NetworkMapClient? = null - - lateinit var securityManager: RPCSecurityManager get - /** Completes once the node has successfully registered with the network map service * or has loaded network map data from local database */ val nodeReadyFuture: CordaFuture get() = _nodeReadyFuture @@ -175,11 +173,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } private inline fun signNodeInfo(nodeInfo: NodeInfo, sign: (PublicKey, SerializedBytes) -> DigitalSignature): SignedNodeInfo { - // For now we assume the node has only one identity (excluding any composite ones) - val owningKey = nodeInfo.legalIdentities.single { it.owningKey !is CompositeKey }.owningKey + // For now we exclude any composite identities, see [SignedNodeInfo] + val owningKeys = nodeInfo.legalIdentities.map { it.owningKey }.filter { it !is CompositeKey } val serialised = nodeInfo.serialize() - val signature = sign(owningKey, serialised) - return SignedNodeInfo(serialised, listOf(signature)) + val signatures = owningKeys.map { sign(it, serialised) } + return SignedNodeInfo(serialised, signatures) } open fun generateAndSaveNodeInfo(): NodeInfo { @@ -202,6 +200,10 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } + protected open fun configure(lh: MutableLazyHub) { + // TODO: Migrate classes and factories from start method. + } + fun generateDatabaseSchema(outputFile: String) { HikariDataSource(HikariConfig(configuration.dataSourceProperties)).use { dataSource -> val jdbcUrl = configuration.dataSourceProperties.getProperty("url", "") @@ -222,16 +224,21 @@ abstract class AbstractNode(val configuration: NodeConfiguration, initCertificate() val schemaService = NodeSchemaService(cordappLoader.cordappSchemas) val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null) + val lh = lazyHub() + configure(lh) val identityService = makeIdentityService(identity.certificate) + lh.obj(identityService) networkMapClient = configuration.compatibilityZoneURL?.let { NetworkMapClient(it, identityService.trustRoot) } retrieveNetworkParameters(identityService.trustRoot) // Do all of this in a database transaction so anything that might need a connection has one. val (startedImpl, schedulerService) = initialiseDatabasePersistence(schemaService, identityService) { database -> + lh.obj(database) val networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database, networkParameters.notaries), identityService) val (keyPairs, info) = initNodeInfo(networkMapCache, identity, identityKeyPair) + lh.obj(info) identityService.loadIdentities(info.legalIdentitiesAndCerts) - val transactionStorage = makeTransactionStorage(database) - val nodeServices = makeServices(keyPairs, schemaService, transactionStorage, database, info, identityService, networkMapCache) + val transactionStorage = makeTransactionStorage(database, configuration.transactionCacheSizeBytes) + val nodeServices = makeServices(lh, keyPairs, schemaService, transactionStorage, database, info, identityService, networkMapCache) val mutualExclusionConfiguration = configuration.enterpriseConfiguration.mutualExclusionConfiguration if (mutualExclusionConfiguration.on) { RunOnceService(database, mutualExclusionConfiguration.machineName, @@ -260,13 +267,13 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } makeVaultObservers(schedulerService, database.hibernateConfig, smm, schemaService, flowLogicRefFactory) val rpcOps = makeRPCOps(flowStarter, database, smm) - startMessagingService(rpcOps) + lh.obj(rpcOps) + lh.getAll(Unit::class) // Run side-effects. installCoreFlows() val cordaServices = installCordaServices(flowStarter) tokenizableServices = nodeServices + cordaServices + schedulerService registerCordappFlows(smm) _services.rpcFlows += cordappLoader.cordapps.flatMap { it.rpcFlows } - startShell(rpcOps) Pair(StartedNodeImpl(this, _services, info, checkpointStorage, smm, attachments, network, database, rpcOps, flowStarter, notaryService), schedulerService) } val networkMapUpdater = NetworkMapUpdater(services.networkMapCache, @@ -302,10 +309,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, */ protected abstract fun getRxIoScheduler(): Scheduler - open fun startShell(rpcOps: CordaRPCOps) { - InteractiveShell.startShell(configuration, rpcOps, securityManager, _services.identityService, _services.database) - } - private fun initNodeInfo(networkMapCache: NetworkMapCacheBaseInternal, identity: PartyAndCertificate, identityKeyPair: KeyPair): Pair, NodeInfo> { @@ -557,7 +560,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, * Builds node internal, advertised, and plugin services. * Returns a list of tokenizable services to be added to the serialisation context. */ - private fun makeServices(keyPairs: Set, schemaService: SchemaService, transactionStorage: WritableTransactionStorage, database: CordaPersistence, info: NodeInfo, identityService: IdentityServiceInternal, networkMapCache: NetworkMapCacheInternal): MutableList { + private fun makeServices(lh: LazyHub, keyPairs: Set, schemaService: SchemaService, transactionStorage: WritableTransactionStorage, database: CordaPersistence, info: NodeInfo, identityService: IdentityServiceInternal, networkMapCache: NetworkMapCacheInternal): MutableList { checkpointStorage = DBCheckpointStorage() val metrics = MetricRegistry() attachments = NodeAttachmentService(metrics) @@ -573,7 +576,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, database, info, networkMapCache) - network = makeMessagingService(database, info) + network = lh[MessagingService::class] // TODO: Retire the lateinit var. val tokenizableServices = mutableListOf(attachments, network, services.vaultService, services.keyManagementService, services.identityService, platformClock, services.auditService, services.monitoringService, services.networkMapCache, services.schemaService, @@ -582,7 +585,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, return tokenizableServices } - protected open fun makeTransactionStorage(database: CordaPersistence): WritableTransactionStorage = DBTransactionStorage() + protected open fun makeTransactionStorage(database: CordaPersistence, transactionCacheSizeBytes: Long): WritableTransactionStorage = DBTransactionStorage(transactionCacheSizeBytes) private fun makeVaultObservers(schedulerService: SchedulerService, hibernateConfig: HibernateConfiguration, smm: StateMachineManager, schemaService: SchemaService, flowLogicRefFactory: FlowLogicRefFactory) { VaultSoftLockManager.install(services.vaultService, smm) ScheduledActivityObserver.install(services.vaultService, schedulerService, flowLogicRefFactory) @@ -743,9 +746,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, _started = null } - protected abstract fun makeMessagingService(database: CordaPersistence, info: NodeInfo): MessagingService - protected abstract fun startMessagingService(rpcOps: RPCOps) - private fun obtainIdentity(notaryConfig: NotaryConfig?): Pair { val keyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword) 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 7a71ea615f..fecb62b8c0 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -5,6 +5,7 @@ import net.corda.core.concurrent.CordaFuture import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.thenMatch import net.corda.core.internal.uncheckedCast +import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.RPCOps import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub @@ -14,8 +15,10 @@ import net.corda.core.serialization.internal.SerializationEnvironmentImpl import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger +import net.corda.lazyhub.MutableLazyHub import net.corda.node.VersionInfo import net.corda.node.internal.cordapp.CordappLoader +import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.serialization.KryoServerSerializationScheme import net.corda.node.services.api.SchemaService @@ -24,6 +27,7 @@ import net.corda.node.services.config.SecurityConfiguration import net.corda.node.services.config.VerifierType import net.corda.node.services.messaging.* import net.corda.node.services.transactions.InMemoryTransactionVerifierService +import net.corda.node.shell.InteractiveShell import net.corda.node.utilities.AddressUtils import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.DemoClock @@ -133,16 +137,27 @@ open class Node(configuration: NodeConfiguration, private var messageBroker: ArtemisMessagingServer? = null private var shutdownHook: ShutdownHook? = null - - override fun makeMessagingService(database: CordaPersistence, info: NodeInfo): MessagingService { + override fun configure(lh: MutableLazyHub) { + super.configure(lh) // Construct security manager reading users data either from the 'security' config section // if present or from rpcUsers list if the former is missing from config. - val securityManagerConfig = configuration.security?.authService ?: - SecurityConfiguration.AuthService.fromUsers(configuration.rpcUsers) + lh.obj(configuration.security?.authService ?: SecurityConfiguration.AuthService.fromUsers(configuration.rpcUsers)) + lh.impl(RPCSecurityManagerImpl::class) + configuration.messagingServerAddress?.also { + lh.obj(MessagingServerAddress(it)) + } ?: run { + lh.factory(this::makeLocalMessageBroker) + } + lh.factory(this::makeMessagingService) + // Side-effects: + lh.factory(this::startMessagingService) + lh.factory(this::startShell) + } - securityManager = RPCSecurityManagerImpl(securityManagerConfig) + class MessagingServerAddress(val address: NetworkHostAndPort) - val serverAddress = configuration.messagingServerAddress ?: makeLocalMessageBroker() + private fun makeMessagingService(database: CordaPersistence, info: NodeInfo, messagingServerAddress: MessagingServerAddress): MessagingService { + val serverAddress = messagingServerAddress.address val advertisedAddress = info.addresses.single() printBasicNodeInfo("Incoming connection address", advertisedAddress.toString()) @@ -162,10 +177,10 @@ open class Node(configuration: NodeConfiguration, networkParameters.maxMessageSize) } - private fun makeLocalMessageBroker(): NetworkHostAndPort { + private fun makeLocalMessageBroker(securityManager: RPCSecurityManager): MessagingServerAddress { with(configuration) { messageBroker = ArtemisMessagingServer(this, p2pAddress.port, rpcAddress?.port, services.networkMapCache, securityManager, networkParameters.maxMessageSize) - return NetworkHostAndPort("localhost", p2pAddress.port) + return MessagingServerAddress(NetworkHostAndPort("localhost", p2pAddress.port)) } } @@ -217,7 +232,7 @@ open class Node(configuration: NodeConfiguration, } } - override fun startMessagingService(rpcOps: RPCOps) { + private fun startMessagingService(rpcOps: RPCOps, securityManager: RPCSecurityManager) { // Start up the embedded MQ server messageBroker?.apply { runOnStop += this::stop @@ -238,6 +253,10 @@ open class Node(configuration: NodeConfiguration, } } + private fun startShell(rpcOps: CordaRPCOps, securityManager: RPCSecurityManager, identityService: IdentityService, database: CordaPersistence) { + InteractiveShell.startShell(configuration, rpcOps, securityManager, identityService, database) + } + /** * If the node is persisting to an embedded H2 database, then expose this via TCP with a DB URL of the form: * jdbc:h2:tcp://:/node diff --git a/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt b/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt index 2237cc1547..3b1f28dc38 100644 --- a/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt @@ -25,6 +25,7 @@ import org.apache.shiro.realm.AuthorizingRealm import org.apache.shiro.realm.jdbc.JdbcRealm import org.apache.shiro.subject.PrincipalCollection import org.apache.shiro.subject.SimplePrincipalCollection +import java.io.Closeable import javax.security.auth.login.FailedLoginException import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.TimeUnit @@ -43,10 +44,6 @@ class RPCSecurityManagerImpl(config: AuthServiceConfig) : RPCSecurityManager { manager = buildImpl(config) } - override fun close() { - manager.destroy() - } - @Throws(FailedLoginException::class) override fun authenticate(principal: String, password: Password): AuthorizingSubject { password.use { @@ -67,6 +64,10 @@ class RPCSecurityManagerImpl(config: AuthServiceConfig) : RPCSecurityManager { subjectId = SimplePrincipalCollection(principal, id.value), manager = manager) + override fun close() { + manager.realms?.filterIsInstance()?.forEach { it.close() } + manager.destroy() + } companion object { @@ -240,7 +241,7 @@ private class InMemoryRealm(users: List, authorizationInfoByUser[principals.primaryPrincipal as String] } -private class NodeJdbcRealm(config: SecurityConfiguration.AuthService.DataSource) : JdbcRealm() { +private class NodeJdbcRealm(config: SecurityConfiguration.AuthService.DataSource) : JdbcRealm(), Closeable { init { credentialsMatcher = buildCredentialMatcher(config.passwordEncryption) @@ -248,6 +249,10 @@ private class NodeJdbcRealm(config: SecurityConfiguration.AuthService.DataSource dataSource = HikariDataSource(HikariConfig(config.connection!!)) permissionResolver = RPCPermissionResolver } + + override fun close() { + (dataSource as? Closeable)?.close() + } } private typealias ShiroCache = org.apache.shiro.cache.Cache 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 ed6053ce05..bd50ac1562 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 @@ -15,6 +15,8 @@ import java.net.URL import java.nio.file.Path import java.util.* +val Int.MB: Long get() = this * 1024L * 1024L + interface NodeConfiguration : NodeSSLConfiguration { // myLegalName should be only used in the initial network registration, we should use the name from the certificate instead of this. // TODO: Remove this so we don't accidentally use this identity in the code? @@ -46,6 +48,17 @@ interface NodeConfiguration : NodeSSLConfiguration { val database: DatabaseConfig val relay: RelayConfiguration? val useAMQPBridges: Boolean get() = true + val transactionCacheSizeBytes: Long get() = defaultTransactionCacheSize + + companion object { + // default to at least 8MB and a bit extra for larger heap sizes + val defaultTransactionCacheSize: Long = 8.MB + getAdditionalCacheMemory() + + // add 5% of any heapsize over 300MB to the default transaction cache size + private fun getAdditionalCacheMemory(): Long { + return Math.max((Runtime.getRuntime().maxMemory() - 300.MB) / 20, 0) + } + } } data class DevModeOptions(val disableCheckpointChecker: Boolean = false) @@ -124,7 +137,8 @@ data class NodeConfigurationImpl( override val additionalNodeInfoPollingFrequencyMsec: Long = 5.seconds.toMillis(), override val sshd: SSHDConfiguration? = null, override val database: DatabaseConfig = DatabaseConfig(exportHibernateJMXStatistics = devMode), - override val useAMQPBridges: Boolean = true + override val useAMQPBridges: Boolean = true, + override val transactionCacheSizeBytes: Long = NodeConfiguration.defaultTransactionCacheSize ) : NodeConfiguration { override val exportJMXto: String get() = "http" diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index cb855ded3e..d715cc70d5 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -2,27 +2,33 @@ package net.corda.node.services.persistence import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.TransactionSignature import net.corda.core.internal.ThreadBox import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.internal.concurrent.doneFuture import net.corda.core.messaging.DataFeed -import net.corda.core.serialization.SerializationDefaults -import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.serialization.deserialize -import net.corda.core.serialization.serialize +import net.corda.core.serialization.* import net.corda.core.toFuture +import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.SignedTransaction import net.corda.node.services.api.WritableTransactionStorage -import net.corda.node.utilities.AppendOnlyPersistentMap +import net.corda.node.utilities.AppendOnlyPersistentMapBase +import net.corda.node.utilities.WeightBasedAppendOnlyPersistentMap import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import net.corda.nodeapi.internal.persistence.bufferUntilDatabaseCommit import net.corda.nodeapi.internal.persistence.wrapWithDatabaseTransaction import rx.Observable import rx.subjects.PublishSubject +import java.util.* import javax.persistence.* -class DBTransactionStorage : WritableTransactionStorage, SingletonSerializeAsToken() { +// cache value type to just store the immutable bits of a signed transaction plus conversion helpers +typealias TxCacheValue = Pair, List> +fun TxCacheValue.toSignedTx() = SignedTransaction(this.first, this.second) +fun SignedTransaction.toTxCacheValue() = TxCacheValue(this.txBits, this.sigs) + +class DBTransactionStorage(cacheSizeBytes: Long) : WritableTransactionStorage, SingletonSerializeAsToken() { @Entity @Table(name = "${NODE_DATABASE_PREFIX}transactions") @@ -37,41 +43,59 @@ class DBTransactionStorage : WritableTransactionStorage, SingletonSerializeAsTok ) private companion object { - fun createTransactionsMap(): AppendOnlyPersistentMap { - return AppendOnlyPersistentMap( + fun createTransactionsMap(maxSizeInBytes: Long) + : AppendOnlyPersistentMapBase { + return WeightBasedAppendOnlyPersistentMap( toPersistentEntityKey = { it.toString() }, fromPersistentEntity = { Pair(SecureHash.parse(it.txId), - it.transaction.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)) + it.transaction.deserialize(context = SerializationDefaults.STORAGE_CONTEXT) + .toTxCacheValue()) }, - toPersistentEntity = { key: SecureHash, value: SignedTransaction -> + toPersistentEntity = { key: SecureHash, value: TxCacheValue -> DBTransaction().apply { txId = key.toString() - transaction = value.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes + transaction = value.toSignedTx(). + serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes } }, - persistentEntityClass = DBTransaction::class.java + persistentEntityClass = DBTransaction::class.java, + maxWeight = maxSizeInBytes, + weighingFunc = { hash, tx -> hash.size + weighTx(tx) } ) } + + // Rough estimate for the average of a public key and the transaction metadata - hard to get exact figures here, + // as public keys can vary in size a lot, and if someone else is holding a reference to the key, it won't add + // to the memory pressure at all here. + private const val transactionSignatureOverheadEstimate = 1024 + + private fun weighTx(tx: Optional): Int { + if (!tx.isPresent) { + return 0 + } + val actTx = tx.get() + return actTx.second.sumBy { it.size + transactionSignatureOverheadEstimate } + actTx.first.size + } } - private val txStorage = ThreadBox(createTransactionsMap()) + private val txStorage = ThreadBox(createTransactionsMap(cacheSizeBytes)) override fun addTransaction(transaction: SignedTransaction): Boolean = txStorage.locked { - addWithDuplicatesAllowed(transaction.id, transaction).apply { + addWithDuplicatesAllowed(transaction.id, transaction.toTxCacheValue()).apply { updatesPublisher.bufferUntilDatabaseCommit().onNext(transaction) } } - override fun getTransaction(id: SecureHash): SignedTransaction? = txStorage.content[id] + override fun getTransaction(id: SecureHash): SignedTransaction? = txStorage.content[id]?.toSignedTx() private val updatesPublisher = PublishSubject.create().toSerialized() override val updates: Observable = updatesPublisher.wrapWithDatabaseTransaction() override fun track(): DataFeed, SignedTransaction> { return txStorage.locked { - DataFeed(allPersisted().map { it.second }.toList(), updatesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction()) + DataFeed(allPersisted().map { it.second.toSignedTx() }.toList(), updatesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction()) } } @@ -81,11 +105,11 @@ class DBTransactionStorage : WritableTransactionStorage, SingletonSerializeAsTok if (existingTransaction == null) { updatesPublisher.filter { it.id == id }.toFuture() } else { - doneFuture(existingTransaction) + doneFuture(existingTransaction.toSignedTx()) } } } @VisibleForTesting - val transactions: Iterable get() = txStorage.content.allPersisted().map { it.second }.toList() + val transactions: Iterable get() = txStorage.content.allPersisted().map { it.second.toSignedTx() }.toList() } diff --git a/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt b/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt index 17fab1f4ac..eb58d0871e 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt @@ -10,11 +10,18 @@ import net.corda.core.schemas.PersistentStateRef import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.node.services.api.SchemaService -import net.corda.nodeapi.internal.persistence.HibernateConfiguration import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager +import net.corda.nodeapi.internal.persistence.HibernateConfiguration import org.hibernate.FlushMode import rx.Observable + +/** + * Small data class bundling together a ContractState and a StateRef (as opposed to a TransactionState and StateRef + * in StateAndRef) + */ +data class ContractStateAndRef(val state: ContractState, val ref: StateRef) + /** * A vault observer that extracts Object Relational Mappings for contract states that support it, and persists them with Hibernate. */ @@ -30,27 +37,33 @@ class HibernateObserver private constructor(private val config: HibernateConfigu } private fun persist(produced: Set>) { - produced.forEach { persistState(it) } - } - - private fun persistState(stateAndRef: StateAndRef) { - val state = stateAndRef.state.data - log.debug { "Asked to persist state ${stateAndRef.ref}" } - schemaService.selectSchemas(state).forEach { persistStateWithSchema(state, stateAndRef.ref, it) } + val stateBySchema: MutableMap> = mutableMapOf() + // map all states by their referenced schemas + produced.forEach { + val contractStateAndRef = ContractStateAndRef(it.state.data, it.ref) + log.debug { "Asked to persist state ${it.ref}" } + schemaService.selectSchemas(contractStateAndRef.state).forEach { + stateBySchema.getOrPut(it) { mutableListOf() }.add(contractStateAndRef) + } + } + // then persist all states for each schema + stateBySchema.forEach { persistStatesWithSchema(it.value, it.key) } } @VisibleForTesting - internal fun persistStateWithSchema(state: ContractState, stateRef: StateRef, schema: MappedSchema) { + internal fun persistStatesWithSchema(statesAndRefs: List, schema: MappedSchema) { val sessionFactory = config.sessionFactoryForSchemas(setOf(schema)) val session = sessionFactory.withOptions(). connection(DatabaseTransactionManager.current().connection). flushMode(FlushMode.MANUAL). openSession() - session.use { - val mappedObject = schemaService.generateMappedObject(state, schema) - mappedObject.stateRef = PersistentStateRef(stateRef) - it.persist(mappedObject) - it.flush() + session.use { thisSession -> + statesAndRefs.forEach { + val mappedObject = schemaService.generateMappedObject(it.state, schema) + mappedObject.stateRef = PersistentStateRef(it.ref) + thisSession.persist(mappedObject) + } + thisSession.flush() } } } diff --git a/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt b/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt index a21da310ee..4912871b46 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt @@ -1,5 +1,7 @@ package net.corda.node.utilities +import com.google.common.cache.LoadingCache +import com.google.common.cache.Weigher import net.corda.core.utilities.contextLogger import net.corda.nodeapi.internal.persistence.currentDBSession import java.util.* @@ -10,23 +12,18 @@ import java.util.* * behaviour is unpredictable! There is a best-effort check for double inserts, but this should *not* be relied on, so * ONLY USE THIS IF YOUR TABLE IS APPEND-ONLY */ -class AppendOnlyPersistentMap( +abstract class AppendOnlyPersistentMapBase( val toPersistentEntityKey: (K) -> EK, val fromPersistentEntity: (E) -> Pair, val toPersistentEntity: (key: K, value: V) -> E, - val persistentEntityClass: Class, - cacheBound: Long = 1024 -) { //TODO determine cacheBound based on entity class later or with node config allowing tuning, or using some heuristic based on heap size + val persistentEntityClass: Class +) { private companion object { private val log = contextLogger() } - private val cache = NonInvalidatingCache>( - bound = cacheBound, - concurrencyLevel = 8, - loadFunction = { key -> Optional.ofNullable(loadValue(key)) } - ) + abstract protected val cache: LoadingCache> /** * Returns the value associated with the key, first loading that value from the storage if necessary. @@ -116,7 +113,7 @@ class AppendOnlyPersistentMap( } } - private fun loadValue(key: K): V? { + protected fun loadValue(key: K): V? { val result = currentDBSession().find(persistentEntityClass, toPersistentEntityKey(key)) return result?.let(fromPersistentEntity)?.second } @@ -135,3 +132,45 @@ class AppendOnlyPersistentMap( cache.invalidateAll() } } + +class AppendOnlyPersistentMap( + toPersistentEntityKey: (K) -> EK, + fromPersistentEntity: (E) -> Pair, + toPersistentEntity: (key: K, value: V) -> E, + persistentEntityClass: Class, + cacheBound: Long = 1024 +) : AppendOnlyPersistentMapBase( + toPersistentEntityKey, + fromPersistentEntity, + toPersistentEntity, + persistentEntityClass) { + //TODO determine cacheBound based on entity class later or with node config allowing tuning, or using some heuristic based on heap size + override val cache = NonInvalidatingCache>( + bound = cacheBound, + concurrencyLevel = 8, + loadFunction = { key -> Optional.ofNullable(loadValue(key)) }) +} + +class WeightBasedAppendOnlyPersistentMap( + toPersistentEntityKey: (K) -> EK, + fromPersistentEntity: (E) -> Pair, + toPersistentEntity: (key: K, value: V) -> E, + persistentEntityClass: Class, + maxWeight: Long, + weighingFunc: (K, Optional) -> Int +) : AppendOnlyPersistentMapBase( + toPersistentEntityKey, + fromPersistentEntity, + toPersistentEntity, + persistentEntityClass) { + override val cache = NonInvalidatingWeightBasedCache>( + maxWeight = maxWeight, + concurrencyLevel = 8, + weigher = object : Weigher> { + override fun weigh(key: K, value: Optional): Int { + return weighingFunc(key, value) + } + }, + loadFunction = { key -> Optional.ofNullable(loadValue(key)) } + ) +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt b/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt index 07dd16b936..0a199a0e31 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt @@ -3,6 +3,7 @@ package net.corda.node.utilities import com.google.common.cache.CacheBuilder import com.google.common.cache.CacheLoader import com.google.common.cache.LoadingCache +import com.google.common.cache.Weigher import com.google.common.util.concurrent.ListenableFuture @@ -21,11 +22,26 @@ class NonInvalidatingCache private constructor( } // TODO look into overriding loadAll() if we ever use it - private class NonInvalidatingCacheLoader(val loadFunction: (K) -> V) : CacheLoader() { + class NonInvalidatingCacheLoader(val loadFunction: (K) -> V) : CacheLoader() { override fun reload(key: K, oldValue: V): ListenableFuture { throw IllegalStateException("Non invalidating cache refreshed") } override fun load(key: K) = loadFunction(key) } +} + +class NonInvalidatingWeightBasedCache private constructor( + val cache: LoadingCache +) : LoadingCache by cache { + constructor (maxWeight: Long, concurrencyLevel: Int, weigher: Weigher, loadFunction: (K) -> V) : + this(buildCache(maxWeight, concurrencyLevel, weigher, loadFunction)) + + + private companion object { + private fun buildCache(maxWeight: Long, concurrencyLevel: Int, weigher: Weigher, loadFunction: (K) -> V): LoadingCache { + val builder = CacheBuilder.newBuilder().maximumWeight(maxWeight).weigher(weigher).concurrencyLevel(concurrencyLevel) + return builder.build(NonInvalidatingCache.NonInvalidatingCacheLoader(loadFunction)) + } + } } \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/lazyhub/LazyHubTests.kt b/node/src/test/kotlin/net/corda/lazyhub/LazyHubTests.kt new file mode 100644 index 0000000000..5d462585bc --- /dev/null +++ b/node/src/test/kotlin/net/corda/lazyhub/LazyHubTests.kt @@ -0,0 +1,640 @@ +package net.corda.lazyhub + +import net.corda.core.internal.uncheckedCast +import org.assertj.core.api.Assertions.catchThrowable +import org.hamcrest.CoreMatchers.* +import org.junit.Assert.* +import org.junit.Ignore +import org.junit.Test +import java.io.Closeable +import java.io.IOException +import java.io.Serializable +import kotlin.reflect.KFunction +import kotlin.reflect.jvm.javaConstructor +import kotlin.reflect.jvm.javaMethod +import kotlin.test.assertEquals +import kotlin.test.fail + +open class LazyHubTests { + private val lh = lazyHub() + + class Config(val info: String) + interface A + interface B { + val a: A + } + + class AImpl(val config: Config) : A + class BImpl(override val a: A) : B + class Spectator { + init { + fail("Should not be instantiated.") + } + } + + @Test + fun `basic functionality`() { + val config = Config("woo") + lh.obj(config) + lh.impl(AImpl::class) + lh.impl(BImpl::class) + lh.impl(Spectator::class) + val b = lh[B::class] + // An impl is instantiated at most once per LazyHub: + assertSame(b.a, lh[A::class]) + assertSame(b, lh[B::class]) + // More specific type to expose config without casting: + val a = lh[AImpl::class] + assertSame(b.a, a) + assertSame(config, a.config) + } + + private fun createA(config: Config): A = AImpl(config) // Declared return type is significant. + internal open fun createB(): B = fail("Should not be called.") + @Test + fun `factory works`() { + lh.obj(Config("x")) + lh.factory(this::createA) // Observe private is OK. + assertSame(AImpl::class.java, lh[A::class].javaClass) + // The factory declares A not AImpl as its return type, and lh doesn't try to be clever: + catchThrowable { lh[AImpl::class] }.run { + assertSame(NoSuchProviderException::class.java, javaClass) + assertEquals(AImpl::class.toString(), message) + } + } + + @Ignore + class Subclass : LazyHubTests() { // Should not run as tests. + @Suppress("unused") + private fun createA(@Suppress("UNUSED_PARAMETER") config: Config): A = fail("Should not be called.") + + override fun createB() = BImpl(AImpl(Config("Subclass"))) // More specific return type is OK. + } + + @Suppress("MemberVisibilityCanPrivate") + internal fun addCreateATo(lh: MutableLazyHub) { + lh.factory(this::createA) + } + + @Suppress("MemberVisibilityCanPrivate") + internal fun addCreateBTo(lh: MutableLazyHub) { + lh.factory(this::createB) + } + + @Test + fun `private factory is not virtual`() { + val baseMethod = this::createA.javaMethod!! + // Check the Subclass version would override if baseMethod wasn't private: + Subclass::class.java.getDeclaredMethod(baseMethod.name, *baseMethod.parameterTypes) + lh.obj(Config("x")) + Subclass().addCreateATo(lh) + lh[A::class] // Should not blow up. + } + + @Test + fun `non-private factory is virtual`() { + Subclass().addCreateBTo(lh) + assertEquals("Subclass", (lh[B::class].a as AImpl).config.info) // Check overridden function was called. + // The signature that was added declares B not BImpl as its return type: + catchThrowable { lh[BImpl::class] }.run { + assertSame(NoSuchProviderException::class.java, javaClass) + assertEquals(BImpl::class.toString(), message) + } + } + + private fun returnsYay() = "yay" + class TakesString(@Suppress("UNUSED_PARAMETER") text: String) + + @Test + fun `too many providers`() { + lh.obj("woo") + lh.factory(this::returnsYay) + lh.impl(TakesString::class) + catchThrowable { lh[TakesString::class] }.run { + assertSame(TooManyProvidersException::class.java, javaClass) + assertEquals(TakesString::class.constructors.single().parameters[0].toString(), message) + assertThat(message, containsString(" #0 ")) + assertThat(message, endsWith(TakesString::class.qualifiedName)) + } + } + + class TakesStringOrInt(val text: String) { + @Suppress("unused") + constructor(number: Int) : this(number.toString()) + } + + @Test + fun `too many providers with alternate constructor`() { + lh.obj("woo") + lh.factory(this::returnsYay) + lh.impl(TakesStringOrInt::class) + val constructors = TakesStringOrInt::class.constructors.toList() + catchThrowable { lh[TakesStringOrInt::class] }.run { + assertSame(NoSuchProviderException::class.java, javaClass) + assertEquals(constructors[0].parameters[0].toString(), message) + assertThat(message, containsString(" #0 ")) + assertThat(message, endsWith(TakesStringOrInt::class.qualifiedName)) + suppressed.single().run { + assertSame(TooManyProvidersException::class.java, javaClass) + assertEquals(constructors[1].parameters[0].toString(), message) + assertThat(message, containsString(" #0 ")) + assertThat(message, endsWith(TakesStringOrInt::class.qualifiedName)) + } + } + lh.obj(123) + assertEquals("123", lh[TakesStringOrInt::class].text) + } + + @Test + fun genericClass() { + class G(val arg: T) + lh.obj("arg") + lh.impl(G::class) + assertEquals("arg", lh[G::class].arg) // Can't inspect type arg T as no such thing exists. + } + + private fun ntv(a: Y) = a.toString() + @Test + fun `nested type variable`() { + // First check it's actually legal to pass any old Closeable into the function: + val arg = Closeable {} + assertEquals(arg.toString(), ntv(arg)) + // Good, now check LazyHub can do it: + val ntv: Function1 = this::ntv + lh.factory(uncheckedCast>(ntv)) + lh.obj(arg) + assertEquals(arg.toString(), lh[String::class]) + } + + class PTWMB(val arg: Y) where Y : Closeable, Y : Serializable + private class CloseableAndSerializable : Closeable, Serializable { + override fun close() {} + } + + @Test + fun `parameter type with multiple bounds in java`() { + // At compile time we must pass something Closeable and Serializable into the constructor: + CloseableAndSerializable().let { assertSame(it, PTWMB(it).arg) } + // But at runtime only Closeable is needed (and Serializable is not enough) due to the leftmost bound erasure rule: + lh.impl(PTWMB::class.java) + lh.obj(object : Serializable {}) + catchThrowable { lh[PTWMB::class] }.run { + assertSame(NoSuchProviderException::class.java, javaClass) + assertThat(message, containsString(" #0 ")) + assertThat(message, endsWith(PTWMB::class.constructors.single().javaConstructor.toString())) + } + val arg = Closeable {} + lh.obj(arg) + assertSame(arg, lh[PTWMB::class].arg) + } + + @Test + fun `parameter type with multiple bounds in kotlin`() { + lh.impl(PTWMB::class) + lh.obj(object : Serializable {}) + catchThrowable { lh[PTWMB::class] }.run { + assertSame(NoSuchProviderException::class.java, javaClass) + assertEquals(PTWMB::class.constructors.single().parameters[0].toString(), message) + assertThat(message, containsString(" #0 ")) + assertThat(message, containsString(PTWMB::class.qualifiedName)) + } + val arg = Closeable {} + lh.obj(arg) + assertSame(arg, lh[PTWMB::class].arg) + } + + private fun ptwmb(arg: Y) where Y : Closeable, Y : Serializable = arg.toString() + @Test + fun `factory parameter type with multiple bounds`() { + val ptwmb: Function1 = this::ptwmb + val kFunction = uncheckedCast>(ptwmb) + lh.factory(kFunction) + lh.obj(object : Serializable {}) + catchThrowable { lh[String::class] }.run { + assertSame(NoSuchProviderException::class.java, javaClass) + assertEquals(kFunction.parameters[0].toString(), message) + assertThat(message, containsString(" #0 ")) + assertThat(message, endsWith(ptwmb.toString())) + } + val arg = Closeable {} + lh.obj(arg) + assertEquals(arg.toString(), lh[String::class]) + } + + private fun upt(a: Y) = a.toString() + @Test + fun `unbounded parameter type`() { + val upt: Function1 = this::upt + val kFunction: KFunction = uncheckedCast(upt) + lh.factory(kFunction) + // The only provider for Any is the factory, which is busy: + catchThrowable { lh[String::class] }.run { + assertSame(CircularDependencyException::class.java, javaClass) + assertThat(message, containsString("'$upt'")) + assertThat(message, endsWith(listOf(upt).toString())) + } + lh.obj(Any()) + // This time the factory isn't attempted: + catchThrowable { lh[String::class] }.run { + assertSame(TooManyProvidersException::class.java, javaClass) + assertEquals(kFunction.parameters[0].toString(), message) + assertThat(message, containsString(" #0 ")) + assertThat(message, endsWith(upt.toString())) + } + } + + open class NoPublicConstructor protected constructor() + + @Test + fun `no public constructor`() { + catchThrowable { lh.impl(NoPublicConstructor::class) }.run { + assertSame(NoPublicConstructorsException::class.java, javaClass) + assertEquals(NoPublicConstructor::class.toString(), message) + } + catchThrowable { lh.impl(NoPublicConstructor::class.java) }.run { + assertSame(NoPublicConstructorsException::class.java, javaClass) + assertEquals(NoPublicConstructor::class.toString(), message) + } + } + + private fun primitiveInt() = 1 + class IntConsumer(@Suppress("UNUSED_PARAMETER") i: Int) + class IntegerConsumer(@Suppress("UNUSED_PARAMETER") i: Int?) + + @Test + fun `boxed satisfies primitive`() { + lh.obj(1) + lh.impl(IntConsumer::class) + lh[IntConsumer::class] + } + + @Test + fun `primitive satisfies boxed`() { + lh.factory(this::primitiveInt) + lh.impl(IntegerConsumer::class.java) + lh[IntegerConsumer::class] + } + + // The primary constructor takes two distinct providers: + class TakesTwoThings(@Suppress("UNUSED_PARAMETER") first: String, @Suppress("UNUSED_PARAMETER") second: Int) { + // This constructor takes one repeated provider but we count it both times so greediness is 2: + @Suppress("unused") + constructor(first: Int, second: Int) : this(first.toString(), second) + + // This constructor would be greediest but is not satisfiable: + @Suppress("unused") + constructor(first: Int, second: String, @Suppress("UNUSED_PARAMETER") third: Config) : this(second, first) + } + + @Test + fun `equally greedy constructors kotlin`() { + lh.obj("str") + lh.obj(123) + lh.impl(TakesTwoThings::class) + catchThrowable { lh[TakesTwoThings::class] }.run { + assertSame(NoUniqueGreediestSatisfiableConstructorException::class.java, javaClass) + val expected = TakesTwoThings::class.constructors.filter { it.parameters.size == 2 } + assertEquals(2, expected.size) + assertThat(message, endsWith(expected.toString())) + } + } + + @Test + fun `equally greedy constructors java`() { + lh.obj("str") + lh.obj(123) + lh.impl(TakesTwoThings::class.java) + catchThrowable { lh[TakesTwoThings::class] }.run { + assertSame(NoUniqueGreediestSatisfiableConstructorException::class.java, javaClass) + val expected = TakesTwoThings::class.java.constructors.filter { it.parameters.size == 2 } + assertEquals(2, expected.size) + assertEquals(expected.toString(), message) + } + } + + private fun nrt(): String? = fail("Should not be invoked.") + @Test + fun `nullable return type is banned`() { + catchThrowable { lh.factory(this::nrt) }.run { + assertSame(NullableReturnTypeException::class.java, javaClass) + assertThat(message, endsWith(this@LazyHubTests::nrt.toString())) + } + } + + @Test + fun unsatisfiableArrayParam() { + class Impl(@Suppress("UNUSED_PARAMETER") v: Array) + lh.impl(Impl::class) + catchThrowable { lh[Impl::class] }.run { + assertSame(UnsatisfiableArrayException::class.java, javaClass) + assertEquals(Impl::class.constructors.single().parameters[0].toString(), message) + } + // Arrays are only special in real params, you should use getAll to get all the Strings: + catchThrowable { lh[Array::class] }.run { + assertSame(NoSuchProviderException::class.java, javaClass) + assertEquals(Array::class.java.toString(), message) + } + assertEquals(emptyList(), lh.getAll(String::class)) + } + + @Test + fun arrayParam1() { + class Impl(val v: Array) + lh.impl(Impl::class) + lh.obj("a") + assertArrayEquals(arrayOf("a"), lh[Impl::class].v) + } + + @Test + fun arrayParam2() { + class Impl(val v: Array) + lh.impl(Impl::class) + lh.obj("y") + lh.obj("x") + assertArrayEquals(arrayOf("y", "x"), lh[Impl::class].v) + } + + @Test + fun nullableArrayParam() { + class Impl(val v: Array?) + lh.impl(Impl::class) + assertEquals(null, lh[Impl::class].v) + } + + @Test + fun arraysAreNotCached() { + class B(val v: Array) + class A(val v: Array, val b: B) + class C(val v: Array) + class D(val v: Array) + lh.obj("x") + lh.obj("y") + lh.impl(A::class) + lh.impl(B::class) + val a = lh[A::class] + a.run { + assertArrayEquals(arrayOf("x", "y"), v) + assertArrayEquals(arrayOf("x", "y"), b.v) + assertNotSame(v, b.v) + } + assertSame(lh[B::class].v, a.b.v) // Because it's the same (cached) instance of B. + lh.impl(C::class) + lh[C::class].run { + assertArrayEquals(arrayOf("x", "y"), v) + assertNotSame(v, a.v) + assertNotSame(v, a.b.v) + } + lh.obj("z") + lh.impl(D::class) + lh[D::class].run { + assertArrayEquals(arrayOf("x", "y", "z"), v) + } + } + + class C1(@Suppress("UNUSED_PARAMETER") c2: C2) + class C2(@Suppress("UNUSED_PARAMETER") c3: String) + + private fun c3(@Suppress("UNUSED_PARAMETER") c2: C2): String { + fail("Should not be called.") + } + + @Test + fun `circularity error kotlin`() { + lh.impl(C1::class) + lh.impl(C2::class) + lh.factory(this::c3) + catchThrowable { lh[C1::class] }.run { + assertSame(CircularDependencyException::class.java, javaClass) + assertThat(message, containsString("'${C2::class}'")) + assertThat(message, endsWith(listOf(C1::class.constructors.single(), C2::class.constructors.single(), this@LazyHubTests::c3).toString())) + } + } + + @Test + fun `circularity error java`() { + lh.impl(C1::class.java) + lh.impl(C2::class.java) + lh.factory(this::c3) + catchThrowable { lh[C1::class] }.run { + assertSame(CircularDependencyException::class.java, javaClass) + assertThat(message, containsString("'${C2::class}'")) + assertThat(message, endsWith(listOf(C1::class.constructors.single().javaConstructor, C2::class.constructors.single().javaConstructor, this@LazyHubTests::c3).toString())) + } + } + + @Test + fun `ancestor hub providers are visible`() { + val c = Config("over here") + lh.obj(c) + lh.child().also { + it.impl(AImpl::class) + assertSame(c, it[AImpl::class].config) + } + lh.child().child().also { + it.impl(AImpl::class) + assertSame(c, it[AImpl::class].config) + } + } + + @Test + fun `descendant hub providers are not visible`() { + val child = lh.child() + child.obj(Config("over here")) + lh.impl(AImpl::class) + catchThrowable { lh[AImpl::class] }.run { + assertSame(NoSuchProviderException::class.java, javaClass) + assertEquals(AImpl::class.constructors.single().parameters.single().toString(), message) + } + // Fails even though we go via the child, as the cached AImpl in lh shouldn't have collaborators from descendant hubs: + catchThrowable { child[AImpl::class] }.run { + assertSame(NoSuchProviderException::class.java, javaClass) + assertEquals(AImpl::class.constructors.single().parameters.single().toString(), message) + } + } + + class AllConfigs(val configs: Array) + + @Test + fun `nearest ancestor with at least one provider wins`() { + lh.obj(Config("deep")) + lh.child().also { + it.child().also { + it.impl(AllConfigs::class) + assertEquals(listOf("deep"), it[AllConfigs::class].configs.map { it.info }) + } + it.obj(Config("shallow1")) + it.obj(Config("shallow2")) + it.child().also { + it.impl(AllConfigs::class) + assertEquals(listOf("shallow1", "shallow2"), it[AllConfigs::class].configs.map { it.info }) + } + it.child().also { + it.obj(Config("local")) + it.impl(AllConfigs::class) + assertEquals(listOf("local"), it[AllConfigs::class].configs.map { it.info }) + } + } + } + + @Test + fun `abstract type`() { + catchThrowable { lh.impl(Runnable::class) }.run { + assertSame(AbstractTypeException::class.java, javaClass) + assertEquals(Runnable::class.toString(), message) + } + catchThrowable { lh.impl(Runnable::class.java) }.run { + assertSame(AbstractTypeException::class.java, javaClass) + assertEquals(Runnable::class.java.toString(), message) + } + } + + private interface Service + open class GoodService : Service + abstract class BadService1 : Service + class BadService2 private constructor() : Service + + private fun badService3(): Service? = fail("Should not be called.") + @Test + fun `existing providers not removed if new type is bad`() { + lh.impl(GoodService::class) + catchThrowable { lh.impl(Service::class, BadService1::class) }.run { + assertSame(AbstractTypeException::class.java, javaClass) + assertEquals(BadService1::class.toString(), message) + } + catchThrowable { lh.impl(Service::class, BadService2::class) }.run { + assertSame(NoPublicConstructorsException::class.java, javaClass) + assertEquals(BadService2::class.toString(), message) + } + catchThrowable { lh.impl(Service::class, BadService2::class.java) }.run { + assertSame(NoPublicConstructorsException::class.java, javaClass) + assertEquals(BadService2::class.toString(), message) + } + // Type system won't let you pass in badService3, but I still want validation up-front: + catchThrowable { lh.factory(Service::class, uncheckedCast(this::badService3)) }.run { + assertSame(NullableReturnTypeException::class.java, javaClass) + assertEquals(this@LazyHubTests::badService3.toString(), message) + } + assertSame(GoodService::class.java, lh[Service::class].javaClass) + } + + class GoodService2 : GoodService() + + @Test + fun `service providers are removed completely`() { + lh.impl(GoodService::class) + assertSame(GoodService::class.java, lh[Service::class].javaClass) + lh.impl(GoodService::class, GoodService2::class) + // In particular, GoodService is no longer registered against Service (or Any): + assertSame(GoodService2::class.java, lh[Service::class].javaClass) + assertSame(GoodService2::class.java, lh[Any::class].javaClass) + } + + class JParamExample(@Suppress("UNUSED_PARAMETER") str: String, @Suppress("UNUSED_PARAMETER") num: Int) + + @Test + fun `JParam has useful toString`() { + val c = JParamExample::class.java.constructors.single() + // Parameter doesn't expose its index, here we deliberately pass in the wrong one to see what happens: + val text = JParam(c.parameters[0], 1, IOException::class.java).toString() + assertThat(text, containsString(" #1 ")) + assertThat(text, anyOf(containsString(" str "), containsString(" arg0 "))) + assertThat(text, endsWith(c.toString())) + } + + private val sideEffects = mutableListOf() + private fun sideEffect1() { + sideEffects.add(1) + } + + private fun sideEffect2() { + sideEffects.add(2) + } + + @Test + fun `side-effects are idempotent as a consequence of caching of results`() { + lh.factory(this::sideEffect1) + assertEquals(listOf(Unit), lh.getAll(Unit::class)) + assertEquals(listOf(1), sideEffects) + lh.factory(this::sideEffect2) + assertEquals(listOf(Unit, Unit), lh.getAll(Unit::class)) // Get both results. + assertEquals(listOf(1, 2), sideEffects) // sideEffect1 didn't run again. + } + + @Test + fun `getAll returns empty list when there is nothing to return`() { + // This is in contrast to the exception thrown by an array param, which would not be useful to replicate here: + assertEquals(emptyList(), lh.getAll(IOException::class)) + } + + // Two params needed to make primary constructor the winner when both are satisfiable. + // It's probably true that the secondary will always trigger a CircularDependencyException, but LazyHub isn't clever enough to tell. + class InvocationSwitcher(@Suppress("UNUSED_PARAMETER") s: String, @Suppress("UNUSED_PARAMETER") t: String) { + @Suppress("unused") + constructor(same: InvocationSwitcher) : this(same.toString(), same.toString()) + } + + @Test + fun `chosen constructor is not set in stone`() { + lh.impl(InvocationSwitcher::class) + assertSame(CircularDependencyException::class.java, catchThrowable { lh[InvocationSwitcher::class] }.javaClass) + lh.obj("alt") + lh[InvocationSwitcher::class] // Succeeds via other constructor. + } + + class GreedinessUnits(@Suppress("UNUSED_PARAMETER") v: Array, @Suppress("UNUSED_PARAMETER") z: Int) { + // Two greediness units even though it's one provider repeated: + @Suppress("unused") + constructor(z1: Int, z2: Int) : this(emptyArray(), z1 + z2) + } + + @Test + fun `array param counts as one greediness unit`() { + lh.obj("x") + lh.obj("y") + lh.obj(100) + lh.impl(GreedinessUnits::class) + assertSame(NoUniqueGreediestSatisfiableConstructorException::class.java, catchThrowable { lh[GreedinessUnits::class] }.javaClass) + } + + interface TriangleBase + interface TriangleSide : TriangleBase + class TriangleImpl : TriangleBase, TriangleSide + + @Test + fun `provider registered exactly once against each supertype`() { + lh.impl(TriangleImpl::class) + lh[TriangleBase::class] // Don't throw TooManyProvidersException. + } + + interface Service1 + interface Service2 + class ServiceImpl1 : Service1, Service2 + class ServiceImpl2 : Service2 + + @Test + fun `do not leak empty provider list`() { + lh.impl(ServiceImpl1::class) + lh.impl(Service2::class, ServiceImpl2::class) + assertSame(NoSuchProviderException::class.java, catchThrowable { lh[Service1::class] }.javaClass) + } + + class Global + class Session(val global: Global, val local: Int) + + @Test + fun `child can be used to create a scope`() { + lh.impl(Global::class) + lh.factory(lh.child().also { + it.obj(1) + it.impl(Session::class) + }, Session::class) + lh.factory(lh.child().also { + it.obj(2) + it.impl(Session::class) + }, Session::class) + val sessions = lh.getAll(Session::class) + val g = lh[Global::class] + sessions.forEach { assertSame(g, it.global) } + assertEquals(listOf(1, 2), sessions.map { it.local }) + } +} diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 6fa98122a5..27a23d28d8 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -314,8 +314,8 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { return mockNet.createNode(MockNodeParameters(legalName = name), nodeFactory = { args -> object : MockNetwork.MockNode(args) { // That constructs a recording tx storage - override fun makeTransactionStorage(database: CordaPersistence): WritableTransactionStorage { - return RecordingTransactionStorage(database, super.makeTransactionStorage(database)) + override fun makeTransactionStorage(database: CordaPersistence, transactionCacheSizeBytes: Long): WritableTransactionStorage { + return RecordingTransactionStorage(database, super.makeTransactionStorage(database, transactionCacheSizeBytes)) } } }) diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt index 125ce66855..8020c4a5a1 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt @@ -10,6 +10,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.internal.configureDatabase +import net.corda.node.services.config.NodeConfiguration import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.* @@ -173,7 +174,7 @@ class DBTransactionStorageTests { private fun newTransactionStorage() { database.transaction { - transactionStorage = DBTransactionStorage() + transactionStorage = DBTransactionStorage(NodeConfiguration.defaultTransactionCacheSize) } } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt index 8241f2d373..25ffc6b11c 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt @@ -29,10 +29,11 @@ import net.corda.finance.sampleschemas.SampleCashSchemaV3 import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.utils.sumCash import net.corda.node.internal.configureDatabase -import net.corda.node.services.api.IdentityServiceInternal +import net.corda.node.services.schema.ContractStateAndRef import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.vault.VaultSchemaV1 +import net.corda.node.services.api.IdentityServiceInternal import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.HibernateConfiguration @@ -504,11 +505,12 @@ class HibernateConfigurationTest { fun `count CashStates in V2`() { database.transaction { // persist cash states explicitly with V2 schema - cashStates.forEach { + val stateAndRefs = cashStates.map { val cashState = it.state.data val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) - hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV2) + ContractStateAndRef(dummyFungibleState, it.ref) } + hibernatePersister.persistStatesWithSchema(stateAndRefs, SampleCashSchemaV2) } // structure query @@ -526,11 +528,12 @@ class HibernateConfigurationTest { database.transaction { vaultFiller.fillWithSomeTestLinearStates(5) // persist cash states explicitly with V2 schema - cashStates.forEach { + val stateAndRefs = cashStates.map { val cashState = it.state.data val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) - hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV2) + ContractStateAndRef(dummyFungibleState, it.ref) } + hibernatePersister.persistStatesWithSchema(stateAndRefs, SampleCashSchemaV2) } // structure query @@ -621,11 +624,12 @@ class HibernateConfigurationTest { fun `select fungible states by owner party`() { database.transaction { // persist original cash states explicitly with V3 schema - cashStates.forEach { + val stateAndRefs = cashStates.map { val cashState = it.state.data val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) - hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) + ContractStateAndRef(dummyFungibleState, it.ref) } + hibernatePersister.persistStatesWithSchema(stateAndRefs, SampleCashSchemaV3) } // structure query @@ -644,19 +648,20 @@ class HibernateConfigurationTest { fun `query fungible states by owner party`() { database.transaction { // persist original cash states explicitly with V3 schema - cashStates.forEach { + val stateAndRefs: MutableList = cashStates.map { val cashState = it.state.data val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) - hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) - } + ContractStateAndRef(dummyFungibleState, it.ref) + }.toMutableList() vaultFiller.fillWithSomeTestCash(100.DOLLARS, issuerServices, 2, issuer.ref(1), ALICE, Random(0L)) val cashStates = vaultFiller.fillWithSomeTestCash(100.DOLLARS, services, 2, identity.ref(0)).states // persist additional cash states explicitly with V3 schema - cashStates.forEach { + stateAndRefs.addAll(cashStates.map { val cashState = it.state.data val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) - hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) - } + ContractStateAndRef(dummyFungibleState, it.ref) + }) + hibernatePersister.persistStatesWithSchema(stateAndRefs, SampleCashSchemaV3) } val sessionFactory = sessionFactoryForSchemas(VaultSchemaV1, CommonSchemaV1, SampleCashSchemaV3) val criteriaBuilder = sessionFactory.criteriaBuilder @@ -695,12 +700,13 @@ class HibernateConfigurationTest { @Test fun `select fungible states by participants`() { database.transaction { - // persist cash states explicitly with V2 schema - cashStates.forEach { + // persist cash states explicitly with V3 schema + val stateAndRefs = cashStates.map { val cashState = it.state.data val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) - hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) + ContractStateAndRef(dummyFungibleState, it.ref) } + hibernatePersister.persistStatesWithSchema(stateAndRefs, SampleCashSchemaV3) } // structure query @@ -721,25 +727,27 @@ class HibernateConfigurationTest { val firstCashState = database.transaction { // persist original cash states explicitly with V3 schema - cashStates.forEach { + val stateAndRefs: MutableList = cashStates.map { val cashState = it.state.data val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) - hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) - } + ContractStateAndRef(dummyFungibleState, it.ref) + }.toMutableList() + val moreCash = vaultFiller.fillWithSomeTestCash(100.DOLLARS, services, 2, identity.ref(0), identity, Random(0L)).states // persist additional cash states explicitly with V3 schema - moreCash.forEach { + stateAndRefs.addAll(moreCash.map { val cashState = it.state.data val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) - hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) - } + ContractStateAndRef(dummyFungibleState, it.ref) + }) val cashStates = vaultFiller.fillWithSomeTestCash(100.DOLLARS, issuerServices, 2, issuer.ref(1), ALICE, Random(0L)).states // persist additional cash states explicitly with V3 schema - cashStates.forEach { + stateAndRefs.addAll(cashStates.map { val cashState = it.state.data val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) - hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) - } + ContractStateAndRef(dummyFungibleState, it.ref) + }) + hibernatePersister.persistStatesWithSchema(stateAndRefs, SampleCashSchemaV3) cashStates.first() } diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt index 77c5cf797b..a162c77a54 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt @@ -19,17 +19,17 @@ import net.corda.testing.contracts.DummyContract import net.corda.testing.dummyCommand import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNodeParameters -import net.corda.testing.singleIdentity import net.corda.testing.node.startFlow +import net.corda.testing.singleIdentity import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before -import org.junit.Ignore import org.junit.Test import java.time.Instant import java.util.* import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertTrue class NotaryServiceTests { private lateinit var mockNet: MockNetwork @@ -100,9 +100,8 @@ class NotaryServiceTests { assertThat(ex.error).isInstanceOf(NotaryError.TimeWindowInvalid::class.java) } - @Ignore("Only applies to deterministic signature schemes (e.g. EdDSA) and when deterministic metadata is attached (no timestamps or nonces)") @Test - fun `should sign identical transaction multiple times (signing is idempotent)`() { + fun `should sign identical transaction multiple times (notarisation is idempotent)`() { val stx = run { val inputState = issueState(aliceServices, alice) val tx = TransactionBuilder(notary) @@ -118,7 +117,16 @@ class NotaryServiceTests { mockNet.runNetwork() - assertEquals(f1.resultFuture.getOrThrow(), f2.resultFuture.getOrThrow()) + // Note that the notary will only return identical signatures when using deterministic signature + // schemes (e.g. EdDSA) and when deterministic metadata is attached (no timestamps or nonces). + // We only really care that both signatures are over the same transaction and by the same notary. + val sig1 = f1.resultFuture.getOrThrow().single() + assertEquals(sig1.by, notary.owningKey) + assertTrue(sig1.isValid(stx.id)) + + val sig2 = f2.resultFuture.getOrThrow().single() + assertEquals(sig2.by, notary.owningKey) + assertTrue(sig2.isValid(stx.id)) } @Test diff --git a/perftestcordapp/src/integrationTest/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/NoSelectionIntegrationTest.kt b/perftestcordapp/src/integrationTest/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/NoSelectionIntegrationTest.kt index bf0cbd457f..c58ea5fb46 100644 --- a/perftestcordapp/src/integrationTest/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/NoSelectionIntegrationTest.kt +++ b/perftestcordapp/src/integrationTest/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/NoSelectionIntegrationTest.kt @@ -5,9 +5,9 @@ import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.finance.DOLLARS import net.corda.node.services.Permissions -import net.corda.nodeapi.internal.config.User import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.driver +import net.corda.testing.node.User import org.junit.Ignore import org.junit.Test diff --git a/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/TwoPartyTradeFlowTest.kt b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/TwoPartyTradeFlowTest.kt index b41e869971..a70fd21d16 100644 --- a/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/TwoPartyTradeFlowTest.kt +++ b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/TwoPartyTradeFlowTest.kt @@ -328,8 +328,8 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { return mockNet.createNode(MockNodeParameters(legalName = name), nodeFactory = { args -> object : MockNetwork.MockNode(args) { // That constructs a recording tx storage - override fun makeTransactionStorage(database: CordaPersistence): WritableTransactionStorage { - return RecordingTransactionStorage(database, super.makeTransactionStorage(database)) + override fun makeTransactionStorage(database: CordaPersistence, transactionCacheSizeBytes: Long): WritableTransactionStorage { + return RecordingTransactionStorage(database, super.makeTransactionStorage(database, transactionCacheSizeBytes)) } } }) diff --git a/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt b/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt index b65cbb870f..c8238d8b75 100644 --- a/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt +++ b/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt @@ -4,8 +4,10 @@ import net.corda.core.messaging.CordaRPCOps import net.corda.core.utilities.getOrThrow import net.corda.node.services.Permissions.Companion.invokeRpc import net.corda.node.services.Permissions.Companion.startFlow -import net.corda.nodeapi.internal.config.User -import net.corda.testing.* +import net.corda.testing.DUMMY_BANK_A_NAME +import net.corda.testing.DUMMY_BANK_B_NAME +import net.corda.testing.DUMMY_NOTARY_NAME +import net.corda.testing.node.User import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.driver import net.corda.testing.internal.IntegrationTest diff --git a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt index 9f8c834519..44108c93c8 100644 --- a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt +++ b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt @@ -1,9 +1,9 @@ package net.corda.attachmentdemo import net.corda.core.internal.div -import net.corda.nodeapi.internal.config.User import net.corda.testing.DUMMY_BANK_A_NAME import net.corda.testing.DUMMY_BANK_B_NAME +import net.corda.testing.node.User import net.corda.testing.driver.driver /** diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt index ab1395dd00..5fd5731632 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt @@ -11,9 +11,9 @@ import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.CashIssueAndPaymentFlow import net.corda.node.services.Permissions.Companion.invokeRpc import net.corda.node.services.Permissions.Companion.startFlow -import net.corda.nodeapi.internal.config.User import net.corda.testing.* import net.corda.testing.driver.driver +import net.corda.testing.node.User import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.toDatabaseSchemaName diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt index b135d7baa1..9e7b1dae83 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt @@ -12,9 +12,9 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.Permissions.Companion.all import net.corda.node.services.config.NotaryConfig -import net.corda.nodeapi.internal.config.User import net.corda.testing.node.internal.demorun.* import net.corda.testing.BOC_NAME +import net.corda.testing.node.User import java.util.* import kotlin.system.exitProcess diff --git a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt index f981389c6b..8a7bc5f2b1 100644 --- a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt +++ b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt @@ -21,7 +21,6 @@ import net.corda.finance.plugin.registerFinanceJSONMappers import net.corda.irs.contract.InterestRateSwap import net.corda.irs.web.IrsDemoWebApplication import net.corda.node.services.config.NodeConfiguration -import net.corda.nodeapi.internal.config.User import net.corda.test.spring.springDriver import net.corda.testing.* import net.corda.testing.http.HttpApi @@ -29,6 +28,7 @@ import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.toDatabaseSchemaName import net.corda.testing.node.NotarySpec +import net.corda.testing.node.User import org.apache.commons.io.IOUtils import org.assertj.core.api.Assertions.assertThat import org.junit.ClassRule diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt index 1b2967c953..8e4028956f 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt @@ -62,7 +62,7 @@ class BFTNotaryCordform : CordformDefinition() { } override fun setup(context: CordformContext) { - DevIdentityGenerator.generateDistributedNotaryIdentity( + DevIdentityGenerator.generateDistributedNotaryCompositeIdentity( notaryNames.map { context.baseDirectory(it.toString()) }, clusterName, minCorrectReplicas(clusterSize) diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt index 6a59809cf6..a86a6be0ea 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt @@ -1,6 +1,7 @@ package net.corda.notarydemo import net.corda.client.rpc.CordaRPCClient +import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.toStringShort import net.corda.core.identity.PartyAndCertificate import net.corda.core.messaging.CordaRPCOps @@ -38,7 +39,8 @@ private class NotaryDemoClientApi(val rpc: CordaRPCOps) { /** Makes calls to the node rpc to start transaction notarisation. */ fun notarise(count: Int) { - println("Notary: \"${notary.name}\", with composite key: ${notary.owningKey.toStringShort()}") + val keyType = if (notary.owningKey is CompositeKey) "composite" else "public" + println("Notary: \"${notary.name}\", with $keyType key: ${notary.owningKey.toStringShort()}") val transactions = buildTransactions(count) println("Notarised ${transactions.size} transactions:") transactions.zip(notariseTransactions(transactions)).forEach { (tx, signersFuture) -> diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt index abceabbe77..f999ad68e6 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt @@ -58,7 +58,7 @@ class RaftNotaryCordform : CordformDefinition() { } override fun setup(context: CordformContext) { - DevIdentityGenerator.generateDistributedNotaryIdentity( + DevIdentityGenerator.generateDistributedNotarySingularIdentity( notaryNames.map { context.baseDirectory(it.toString()) }, clusterName ) diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt index 4910a57d3e..8386ccece9 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt @@ -4,9 +4,11 @@ import net.corda.cordform.CordformContext import net.corda.cordform.CordformDefinition import net.corda.node.services.Permissions.Companion.all import net.corda.node.services.config.NotaryConfig -import net.corda.nodeapi.internal.config.User +import net.corda.testing.ALICE_NAME +import net.corda.testing.BOB_NAME +import net.corda.testing.DUMMY_NOTARY_NAME +import net.corda.testing.node.User import net.corda.testing.node.internal.demorun.* -import net.corda.testing.* import java.nio.file.Paths fun main(args: Array) = SingleNotaryCordform().deployNodes() diff --git a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt index 594cffa8e4..b3383de2bb 100644 --- a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt +++ b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt @@ -8,13 +8,13 @@ import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.Permissions.Companion.all import net.corda.node.services.Permissions.Companion.startFlow -import net.corda.nodeapi.internal.config.User import net.corda.testing.* import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.toDatabaseSchemaName +import net.corda.testing.node.User import net.corda.testing.node.internal.poll import net.corda.traderdemo.flow.BuyerFlow import net.corda.traderdemo.flow.CommercialPaperIssueFlow diff --git a/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt index c3314ff155..6b4f7fab0e 100644 --- a/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt +++ b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt @@ -4,10 +4,10 @@ import net.corda.core.internal.div import net.corda.finance.flows.CashIssueFlow import net.corda.node.services.Permissions.Companion.all import net.corda.node.services.Permissions.Companion.startFlow -import net.corda.nodeapi.internal.config.User import net.corda.testing.BOC_NAME import net.corda.testing.DUMMY_BANK_A_NAME import net.corda.testing.DUMMY_BANK_B_NAME +import net.corda.testing.node.User import net.corda.testing.driver.driver import net.corda.traderdemo.flow.CommercialPaperIssueFlow import net.corda.traderdemo.flow.SellerFlow diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/node/FlowStackSnapshotTest.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/node/FlowStackSnapshotTest.kt index 4212902a73..17fdbc814a 100644 --- a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/node/FlowStackSnapshotTest.kt +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/node/FlowStackSnapshotTest.kt @@ -9,7 +9,6 @@ import net.corda.core.internal.read import net.corda.core.messaging.startFlow import net.corda.core.serialization.CordaSerializable import net.corda.node.services.Permissions.Companion.startFlow -import net.corda.nodeapi.internal.config.User import net.corda.testing.driver.driver import org.junit.Ignore import org.junit.Test diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index d0629c0470..4a8c6101c9 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -14,8 +14,8 @@ import net.corda.node.internal.Node import net.corda.node.internal.StartedNode import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.VerifierType -import net.corda.nodeapi.internal.config.User import net.corda.testing.DUMMY_NOTARY_NAME +import net.corda.testing.node.User import net.corda.testing.node.NotarySpec import net.corda.testing.node.internal.DriverDSLImpl import net.corda.testing.node.internal.genericDriver diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/DriverDSL.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/DriverDSL.kt index a4908d5f53..2cf3be566e 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/DriverDSL.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/DriverDSL.kt @@ -7,7 +7,7 @@ import net.corda.core.identity.Party import net.corda.core.internal.concurrent.map import net.corda.node.internal.Node import net.corda.node.services.config.VerifierType -import net.corda.nodeapi.internal.config.User +import net.corda.testing.node.User import net.corda.testing.node.NotarySpec import java.nio.file.Path diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index 331f1b940d..52575e5f4f 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -5,7 +5,6 @@ import com.google.common.jimfs.Jimfs import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever import net.corda.core.DoNotImplement -import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.Crypto import net.corda.core.crypto.random63BitValue import net.corda.core.identity.CordaX500Name @@ -15,17 +14,15 @@ import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.createDirectories import net.corda.core.internal.createDirectory import net.corda.core.internal.uncheckedCast -import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.MessageRecipients -import net.corda.core.messaging.RPCOps import net.corda.core.messaging.SingleMessageRecipient -import net.corda.core.node.NodeInfo import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.SerializationWhitelist import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.core.utilities.seconds +import net.corda.lazyhub.MutableLazyHub import net.corda.node.VersionInfo import net.corda.node.internal.AbstractNode import net.corda.node.internal.StartedNode @@ -295,9 +292,14 @@ open class MockNetwork(private val cordappPackages: List, } } + override fun configure(lh: MutableLazyHub) { + super.configure(lh) + lh.factory(this::makeMessagingService) + } + // We only need to override the messaging service here, as currently everything that hits disk does so // through the java.nio API which we are already mocking via Jimfs. - override fun makeMessagingService(database: CordaPersistence, info: NodeInfo): MessagingService { + private fun makeMessagingService(database: CordaPersistence): MessagingService { require(id >= 0) { "Node ID must be zero or positive, was passed: " + id } return mockNet.messagingNetwork.createNodeWithID( !mockNet.threadPerNode, @@ -316,14 +318,6 @@ open class MockNetwork(private val cordappPackages: List, return E2ETestKeyManagementService(identityService, keyPairs) } - override fun startShell(rpcOps: CordaRPCOps) { - //No mock shell - } - - override fun startMessagingService(rpcOps: RPCOps) { - // Nothing to do - } - // This is not thread safe, but node construction is done on a single thread, so that should always be fine override fun generateKeyPair(): KeyPair { counter = counter.add(BigInteger.ONE) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/NotarySpec.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/NotarySpec.kt index b6533ebd98..348de42338 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/NotarySpec.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/NotarySpec.kt @@ -3,7 +3,6 @@ package net.corda.testing.node import net.corda.core.DoNotImplement import net.corda.core.identity.CordaX500Name import net.corda.node.services.config.VerifierType -import net.corda.nodeapi.internal.config.User data class NotarySpec( val name: CordaX500Name, @@ -14,10 +13,12 @@ data class NotarySpec( ) @DoNotImplement -sealed class ClusterSpec { +abstract class ClusterSpec { abstract val clusterSize: Int - data class Raft(override val clusterSize: Int) : ClusterSpec() { + data class Raft( + override val clusterSize: Int + ) : ClusterSpec() { init { require(clusterSize > 0) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/User.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/User.kt new file mode 100644 index 0000000000..a60ea9fc26 --- /dev/null +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/User.kt @@ -0,0 +1,7 @@ +package net.corda.testing.node + +/** Object encapsulating a node rpc user and their associated permissions for use when testing */ +data class User( + val username: String, + val password: String, + val permissions: Set) \ No newline at end of file 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 f29fddf507..f9df016fdb 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 @@ -31,7 +31,6 @@ import net.corda.node.utilities.registration.NetworkRegistrationHelper import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.addShutdownHook -import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.parseAs import net.corda.nodeapi.internal.config.toConfig import net.corda.nodeapi.internal.crypto.X509Utilities @@ -49,6 +48,7 @@ import net.corda.testing.driver.* import net.corda.testing.node.ClusterSpec import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import net.corda.testing.node.NotarySpec +import net.corda.testing.node.User import net.corda.testing.node.internal.DriverDSLImpl.ClusterType.NON_VALIDATING_RAFT import net.corda.testing.node.internal.DriverDSLImpl.ClusterType.VALIDATING_RAFT import net.corda.testing.setGlobalSerialization @@ -74,6 +74,7 @@ import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger import kotlin.concurrent.thread +import net.corda.nodeapi.internal.config.User as InternalUser class DriverDSLImpl( val portAllocation: PortAllocation, @@ -127,8 +128,7 @@ class DriverDSLImpl( val jarPattern = jarNamePattern.toRegex() val jarFileUrl = urls.first { jarPattern.matches(it.path) } Paths.get(jarFileUrl.toURI()).toString() - } - catch(e: Exception) { + } catch (e: Exception) { log.warn("Unable to locate JAR `$jarNamePattern` on classpath: ${e.message}", e) throw e } @@ -269,7 +269,7 @@ class DriverDSLImpl( if (cordform.notary == null) continue val name = CordaX500Name.parse(cordform.name) val notaryConfig = ConfigFactory.parseMap(cordform.notary).parseAs() - // We need to first group the nodes that form part of a cluser. We assume for simplicity that nodes of the + // We need to first group the nodes that form part of a cluster. We assume for simplicity that nodes of the // same cluster type and validating flag are part of the same cluster. if (notaryConfig.raft != null) { val key = if (notaryConfig.validating) VALIDATING_RAFT else NON_VALIDATING_RAFT @@ -284,10 +284,17 @@ class DriverDSLImpl( } clusterNodes.asMap().forEach { type, nodeNames -> - val identity = DevIdentityGenerator.generateDistributedNotaryIdentity( - dirs = nodeNames.map { baseDirectory(it) }, - notaryName = type.clusterName - ) + val identity = if (type == ClusterType.NON_VALIDATING_RAFT || type == ClusterType.VALIDATING_RAFT) { + DevIdentityGenerator.generateDistributedNotarySingularIdentity( + dirs = nodeNames.map { baseDirectory(it) }, + notaryName = type.clusterName + ) + } else { + DevIdentityGenerator.generateDistributedNotaryCompositeIdentity( + dirs = nodeNames.map { baseDirectory(it) }, + notaryName = type.clusterName + ) + } notaryInfos += NotaryInfo(identity, type.validating) } @@ -385,13 +392,30 @@ class DriverDSLImpl( private fun startNotaryIdentityGeneration(): CordaFuture> { return executorService.fork { notarySpecs.map { spec -> - val identity = if (spec.cluster == null) { - DevIdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(spec.name), spec.name) - } else { - DevIdentityGenerator.generateDistributedNotaryIdentity( - dirs = generateNodeNames(spec).map { baseDirectory(it) }, - notaryName = spec.name - ) + val identity = when (spec.cluster) { + null -> { + DevIdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(spec.name), spec.name) + } + is ClusterSpec.Raft -> { + DevIdentityGenerator.generateDistributedNotarySingularIdentity( + dirs = generateNodeNames(spec).map { baseDirectory(it) }, + notaryName = spec.name + ) + } + is DummyClusterSpec -> { + if (spec.cluster.compositeServiceIdentity) { + DevIdentityGenerator.generateDistributedNotarySingularIdentity( + dirs = generateNodeNames(spec).map { baseDirectory(it) }, + notaryName = spec.name + ) + } else { + DevIdentityGenerator.generateDistributedNotaryCompositeIdentity( + dirs = generateNodeNames(spec).map { baseDirectory(it) }, + notaryName = spec.name + ) + } + } + else -> throw UnsupportedOperationException("Cluster spec ${spec.cluster} not supported by Driver") } NotaryInfo(identity, spec.validating) } @@ -436,9 +460,12 @@ class DriverDSLImpl( private fun startNotaries(localNetworkMap: LocalNetworkMap?): List>> { return notarySpecs.map { - when { - it.cluster == null -> startSingleNotary(it, localNetworkMap) - it.cluster is ClusterSpec.Raft -> startRaftNotaryCluster(it, localNetworkMap) + when (it.cluster) { + null -> startSingleNotary(it, localNetworkMap) + is ClusterSpec.Raft, + // DummyCluster is used for testing the notary communication path, and it does not matter + // which underlying consensus algorithm is used, so we just stick to Raft + is DummyClusterSpec -> startRaftNotaryCluster(it, localNetworkMap) else -> throw IllegalArgumentException("BFT-SMaRt not supported") } } @@ -658,7 +685,7 @@ class DriverDSLImpl( companion object { internal val log = contextLogger() - private val defaultRpcUserList = listOf(User("default", "default", setOf("ALL")).toConfig().root().unwrapped()) + private val defaultRpcUserList = listOf(InternalUser("default", "default", setOf("ALL")).toConfig().root().unwrapped()) private val names = arrayOf(ALICE_NAME, BOB_NAME, DUMMY_BANK_A_NAME) /** * A sub-set of permissions that grant most of the essential operations used in the unit/integration tests as well as @@ -721,7 +748,7 @@ class DriverDSLImpl( "name" to config.corda.myLegalName, "visualvm.display.name" to "corda-${config.corda.myLegalName}", "java.io.tmpdir" to System.getProperty("java.io.tmpdir"), // Inherit from parent process - "log4j2.debug" to if(debugPort != null) "true" else "false" + "log4j2.debug" to if (debugPort != null) "true" else "false" ) if (cordappPackages.isNotEmpty()) { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DummyClusterSpec.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DummyClusterSpec.kt new file mode 100644 index 0000000000..13a5d3df2a --- /dev/null +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DummyClusterSpec.kt @@ -0,0 +1,20 @@ +package net.corda.testing.node.internal + +import net.corda.testing.node.ClusterSpec + +/** + * Only used for testing the notary communication path. Can be configured to act as a Raft (singular identity), + * or a BFT (composite key identity) notary service. + */ +data class DummyClusterSpec( + override val clusterSize: Int, + /** + * If *true*, the cluster will use a shared composite public key for the service identity, with individual + * private keys. If *false*, the same "singular" key pair will be shared by all replicas. + */ + val compositeServiceIdentity: Boolean = false +) : ClusterSpec() { + init { + require(clusterSize > 0) + } +} \ No newline at end of file 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 93f6f32daa..430fa81d58 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 @@ -12,14 +12,15 @@ import net.corda.node.internal.Node import net.corda.node.internal.StartedNode import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.services.config.* -import net.corda.nodeapi.internal.config.User -import net.corda.testing.internal.IntegrationTest -import net.corda.testing.SerializationEnvironmentRule +import net.corda.nodeapi.internal.config.toConfig import net.corda.nodeapi.internal.network.NetworkParametersCopier +import net.corda.testing.SerializationEnvironmentRule import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.getFreeLocalPorts +import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.testThreadFactory import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO +import net.corda.testing.node.User import org.apache.logging.log4j.Level import org.junit.After import org.junit.Before @@ -95,7 +96,7 @@ abstract class NodeBasedTest(private val cordappPackages: List = emptyLi "myLegalName" to legalName.toString(), "p2pAddress" to p2pAddress, "rpcAddress" to localPort[1].toString(), - "rpcUsers" to rpcUsers.map { it.toMap() } + "rpcUsers" to rpcUsers.map { it.toConfig().root().unwrapped() } ) + configOverrides ) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt index a5dff915ca..5370276fb2 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt @@ -22,12 +22,12 @@ import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.RPCApi -import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.serialization.KRYO_RPC_CLIENT_CONTEXT import net.corda.testing.MAX_MESSAGE_SIZE import net.corda.testing.driver.JmxPolicy import net.corda.testing.driver.PortAllocation import net.corda.testing.node.NotarySpec +import net.corda.testing.node.User import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.TransportConfiguration import org.apache.activemq.artemis.api.core.client.ActiveMQClient @@ -52,6 +52,7 @@ import java.lang.reflect.Method import java.nio.file.Path import java.nio.file.Paths import java.util.* +import net.corda.nodeapi.internal.config.User as InternalUser inline fun RPCDriverDSL.startInVmRpcClient( username: String = rpcTestUser.username, @@ -433,7 +434,7 @@ data class RPCDriverDSL( minLargeMessageSize = MAX_MESSAGE_SIZE isUseGlobalPools = false } - val rpcSecurityManager = RPCSecurityManagerImpl.fromUserList(users = listOf(rpcUser), id = AuthServiceId("TEST_SECURITY_MANAGER")) + val rpcSecurityManager = RPCSecurityManagerImpl.fromUserList(users = listOf(InternalUser(rpcUser.username, rpcUser.password, rpcUser.permissions)) , id = AuthServiceId("TEST_SECURITY_MANAGER")) val rpcServer = RPCServer( ops, rpcUser.username, 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 57114425f6..6e5f7c02af 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 @@ -41,14 +41,14 @@ class ShutdownManager(private val executorService: ExecutorService) { } } - val shutdowns = shutdownActionFutures.map { Try.on { it.getOrThrow(1.seconds) } } + val shutdowns = shutdownActionFutures.map { Try.on { it.getOrThrow(60.seconds) } } shutdowns.reversed().forEach { when (it) { is Try.Success -> try { it.value() } catch (t: Throwable) { - log.warn("Exception while shutting down", t) + log.warn("Exception while calling a shutdown action, this might create resource leaks", 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/demorun/CordformUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/CordformUtils.kt index 25ab1bed6b..5807bd893b 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/CordformUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/CordformUtils.kt @@ -6,8 +6,8 @@ import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformNode import net.corda.core.identity.CordaX500Name import net.corda.node.services.config.NotaryConfig -import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.toConfig +import net.corda.testing.node.User fun CordformDefinition.node(configure: CordformNode.() -> Unit) { addNode { cordformNode -> cordformNode.configure() } @@ -16,7 +16,7 @@ fun CordformDefinition.node(configure: CordformNode.() -> Unit) { fun CordformNode.name(name: CordaX500Name) = name(name.toString()) fun CordformNode.rpcUsers(vararg users: User) { - rpcUsers = users.map { it.toMap() } + rpcUsers = users.map { it.toConfig().root().unwrapped() } } fun CordformNode.notary(notaryConfig: NotaryConfig) { diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt index 1f91cc92e8..c951f3e541 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -23,9 +23,9 @@ import net.corda.finance.flows.* import net.corda.finance.flows.CashExitFlow.ExitRequest import net.corda.finance.flows.CashIssueAndPaymentFlow.IssueAndPaymentRequest import net.corda.node.services.Permissions.Companion.startFlow -import net.corda.nodeapi.internal.config.User import net.corda.testing.ALICE_NAME import net.corda.testing.BOB_NAME +import net.corda.testing.node.User import net.corda.testing.driver.JmxPolicy import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.PortAllocation diff --git a/tools/jmeter/src/test/kotlin/com/r3/corda/jmeter/StartLocalPerfCorDapp.kt b/tools/jmeter/src/test/kotlin/com/r3/corda/jmeter/StartLocalPerfCorDapp.kt index 7f23b83893..15939f8731 100644 --- a/tools/jmeter/src/test/kotlin/com/r3/corda/jmeter/StartLocalPerfCorDapp.kt +++ b/tools/jmeter/src/test/kotlin/com/r3/corda/jmeter/StartLocalPerfCorDapp.kt @@ -2,9 +2,9 @@ package com.r3.corda.jmeter import net.corda.core.utilities.getOrThrow import net.corda.node.services.Permissions -import net.corda.nodeapi.internal.config.User import net.corda.testing.DUMMY_NOTARY_NAME import net.corda.testing.node.NotarySpec +import net.corda.testing.node.User import org.slf4j.LoggerFactory import java.io.BufferedReader import java.io.InputStreamReader